@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,118 @@
|
|
|
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
|
+
* Keep the media bucket's upload CORS in step with the names the console is
|
|
19
|
+
* SERVED on (AGL-1452).
|
|
20
|
+
*
|
|
21
|
+
* ## Why this is automation rather than a checklist line
|
|
22
|
+
*
|
|
23
|
+
* GCS matches the CORS `origin` list as an exact string — no subtree form —
|
|
24
|
+
* so every serving console name needs its own entry, and a missing one fails
|
|
25
|
+
* only on files large enough to take the signed direct-to-GCS path, behind a
|
|
26
|
+
* generic "try again" snackbar. Nothing about the symptom points at bucket
|
|
27
|
+
* configuration, and nothing at attach time pointed at it either.
|
|
28
|
+
*
|
|
29
|
+
* That was already costing us, not merely threatening to. Driving real
|
|
30
|
+
* preflights against `gs://aglyn-main.appspot.com` on 2026-08-20: of the
|
|
31
|
+
* console project's seventeen attached names, exactly one —
|
|
32
|
+
* `https://app.aglyn.com` — was permitted, while five names that serve the
|
|
33
|
+
* console at 200 (`zgover.aglyn.com`, `test-org.aglyn.com`,
|
|
34
|
+
* `aglyn-org.aglyn.com`, `sale-test.aglyn.com`, `zachary1748.aglyn.com`) were
|
|
35
|
+
* refused. AGL-1452 recorded workspace subdomains as latent because none were
|
|
36
|
+
* attached at the time; they are attached now.
|
|
37
|
+
*
|
|
38
|
+
* ## Serving names only
|
|
39
|
+
*
|
|
40
|
+
* A name attached as a REDIRECT never becomes a browser origin — the browser
|
|
41
|
+
* is already at the redirect target before it uploads — so it needs no entry.
|
|
42
|
+
* Measured: `console.aglyn.com` 308s and `app.aglyn.io` 307s to
|
|
43
|
+
* `app.aglyn.com`, and neither is in the CORS list, and neither is broken.
|
|
44
|
+
* `attachProjectDomain` therefore calls this only for a name it attached
|
|
45
|
+
* WITHOUT a redirect.
|
|
46
|
+
*
|
|
47
|
+
* ## When it cannot
|
|
48
|
+
*
|
|
49
|
+
* The runtime service account may not hold `storage.buckets.update`. That is
|
|
50
|
+
* a real and likely outcome, and the one thing this must not do about it is
|
|
51
|
+
* be quiet: the verdict carries `permitted: false` and the exact ordered
|
|
52
|
+
* read-modify-write command, so the failure is legible at attach time instead
|
|
53
|
+
* of arriving days later as a broken upload. It never throws and never blocks
|
|
54
|
+
* an attach — a domain must not fail to attach over a storage API.
|
|
55
|
+
*/
|
|
56
|
+
import { type CorsRule } from '@aglyn/aglyn/server';
|
|
57
|
+
export interface UploadCorsVerdict {
|
|
58
|
+
/** The origin a browser at this name will send. */
|
|
59
|
+
origin: string;
|
|
60
|
+
/** Can a signed `PUT` from that origin complete, now that we are done? */
|
|
61
|
+
permitted: boolean;
|
|
62
|
+
/** Origins this run added to the bucket. Empty when it was already covered. */
|
|
63
|
+
added: string[];
|
|
64
|
+
/** What a human must run, when we could not. Null when there is nothing owed. */
|
|
65
|
+
remedy: string | null;
|
|
66
|
+
/** Which step failed, for a caller that wants to branch. */
|
|
67
|
+
detail: string | null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The bucket, behind a seam.
|
|
71
|
+
*
|
|
72
|
+
* Injectable so the merge and the failure paths are exercised without a
|
|
73
|
+
* network — the reason AGL-1408 shipped broken is that the only leg nobody
|
|
74
|
+
* could test cheaply was the only leg that mattered.
|
|
75
|
+
*/
|
|
76
|
+
export interface BucketCorsIO {
|
|
77
|
+
bucket: string;
|
|
78
|
+
/** Null when the configuration could not be read at all. */
|
|
79
|
+
read(): Promise<{
|
|
80
|
+
rules: CorsRule[];
|
|
81
|
+
metageneration: string;
|
|
82
|
+
} | null>;
|
|
83
|
+
/**
|
|
84
|
+
* Write the merged configuration back, conditional on the metageneration
|
|
85
|
+
* that was read — two attaches racing would otherwise clobber each other.
|
|
86
|
+
*/
|
|
87
|
+
write(rules: CorsRule[], metageneration: string): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
/** The live bucket, or null when this deployment has no media bucket configured. */
|
|
90
|
+
export declare function liveBucketCorsIO(): BucketCorsIO | null;
|
|
91
|
+
export declare function reconcileUploadCors(host: string, io?: BucketCorsIO | null): Promise<UploadCorsVerdict | null>;
|
|
92
|
+
/** What a detach did, or could not do, about the origin it left behind. */
|
|
93
|
+
export interface UploadCorsRelease {
|
|
94
|
+
/** The origin the detached name used to send. */
|
|
95
|
+
origin: string;
|
|
96
|
+
/** Is that origin gone from the bucket now? */
|
|
97
|
+
revoked: boolean;
|
|
98
|
+
/** Why not, for a caller that wants to branch. Null when it is gone. */
|
|
99
|
+
detail: string | null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Take an origin back off the bucket when its name stops serving the console
|
|
103
|
+
* (AGL-1452).
|
|
104
|
+
*
|
|
105
|
+
* The mirror of `reconcileUploadCors`, and the half that was missing: attaching
|
|
106
|
+
* added an origin and detaching left it. That is a standing permission for a
|
|
107
|
+
* host we no longer serve — and for a white-label customer domain, a host that
|
|
108
|
+
* somebody else still controls, on a bucket where the signed URL IS the
|
|
109
|
+
* authorization.
|
|
110
|
+
*
|
|
111
|
+
* Same three properties as the attach side. It never throws, never blocks a
|
|
112
|
+
* detach, and reports what it could not do rather than going quiet: a domain
|
|
113
|
+
* must not fail to detach because a storage API was slow, and the operator who
|
|
114
|
+
* detached a customer must be able to see that the permission outlived them.
|
|
115
|
+
*
|
|
116
|
+
* Refuses the platform origin structurally, not by convention.
|
|
117
|
+
*/
|
|
118
|
+
export declare function releaseUploadCors(host: string, io?: BucketCorsIO | null): Promise<UploadCorsRelease | null>;
|
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
* Keep the media bucket's upload CORS in step with the names the console is
|
|
18
|
+
* SERVED on (AGL-1452).
|
|
19
|
+
*
|
|
20
|
+
* ## Why this is automation rather than a checklist line
|
|
21
|
+
*
|
|
22
|
+
* GCS matches the CORS `origin` list as an exact string — no subtree form —
|
|
23
|
+
* so every serving console name needs its own entry, and a missing one fails
|
|
24
|
+
* only on files large enough to take the signed direct-to-GCS path, behind a
|
|
25
|
+
* generic "try again" snackbar. Nothing about the symptom points at bucket
|
|
26
|
+
* configuration, and nothing at attach time pointed at it either.
|
|
27
|
+
*
|
|
28
|
+
* That was already costing us, not merely threatening to. Driving real
|
|
29
|
+
* preflights against `gs://aglyn-main.appspot.com` on 2026-08-20: of the
|
|
30
|
+
* console project's seventeen attached names, exactly one —
|
|
31
|
+
* `https://app.aglyn.com` — was permitted, while five names that serve the
|
|
32
|
+
* console at 200 (`zgover.aglyn.com`, `test-org.aglyn.com`,
|
|
33
|
+
* `aglyn-org.aglyn.com`, `sale-test.aglyn.com`, `zachary1748.aglyn.com`) were
|
|
34
|
+
* refused. AGL-1452 recorded workspace subdomains as latent because none were
|
|
35
|
+
* attached at the time; they are attached now.
|
|
36
|
+
*
|
|
37
|
+
* ## Serving names only
|
|
38
|
+
*
|
|
39
|
+
* A name attached as a REDIRECT never becomes a browser origin — the browser
|
|
40
|
+
* is already at the redirect target before it uploads — so it needs no entry.
|
|
41
|
+
* Measured: `console.aglyn.com` 308s and `app.aglyn.io` 307s to
|
|
42
|
+
* `app.aglyn.com`, and neither is in the CORS list, and neither is broken.
|
|
43
|
+
* `attachProjectDomain` therefore calls this only for a name it attached
|
|
44
|
+
* WITHOUT a redirect.
|
|
45
|
+
*
|
|
46
|
+
* ## When it cannot
|
|
47
|
+
*
|
|
48
|
+
* The runtime service account may not hold `storage.buckets.update`. That is
|
|
49
|
+
* a real and likely outcome, and the one thing this must not do about it is
|
|
50
|
+
* be quiet: the verdict carries `permitted: false` and the exact ordered
|
|
51
|
+
* read-modify-write command, so the failure is legible at attach time instead
|
|
52
|
+
* of arriving days later as a broken upload. It never throws and never blocks
|
|
53
|
+
* an attach — a domain must not fail to attach over a storage API.
|
|
54
|
+
*/ import { mergeUploadOrigins, permitsUploadOrigin, revokeUploadOrigins, uploadCorsRemedy, uploadOriginFor } from "@aglyn/aglyn/server";
|
|
55
|
+
/**
|
|
56
|
+
* The one origin a release must never take away.
|
|
57
|
+
*
|
|
58
|
+
* Configured rather than provisioned, so it will never appear in a detach —
|
|
59
|
+
* but the guard is here rather than at the call site because the cost of
|
|
60
|
+
* getting it wrong is every customer's large uploads at once, and a guard that
|
|
61
|
+
* lives at the call site is a guard the next call site does not have.
|
|
62
|
+
*/ function platformOrigin() {
|
|
63
|
+
var _process_env_NEXT_PUBLIC_CONSOLE_URL;
|
|
64
|
+
return uploadOriginFor((_process_env_NEXT_PUBLIC_CONSOLE_URL = process.env['NEXT_PUBLIC_CONSOLE_URL']) != null ? _process_env_NEXT_PUBLIC_CONSOLE_URL : 'app.aglyn.com');
|
|
65
|
+
}
|
|
66
|
+
/** The live bucket, or null when this deployment has no media bucket configured. */ export function liveBucketCorsIO() {
|
|
67
|
+
var _process_env_NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET;
|
|
68
|
+
const bucket = String((_process_env_NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET = process.env['NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET']) != null ? _process_env_NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET : '').trim();
|
|
69
|
+
if (!bucket) return null;
|
|
70
|
+
// Imported lazily, not at module scope. `firebase-admin.ts` builds a
|
|
71
|
+
// Firebase app on load, and this module is imported by `workspace-domains`,
|
|
72
|
+
// which had no Firebase dependency at all and must keep none — a domain
|
|
73
|
+
// attach is an HTTP call to Vercel and nothing more.
|
|
74
|
+
const handle = async ()=>{
|
|
75
|
+
const { firebaseAdmin } = await import("./firebase-admin.js");
|
|
76
|
+
return firebaseAdmin.app().storage().bucket(bucket);
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
bucket,
|
|
80
|
+
read: async ()=>{
|
|
81
|
+
var _ref, _ref1;
|
|
82
|
+
const [metadata] = await (await handle()).getMetadata();
|
|
83
|
+
return {
|
|
84
|
+
rules: (_ref = metadata == null ? void 0 : metadata.cors) != null ? _ref : [],
|
|
85
|
+
metageneration: String((_ref1 = metadata == null ? void 0 : metadata.metageneration) != null ? _ref1 : '')
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
write: async (rules, metageneration)=>{
|
|
89
|
+
await (await handle()).setMetadata({
|
|
90
|
+
cors: rules
|
|
91
|
+
}, metageneration ? {
|
|
92
|
+
ifMetagenerationMatch: metageneration
|
|
93
|
+
} : {});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export async function reconcileUploadCors(host, io = liveBucketCorsIO()) {
|
|
98
|
+
const origin = uploadOriginFor(host);
|
|
99
|
+
if (!origin || !io) return null;
|
|
100
|
+
const refused = (detail)=>({
|
|
101
|
+
origin,
|
|
102
|
+
permitted: false,
|
|
103
|
+
added: [],
|
|
104
|
+
remedy: uploadCorsRemedy(io.bucket, [
|
|
105
|
+
origin
|
|
106
|
+
]),
|
|
107
|
+
detail
|
|
108
|
+
});
|
|
109
|
+
let current;
|
|
110
|
+
try {
|
|
111
|
+
current = await io.read();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('[upload-cors] could not read bucket CORS', io.bucket, error);
|
|
114
|
+
return refused('read-failed');
|
|
115
|
+
}
|
|
116
|
+
if (!current) return refused('read-failed');
|
|
117
|
+
if (permitsUploadOrigin(current.rules, origin)) {
|
|
118
|
+
return {
|
|
119
|
+
origin,
|
|
120
|
+
permitted: true,
|
|
121
|
+
added: [],
|
|
122
|
+
remedy: null,
|
|
123
|
+
detail: null
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
let merged;
|
|
127
|
+
try {
|
|
128
|
+
merged = mergeUploadOrigins(current.rules, [
|
|
129
|
+
origin
|
|
130
|
+
]);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// `mergeUploadOrigins` throws only on a wildcard, which `uploadOriginFor`
|
|
133
|
+
// has already ruled out — so reaching here means the invariant moved.
|
|
134
|
+
console.error('[upload-cors] refused to build a policy', origin, error);
|
|
135
|
+
return refused('merge-refused');
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await io.write(merged.rules, current.metageneration);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('[upload-cors] could not write bucket CORS — large uploads from', origin, 'will fail until a human runs the command in the verdict', error);
|
|
141
|
+
return refused('write-failed');
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
origin,
|
|
145
|
+
permitted: true,
|
|
146
|
+
added: merged.added,
|
|
147
|
+
remedy: null,
|
|
148
|
+
detail: null
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Take an origin back off the bucket when its name stops serving the console
|
|
153
|
+
* (AGL-1452).
|
|
154
|
+
*
|
|
155
|
+
* The mirror of `reconcileUploadCors`, and the half that was missing: attaching
|
|
156
|
+
* added an origin and detaching left it. That is a standing permission for a
|
|
157
|
+
* host we no longer serve — and for a white-label customer domain, a host that
|
|
158
|
+
* somebody else still controls, on a bucket where the signed URL IS the
|
|
159
|
+
* authorization.
|
|
160
|
+
*
|
|
161
|
+
* Same three properties as the attach side. It never throws, never blocks a
|
|
162
|
+
* detach, and reports what it could not do rather than going quiet: a domain
|
|
163
|
+
* must not fail to detach because a storage API was slow, and the operator who
|
|
164
|
+
* detached a customer must be able to see that the permission outlived them.
|
|
165
|
+
*
|
|
166
|
+
* Refuses the platform origin structurally, not by convention.
|
|
167
|
+
*/ export async function releaseUploadCors(host, io = liveBucketCorsIO()) {
|
|
168
|
+
var _platformOrigin;
|
|
169
|
+
const origin = uploadOriginFor(host);
|
|
170
|
+
if (!origin || !io) return null;
|
|
171
|
+
if (origin === platformOrigin()) {
|
|
172
|
+
return {
|
|
173
|
+
origin,
|
|
174
|
+
revoked: false,
|
|
175
|
+
detail: 'platform-origin'
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
let current;
|
|
179
|
+
try {
|
|
180
|
+
current = await io.read();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('[upload-cors] could not read bucket CORS', io.bucket, error);
|
|
183
|
+
return {
|
|
184
|
+
origin,
|
|
185
|
+
revoked: false,
|
|
186
|
+
detail: 'read-failed'
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (!current) return {
|
|
190
|
+
origin,
|
|
191
|
+
revoked: false,
|
|
192
|
+
detail: 'read-failed'
|
|
193
|
+
};
|
|
194
|
+
// Already gone is the success case, not a no-op to report as a failure: a
|
|
195
|
+
// detach that runs twice must not read as a permission that would not go.
|
|
196
|
+
if (!permitsUploadOrigin(current.rules, origin)) {
|
|
197
|
+
return {
|
|
198
|
+
origin,
|
|
199
|
+
revoked: true,
|
|
200
|
+
detail: null
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const { rules, removed } = revokeUploadOrigins(current.rules, [
|
|
204
|
+
origin
|
|
205
|
+
], {
|
|
206
|
+
keep: [
|
|
207
|
+
(_platformOrigin = platformOrigin()) != null ? _platformOrigin : ''
|
|
208
|
+
].filter(Boolean)
|
|
209
|
+
});
|
|
210
|
+
if (removed.length === 0) {
|
|
211
|
+
return {
|
|
212
|
+
origin,
|
|
213
|
+
revoked: false,
|
|
214
|
+
detail: 'refused'
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
await io.write(rules, current.metageneration);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error('[upload-cors] could not revoke bucket CORS —', origin, 'keeps a standing permission to complete signed uploads; run', 'npm run check:upload-cors -- --prune', error);
|
|
221
|
+
return {
|
|
222
|
+
origin,
|
|
223
|
+
revoked: false,
|
|
224
|
+
detail: 'write-failed'
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
origin,
|
|
229
|
+
revoked: true,
|
|
230
|
+
detail: null
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
//# sourceMappingURL=upload-cors-reconcile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/upload-cors-reconcile.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 * Keep the media bucket's upload CORS in step with the names the console is\n * SERVED on (AGL-1452).\n *\n * ## Why this is automation rather than a checklist line\n *\n * GCS matches the CORS `origin` list as an exact string — no subtree form —\n * so every serving console name needs its own entry, and a missing one fails\n * only on files large enough to take the signed direct-to-GCS path, behind a\n * generic \"try again\" snackbar. Nothing about the symptom points at bucket\n * configuration, and nothing at attach time pointed at it either.\n *\n * That was already costing us, not merely threatening to. Driving real\n * preflights against `gs://aglyn-main.appspot.com` on 2026-08-20: of the\n * console project's seventeen attached names, exactly one —\n * `https://app.aglyn.com` — was permitted, while five names that serve the\n * console at 200 (`zgover.aglyn.com`, `test-org.aglyn.com`,\n * `aglyn-org.aglyn.com`, `sale-test.aglyn.com`, `zachary1748.aglyn.com`) were\n * refused. AGL-1452 recorded workspace subdomains as latent because none were\n * attached at the time; they are attached now.\n *\n * ## Serving names only\n *\n * A name attached as a REDIRECT never becomes a browser origin — the browser\n * is already at the redirect target before it uploads — so it needs no entry.\n * Measured: `console.aglyn.com` 308s and `app.aglyn.io` 307s to\n * `app.aglyn.com`, and neither is in the CORS list, and neither is broken.\n * `attachProjectDomain` therefore calls this only for a name it attached\n * WITHOUT a redirect.\n *\n * ## When it cannot\n *\n * The runtime service account may not hold `storage.buckets.update`. That is\n * a real and likely outcome, and the one thing this must not do about it is\n * be quiet: the verdict carries `permitted: false` and the exact ordered\n * read-modify-write command, so the failure is legible at attach time instead\n * of arriving days later as a broken upload. It never throws and never blocks\n * an attach — a domain must not fail to attach over a storage API.\n */\n\nimport {\n mergeUploadOrigins,\n permitsUploadOrigin,\n revokeUploadOrigins,\n uploadCorsRemedy,\n uploadOriginFor,\n type CorsRule,\n} from '@aglyn/aglyn/server'\n\n/**\n * The one origin a release must never take away.\n *\n * Configured rather than provisioned, so it will never appear in a detach —\n * but the guard is here rather than at the call site because the cost of\n * getting it wrong is every customer's large uploads at once, and a guard that\n * lives at the call site is a guard the next call site does not have.\n */\nfunction platformOrigin(): string | null {\n return uploadOriginFor(\n process.env['NEXT_PUBLIC_CONSOLE_URL'] ?? 'app.aglyn.com',\n )\n}\n\nexport interface UploadCorsVerdict {\n /** The origin a browser at this name will send. */\n origin: string\n /** Can a signed `PUT` from that origin complete, now that we are done? */\n permitted: boolean\n /** Origins this run added to the bucket. Empty when it was already covered. */\n added: string[]\n /** What a human must run, when we could not. Null when there is nothing owed. */\n remedy: string | null\n /** Which step failed, for a caller that wants to branch. */\n detail: string | null\n}\n\n/**\n * The bucket, behind a seam.\n *\n * Injectable so the merge and the failure paths are exercised without a\n * network — the reason AGL-1408 shipped broken is that the only leg nobody\n * could test cheaply was the only leg that mattered.\n */\nexport interface BucketCorsIO {\n bucket: string\n /** Null when the configuration could not be read at all. */\n read(): Promise<{ rules: CorsRule[]; metageneration: string } | null>\n /**\n * Write the merged configuration back, conditional on the metageneration\n * that was read — two attaches racing would otherwise clobber each other.\n */\n write(rules: CorsRule[], metageneration: string): Promise<void>\n}\n\n/** The live bucket, or null when this deployment has no media bucket configured. */\nexport function liveBucketCorsIO(): BucketCorsIO | null {\n const bucket = String(\n process.env['NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET'] ?? '',\n ).trim()\n if (!bucket) return null\n // Imported lazily, not at module scope. `firebase-admin.ts` builds a\n // Firebase app on load, and this module is imported by `workspace-domains`,\n // which had no Firebase dependency at all and must keep none — a domain\n // attach is an HTTP call to Vercel and nothing more.\n const handle = async () => {\n const { firebaseAdmin } = await import('./firebase-admin')\n return firebaseAdmin.app().storage().bucket(bucket)\n }\n return {\n bucket,\n read: async () => {\n const [metadata] = await (await handle()).getMetadata()\n return {\n rules: (metadata?.cors ?? []) as CorsRule[],\n metageneration: String(metadata?.metageneration ?? ''),\n }\n },\n write: async (rules, metageneration) => {\n await (await handle()).setMetadata(\n { cors: rules },\n metageneration ? { ifMetagenerationMatch: metageneration } : {},\n )\n },\n }\n}\n\nexport async function reconcileUploadCors(\n host: string,\n io: BucketCorsIO | null = liveBucketCorsIO(),\n): Promise<UploadCorsVerdict | null> {\n const origin = uploadOriginFor(host)\n if (!origin || !io) return null\n\n const refused = (detail: string): UploadCorsVerdict => ({\n origin,\n permitted: false,\n added: [],\n remedy: uploadCorsRemedy(io.bucket, [origin]),\n detail,\n })\n\n let current: { rules: CorsRule[]; metageneration: string } | null\n try {\n current = await io.read()\n } catch (error) {\n console.error('[upload-cors] could not read bucket CORS', io.bucket, error)\n return refused('read-failed')\n }\n if (!current) return refused('read-failed')\n\n if (permitsUploadOrigin(current.rules, origin)) {\n return { origin, permitted: true, added: [], remedy: null, detail: null }\n }\n\n let merged: { rules: CorsRule[]; added: string[] }\n try {\n merged = mergeUploadOrigins(current.rules, [origin])\n } catch (error) {\n // `mergeUploadOrigins` throws only on a wildcard, which `uploadOriginFor`\n // has already ruled out — so reaching here means the invariant moved.\n console.error('[upload-cors] refused to build a policy', origin, error)\n return refused('merge-refused')\n }\n\n try {\n await io.write(merged.rules, current.metageneration)\n } catch (error) {\n console.error(\n '[upload-cors] could not write bucket CORS — large uploads from',\n origin,\n 'will fail until a human runs the command in the verdict',\n error,\n )\n return refused('write-failed')\n }\n\n return {\n origin,\n permitted: true,\n added: merged.added,\n remedy: null,\n detail: null,\n }\n}\n\n/** What a detach did, or could not do, about the origin it left behind. */\nexport interface UploadCorsRelease {\n /** The origin the detached name used to send. */\n origin: string\n /** Is that origin gone from the bucket now? */\n revoked: boolean\n /** Why not, for a caller that wants to branch. Null when it is gone. */\n detail: string | null\n}\n\n/**\n * Take an origin back off the bucket when its name stops serving the console\n * (AGL-1452).\n *\n * The mirror of `reconcileUploadCors`, and the half that was missing: attaching\n * added an origin and detaching left it. That is a standing permission for a\n * host we no longer serve — and for a white-label customer domain, a host that\n * somebody else still controls, on a bucket where the signed URL IS the\n * authorization.\n *\n * Same three properties as the attach side. It never throws, never blocks a\n * detach, and reports what it could not do rather than going quiet: a domain\n * must not fail to detach because a storage API was slow, and the operator who\n * detached a customer must be able to see that the permission outlived them.\n *\n * Refuses the platform origin structurally, not by convention.\n */\nexport async function releaseUploadCors(\n host: string,\n io: BucketCorsIO | null = liveBucketCorsIO(),\n): Promise<UploadCorsRelease | null> {\n const origin = uploadOriginFor(host)\n if (!origin || !io) return null\n if (origin === platformOrigin()) {\n return { origin, revoked: false, detail: 'platform-origin' }\n }\n\n let current: { rules: CorsRule[]; metageneration: string } | null\n try {\n current = await io.read()\n } catch (error) {\n console.error('[upload-cors] could not read bucket CORS', io.bucket, error)\n return { origin, revoked: false, detail: 'read-failed' }\n }\n if (!current) return { origin, revoked: false, detail: 'read-failed' }\n\n // Already gone is the success case, not a no-op to report as a failure: a\n // detach that runs twice must not read as a permission that would not go.\n if (!permitsUploadOrigin(current.rules, origin)) {\n return { origin, revoked: true, detail: null }\n }\n\n const { rules, removed } = revokeUploadOrigins(current.rules, [origin], {\n keep: [platformOrigin() ?? ''].filter(Boolean),\n })\n if (removed.length === 0) {\n return { origin, revoked: false, detail: 'refused' }\n }\n\n try {\n await io.write(rules, current.metageneration)\n } catch (error) {\n console.error(\n '[upload-cors] could not revoke bucket CORS —',\n origin,\n 'keeps a standing permission to complete signed uploads; run',\n 'npm run check:upload-cors -- --prune',\n error,\n )\n return { origin, revoked: false, detail: 'write-failed' }\n }\n return { origin, revoked: true, detail: null }\n}\n"],"names":["mergeUploadOrigins","permitsUploadOrigin","revokeUploadOrigins","uploadCorsRemedy","uploadOriginFor","platformOrigin","process","env","liveBucketCorsIO","bucket","String","trim","handle","firebaseAdmin","app","storage","read","metadata","getMetadata","rules","cors","metageneration","write","setMetadata","ifMetagenerationMatch","reconcileUploadCors","host","io","origin","refused","detail","permitted","added","remedy","current","error","console","merged","releaseUploadCors","revoked","removed","keep","filter","Boolean","length"],"mappings":"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"}
|
|
@@ -0,0 +1,262 @@
|
|
|
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 ContactFacet, type ContactInteraction, type ContactLifecycleStage, type ContactSource } from '@aglyn/aglyn/server';
|
|
18
|
+
import { type ResolvedCampaignTouch } from './campaign-conversion-attribution';
|
|
19
|
+
import { type MarketingConsentSource } from '@aglyn/aglyn/app-utils/marketing-consent';
|
|
20
|
+
/**
|
|
21
|
+
* What an upsert did, for the callers that need to know (AGL-2602).
|
|
22
|
+
*
|
|
23
|
+
* The capture doors never look: a form submission or an order must succeed
|
|
24
|
+
* whatever happened to the CRM record, which is why this function swallows
|
|
25
|
+
* its own errors and why every existing caller `await`s it for its side
|
|
26
|
+
* effect alone. An IMPORT is the caller that has to know, row by row —
|
|
27
|
+
* "created" and "merged" are its two headline numbers, and a row the
|
|
28
|
+
* audience band refused has to be handed back to the operator as a row
|
|
29
|
+
* rather than becoming one more tick on a counter nobody reconciles against
|
|
30
|
+
* a file. A verdict is returned rather than thrown so the swallowing stays:
|
|
31
|
+
* `refused: 'error'` is the same silence the doors have always had, now with
|
|
32
|
+
* a name.
|
|
33
|
+
*/
|
|
34
|
+
export type UpsertHostContactVerdict = {
|
|
35
|
+
contactId: string;
|
|
36
|
+
/** True when this call created the row; false when it merged into one. */
|
|
37
|
+
created: boolean;
|
|
38
|
+
} | {
|
|
39
|
+
/**
|
|
40
|
+
* `erased`: the site holds an erasure row for the address (AGL-2623),
|
|
41
|
+
* so no record is created — a capture must not quietly rebuild a
|
|
42
|
+
* person the workspace erased. Only a CREATE is refused; a merge
|
|
43
|
+
* cannot arise, because the erasure removed the row it would merge
|
|
44
|
+
* into.
|
|
45
|
+
*/
|
|
46
|
+
refused: 'invalid-email' | 'band' | 'erased' | 'error';
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The per-holder profile fields a door may write alongside the identity.
|
|
50
|
+
*
|
|
51
|
+
* `Pick`ed from the facet rather than typed afresh so the two cannot drift:
|
|
52
|
+
* a field the facet grows is a field this option may carry the moment the
|
|
53
|
+
* pick names it, and one it does not name is refused at compile time.
|
|
54
|
+
*/
|
|
55
|
+
export type UpsertHostContactFacet = Partial<Pick<ContactFacet, 'phone' | 'jobTitle' | 'companyId' | 'address' | 'ownerUid' | 'lifecycleStage' | 'custom'>>;
|
|
56
|
+
/**
|
|
57
|
+
* The profile fields a door may hand this function (AGL-2596): the parts of
|
|
58
|
+
* a person's record that no capture surface collects — the console's create
|
|
59
|
+
* drawer and the import do, and the order door adds the stage. `custom` is
|
|
60
|
+
* the holder's own field values, keyed by `ContactFieldDefinition.key`; the
|
|
61
|
+
* import maps spreadsheet columns onto them, and the definitions live under
|
|
62
|
+
* the same group the values are written to.
|
|
63
|
+
*/
|
|
64
|
+
export type ContactProfileInput = UpsertHostContactFacet;
|
|
65
|
+
/**
|
|
66
|
+
* Contacts ingestion (AGL-197): upserts an org-scoped contact doc (AGL-237)
|
|
67
|
+
* keyed by normalized email from any capture point (forms, membership,
|
|
68
|
+
* orders, bookings). Fire-and-forget by design — callers should never
|
|
69
|
+
* fail their primary write because contact capture had a problem.
|
|
70
|
+
*
|
|
71
|
+
* Quota (AGL-890): contacts are audience BANDS, not hard caps. Paid plans
|
|
72
|
+
* always create — contacts past the included band meter onto the monthly
|
|
73
|
+
* invoice (report-usage cron). Free hard-bands at the included count:
|
|
74
|
+
* only there do dropped creations increment `counters/contactsDropped`,
|
|
75
|
+
* surfaced as a console alert (AGL-891). Interactions on existing
|
|
76
|
+
* contacts always append regardless of plan.
|
|
77
|
+
*/
|
|
78
|
+
/**
|
|
79
|
+
* What a capture door learns when its capture made a NEW person.
|
|
80
|
+
*
|
|
81
|
+
* Handed to {@link UpsertHostContactOptions.onCreated} once, on the create
|
|
82
|
+
* branch only. The merge branch is a visit by somebody the org already held,
|
|
83
|
+
* which is another interaction and not a new contact — the same line
|
|
84
|
+
* `campaignTouch` draws for attribution. Scalars and one string array, so
|
|
85
|
+
* the runtime can flatten it into an event payload without inventing keys.
|
|
86
|
+
*/
|
|
87
|
+
export interface HostContactCreated {
|
|
88
|
+
contactId: string;
|
|
89
|
+
hostId: string;
|
|
90
|
+
email: string;
|
|
91
|
+
name?: string;
|
|
92
|
+
source: ContactSource;
|
|
93
|
+
/** The capture surface's campaigns, normalized — `[]` when it had none. */
|
|
94
|
+
campaignIds: string[];
|
|
95
|
+
/**
|
|
96
|
+
* The stage the create wrote onto the capturing facet — the door's
|
|
97
|
+
* {@link UpsertHostContactOptions.initialLifecycleStage}, or the profile's
|
|
98
|
+
* own — and absent when the capture named none, so an automation can
|
|
99
|
+
* filter `lifecycleStage == "lead"` on the day the person appears.
|
|
100
|
+
*/
|
|
101
|
+
lifecycleStage?: ContactLifecycleStage;
|
|
102
|
+
}
|
|
103
|
+
export interface UpsertHostContactOptions {
|
|
104
|
+
hostId: string;
|
|
105
|
+
email: unknown;
|
|
106
|
+
name?: string;
|
|
107
|
+
source: ContactSource;
|
|
108
|
+
interaction: Omit<ContactInteraction, 'type' | 'atMs'> & {
|
|
109
|
+
atMs?: number;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Explicit marketing opt-in (AGL-301) with a consent timestamp, recorded
|
|
113
|
+
* against {@link hostId} — the brand whose form carried the checkbox.
|
|
114
|
+
*/
|
|
115
|
+
marketingConsent?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* An explicit REFUSAL, recorded against {@link hostId} (AGL-3185).
|
|
118
|
+
*
|
|
119
|
+
* Its own flag rather than `marketingConsent: false`, because every door
|
|
120
|
+
* that passes a checkbox's value passes `false` for a box left alone, and
|
|
121
|
+
* a box left alone records nothing — absence is the third state, and
|
|
122
|
+
* turning it into a refusal would silently make every unticked visitor
|
|
123
|
+
* unmailable. A door sets this only for an act of refusal: a switch turned
|
|
124
|
+
* off, a prompt answered no. Ignored when {@link marketingConsent} is true.
|
|
125
|
+
*/
|
|
126
|
+
declineMarketingConsent?: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* The provenance stored on the consent entry {@link marketingConsent} or
|
|
129
|
+
* {@link declineMarketingConsent} writes (AGL-3185): who recorded it, which
|
|
130
|
+
* door, which wording version. The console doors pass the person's own
|
|
131
|
+
* (`actor: 'person'`); a site's capture surface passes none, which the
|
|
132
|
+
* reader takes as the person's own act anyway.
|
|
133
|
+
*/
|
|
134
|
+
marketingConsentSource?: MarketingConsentSource;
|
|
135
|
+
/**
|
|
136
|
+
* Order value in cents — rolls into RFM fields (AGL-328).
|
|
137
|
+
*
|
|
138
|
+
* WHAT IT COUNTS (AGL-1748). GROSS of the platform fee and GROSS of
|
|
139
|
+
* refunds — the money the customer handed over, not the money the merchant
|
|
140
|
+
* kept. Every writer passes the same thing: whatever was actually charged
|
|
141
|
+
* (`amount_total` for a Stripe path, `totals.totalCents` for POS), never a
|
|
142
|
+
* figure re-derived from product docs, which is the AGL-1698/AGL-1711
|
|
143
|
+
* lesson. Gross of the fee because this is a CUSTOMER attribute answering
|
|
144
|
+
* "what is this person worth to me?", and the fee is a cost of the channel,
|
|
145
|
+
* not something the buyer failed to spend.
|
|
146
|
+
*
|
|
147
|
+
* Refunds are still NOT netted here, and now they are recorded elsewhere
|
|
148
|
+
* (AGL-1754). `refund.ts` writes `refundedCents`, `refundedOrdersCount` and
|
|
149
|
+
* `lastRefundAtMs` BESIDE these fields — the shape AGL-1747 chose for the
|
|
150
|
+
* same question on the orders CSV — rather than decrementing a stored number
|
|
151
|
+
* whose meaning would then differ between rows written before and after that
|
|
152
|
+
* commit. So `ltvCents` and `ordersCount` remain gross by definition, and a
|
|
153
|
+
* READER that wants the net computes `ltvCents - refundedCents`, clamping
|
|
154
|
+
* only what it ranks on: the difference can be negative for a customer whose
|
|
155
|
+
* pre-AGL-1748 purchase was never counted and whose refund was, which is a
|
|
156
|
+
* missing purchase showing itself rather than a corrupt contact. AGL-1753 is
|
|
157
|
+
* the backfill that reconciles it. See `contact-refund.ts` in the commerce
|
|
158
|
+
* plugin for the full reasoning and for why a refund never CREATES a contact.
|
|
159
|
+
*
|
|
160
|
+
* Passing 0 or omitting it means "no purchase": `ltvCents`, `ordersCount`,
|
|
161
|
+
* `lastPurchaseAtMs` and `firstPurchaseAtMs` are all left untouched, which
|
|
162
|
+
* is why a caller that formats the amount into the interaction summary and
|
|
163
|
+
* forgets this field records a customer who has apparently never bought
|
|
164
|
+
* anything.
|
|
165
|
+
*/
|
|
166
|
+
purchaseCents?: number;
|
|
167
|
+
/**
|
|
168
|
+
* The currency {@link purchaseCents} is in, lowercase, when the door knows.
|
|
169
|
+
*
|
|
170
|
+
* Absent everywhere today, because no order document carries a currency and
|
|
171
|
+
* every checkout door writes `currency: 'usd'` onto the Stripe line items.
|
|
172
|
+
* `attributeOrderToEmail` defaults it on that basis and says so. The field
|
|
173
|
+
* exists so a door that ever charges in something else can pass it, and the
|
|
174
|
+
* campaign revenue report keeps it in its own bucket rather than adding it
|
|
175
|
+
* to the dollars.
|
|
176
|
+
*/
|
|
177
|
+
purchaseCurrency?: string;
|
|
178
|
+
/**
|
|
179
|
+
* The campaign this person came from, already resolved by the door.
|
|
180
|
+
*
|
|
181
|
+
* ⛔ The ORDER path passes none, and must not start. An order already has
|
|
182
|
+
* its own join one branch below — `attributeOrderToEmail`, keyed on the
|
|
183
|
+
* order id — and a second record for the same sale would be the same money
|
|
184
|
+
* counted twice under two rules. This is the door for the moments an order
|
|
185
|
+
* does NOT cover: a form submission, a membership sign-up, a booking, a
|
|
186
|
+
* newsletter capture.
|
|
187
|
+
*
|
|
188
|
+
* Resolved rather than raw, for the reason `addHostLead` states: one
|
|
189
|
+
* visitor action reaches several writers and the touch lookup is paid once.
|
|
190
|
+
*/
|
|
191
|
+
campaignTouch?: ResolvedCampaignTouch | null;
|
|
192
|
+
/**
|
|
193
|
+
* The campaigns the CAPTURE SURFACE is filed under.
|
|
194
|
+
*
|
|
195
|
+
* ⚠️ A different fact from {@link campaignTouch} beside it, and the two must
|
|
196
|
+
* never be folded together. A touch is where the visitor came FROM — an ad,
|
|
197
|
+
* a link, a browser-supplied label resolved through an allowlist. This is
|
|
198
|
+
* which campaigns the merchant put the form itself in, which is the
|
|
199
|
+
* merchant's own act and is true of everybody who fills that form in,
|
|
200
|
+
* including the visitor who arrived by typing the address.
|
|
201
|
+
*
|
|
202
|
+
* ⛔ And it is not consent. Filing a form under a campaign says nothing
|
|
203
|
+
* about what the person agreed to; `marketingConsent` above is the only
|
|
204
|
+
* input that records a basis.
|
|
205
|
+
*/
|
|
206
|
+
campaignIds?: readonly string[];
|
|
207
|
+
/**
|
|
208
|
+
* Tags to put on THIS holder's facet (AGL-2602).
|
|
209
|
+
*
|
|
210
|
+
* Added to, never replaced, on a merge: a person the merchant tagged by
|
|
211
|
+
* hand and later imported keeps the hand-written tag beside the file's.
|
|
212
|
+
*/
|
|
213
|
+
tags?: readonly string[];
|
|
214
|
+
/**
|
|
215
|
+
* The profile a door knows about the person — phone, title, company,
|
|
216
|
+
* address, owner, stage, custom values — written into THIS holder's facet
|
|
217
|
+
* (AGL-2602). Only the keys present are written, so a door that knows the
|
|
218
|
+
* phone and nothing else does not blank the title somebody typed.
|
|
219
|
+
*/
|
|
220
|
+
facet?: UpsertHostContactFacet;
|
|
221
|
+
/**
|
|
222
|
+
* The EARLIEST stage that describes what this capture was (AGL-2612).
|
|
223
|
+
*
|
|
224
|
+
* Every door names one: a form submission is a `lead`, a newsletter opt-in
|
|
225
|
+
* or a member sign-up is a `subscriber`, a purchase — an order, a paid
|
|
226
|
+
* booking — is a `customer`. The rule applied to it is
|
|
227
|
+
* `advanceContactLifecycleStage`: it fills an empty stage and advances an
|
|
228
|
+
* earlier one, and never moves anybody back, so a customer who fills in
|
|
229
|
+
* the contact form is still a customer and a subscriber who then submits
|
|
230
|
+
* a form becomes a lead. Applied on top of `facet.lifecycleStage` when a
|
|
231
|
+
* door carries both — the caller's stage is the base and this is its
|
|
232
|
+
* floor.
|
|
233
|
+
*
|
|
234
|
+
* A FLOOR and not a value, which is why it is not the facet field: the
|
|
235
|
+
* facet's `lifecycleStage` is a SET, the shape the console's create
|
|
236
|
+
* drawer and the import need — the merchant typed a stage and that is the
|
|
237
|
+
* stage — and a capture door writing a set would put every returning
|
|
238
|
+
* customer back to `lead` on their next enquiry. Omitted by the doors
|
|
239
|
+
* that carry the caller's own stage or none (manual, import, API, a lead
|
|
240
|
+
* conversion), whose contacts read as "no stage", which is true.
|
|
241
|
+
*/
|
|
242
|
+
initialLifecycleStage?: ContactLifecycleStage;
|
|
243
|
+
/**
|
|
244
|
+
* Told when this capture created a contact (AGL-2605).
|
|
245
|
+
*
|
|
246
|
+
* A HOOK rather than an event emitted from here, and the reason is the
|
|
247
|
+
* dependency direction: the event fan-out lives in `libs/tenant/runtime`,
|
|
248
|
+
* which imports THIS library for its Firestore handle and its org helpers.
|
|
249
|
+
* An import back up from here would be a cycle, and the module boundaries
|
|
250
|
+
* (`scope:data` may depend on data and util only) refuse it besides. So
|
|
251
|
+
* this module reports the fact and the runtime's `captureHostContact`
|
|
252
|
+
* turns it into `contactCreated` — every server door goes through that
|
|
253
|
+
* wrapper, and a door that calls this function directly has chosen to
|
|
254
|
+
* create contacts nothing can react to.
|
|
255
|
+
*
|
|
256
|
+
* Awaited with its own catch, like the order join above: the least
|
|
257
|
+
* important write on the path, and a failure in it must not cost the
|
|
258
|
+
* capture that already happened.
|
|
259
|
+
*/
|
|
260
|
+
onCreated?: (created: HostContactCreated) => void | Promise<void>;
|
|
261
|
+
}
|
|
262
|
+
export declare function upsertHostContact(options: UpsertHostContactOptions): Promise<UpsertHostContactVerdict>;
|