@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,755 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* Server half of the first-party error beacon (AGL-1538): validates a
|
|
19
|
+
* browser-posted batch of uncaught errors and forwards each to Google Cloud
|
|
20
|
+
* Error Reporting, where grouping, retention, and the alerting policy live.
|
|
21
|
+
*
|
|
22
|
+
* Why Error Reporting rather than a vendor: Google is already a subprocessor
|
|
23
|
+
* and the GCP monitoring/alerting stack already pages (AGL-1502) — a Sentry
|
|
24
|
+
* would add a subprocessor and a DPA line item, which is a deliberately
|
|
25
|
+
* deferred decision. Why a route in between rather than client-direct: the
|
|
26
|
+
* API needs OAuth, and the route is also where the payload is disarmed —
|
|
27
|
+
* clamped, capped, and stripped before anything leaves our origin.
|
|
28
|
+
*
|
|
29
|
+
* The caller is an unauthenticated browser, so every parsed field is clamped
|
|
30
|
+
* here AGAIN regardless of what the client-side scrubber promised — the
|
|
31
|
+
* beacon script is a promise, this is the boundary.
|
|
32
|
+
*
|
|
33
|
+
* Fail-soft throughout: a reporting failure logs one line and returns.
|
|
34
|
+
* The one thing this module must never do is turn an observed error into a
|
|
35
|
+
* served one.
|
|
36
|
+
*/ import { getApp } from "firebase-admin/app";
|
|
37
|
+
// Side-effect import: initializes the firebase-admin default app (cert
|
|
38
|
+
// credential) exactly the way firebase-admin.ts does, so `getApp()` below
|
|
39
|
+
// always finds it and its credential can mint the OAuth token.
|
|
40
|
+
import "@aglyn/shared-util-fbserver";
|
|
41
|
+
// The readable half of the server-error signal (AGL-1921) — see the block
|
|
42
|
+
// comment in `reportServerError`.
|
|
43
|
+
import { beaconHealth, deploymentCommitRef, deploymentEnvironmentLabel, isDeployedRuntime, isTransientBeaconCode } from "@aglyn/aglyn/server";
|
|
44
|
+
// The scheme list the beacon and the CSP collector share (AGL-2786). A
|
|
45
|
+
// subpath, not the `/server` barrel: specs that stub that barrel with a
|
|
46
|
+
// hand-built factory would silently turn this into `undefined`.
|
|
47
|
+
import { isForeignScriptStack } from "@aglyn/aglyn/app-utils/foreign-script";
|
|
48
|
+
import { readBeaconHeartbeat, recordBeaconHeartbeat, recordServerError } from "./rate-limit-store.js";
|
|
49
|
+
const MAX_EVENTS_PER_REQUEST = 10;
|
|
50
|
+
const MAX_MESSAGE = 1024;
|
|
51
|
+
const MAX_STACK = 8192;
|
|
52
|
+
const MAX_URL = 512;
|
|
53
|
+
const REPORT_TIMEOUT_MS = 4000;
|
|
54
|
+
function clampString(value, max) {
|
|
55
|
+
return typeof value === 'string' ? value.slice(0, max) : '';
|
|
56
|
+
}
|
|
57
|
+
/** Origin + pathname only, server-enforced — never trust the client scrub. */ function scrubUrl(value) {
|
|
58
|
+
const raw = clampString(value, MAX_URL);
|
|
59
|
+
if (!raw) return '';
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(raw);
|
|
62
|
+
return `${url.origin}${url.pathname}`;
|
|
63
|
+
} catch (unused) {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Was this stack thrown with no code of ours on it, and was all of it seen?
|
|
69
|
+
*
|
|
70
|
+
* Every frame under a scheme no deployment of ours serves (AGL-2786) — an
|
|
71
|
+
* in-app webview's bridge, an extension's content script. The beacon drops
|
|
72
|
+
* these before sending; this is the half that also covers a bundle already
|
|
73
|
+
* cached in a visitor's tab, which keeps posting whatever its copy of the rule
|
|
74
|
+
* missed for as long as the tab lives.
|
|
75
|
+
*
|
|
76
|
+
* Only a stack that arrived WHOLE is judged. One at the clamp may be the head
|
|
77
|
+
* of a longer stack, and the tail it lost could hold the frame that makes the
|
|
78
|
+
* error ours.
|
|
79
|
+
*
|
|
80
|
+
* The beacon's other tell — every frame is the document — is deliberately not
|
|
81
|
+
* applied here. This side has no page to compare against, and our own inline
|
|
82
|
+
* code's handled errors carry the document's URL too.
|
|
83
|
+
*/ function isForeignOnlyStack(stack) {
|
|
84
|
+
return stack.length > 0 && stack.length < MAX_STACK && isForeignScriptStack(stack);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The events in a posted payload, clamped and capped. Anything malformed is
|
|
88
|
+
* simply dropped — this endpoint never argues with a browser — and so is an
|
|
89
|
+
* error none of our code threw (see {@link isForeignOnlyStack}).
|
|
90
|
+
*/ export function parseClientErrorEvents(payload) {
|
|
91
|
+
const events = payload == null ? void 0 : payload.events;
|
|
92
|
+
if (!Array.isArray(events)) return [];
|
|
93
|
+
const parsed = [];
|
|
94
|
+
for (const entry of events.slice(0, MAX_EVENTS_PER_REQUEST)){
|
|
95
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
96
|
+
const event = entry;
|
|
97
|
+
const message = clampString(event.message, MAX_MESSAGE);
|
|
98
|
+
if (!message) continue;
|
|
99
|
+
const stack = clampString(event.stack, MAX_STACK);
|
|
100
|
+
if (isForeignOnlyStack(stack)) continue;
|
|
101
|
+
parsed.push({
|
|
102
|
+
kind: clampString(event.kind, 32) || 'error',
|
|
103
|
+
message,
|
|
104
|
+
stack: stack || undefined,
|
|
105
|
+
source: scrubUrl(event.source) || undefined,
|
|
106
|
+
line: typeof event.line === 'number' ? Math.trunc(event.line) : undefined,
|
|
107
|
+
url: scrubUrl(event.url) || undefined
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return parsed;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Error Reporting groups by parsing the `message` as a stack trace; an event
|
|
114
|
+
* WITHOUT a stack must carry `context.reportLocation` instead or ingestion
|
|
115
|
+
* drops it. Both shapes are built here so every accepted beacon event is
|
|
116
|
+
* representable.
|
|
117
|
+
*/ function toReportedEvent(event, service, version) {
|
|
118
|
+
var _event_line;
|
|
119
|
+
var _event_stack;
|
|
120
|
+
const hasStack = Boolean((_event_stack = event.stack) == null ? void 0 : _event_stack.includes('\n'));
|
|
121
|
+
return {
|
|
122
|
+
'@type': 'type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent',
|
|
123
|
+
serviceContext: {
|
|
124
|
+
service,
|
|
125
|
+
version
|
|
126
|
+
},
|
|
127
|
+
message: hasStack ? event.stack : `${event.kind}: ${event.message}`,
|
|
128
|
+
// Outside `serviceContext` so Error Reporting ignores it while a log-match
|
|
129
|
+
// policy can still filter on it — the same placement, for the same reason,
|
|
130
|
+
// that the server half gives `route`/`method`/`digest`.
|
|
131
|
+
//
|
|
132
|
+
// Until AGL-2523 the kind reached the payload ONLY on the stackless path,
|
|
133
|
+
// folded into `message` and `reportLocation.functionName`. Every stacked
|
|
134
|
+
// error — which is nearly all of them — arrived unclassifiable, so the
|
|
135
|
+
// `Client error beacon` policy had no way to express anything narrower
|
|
136
|
+
// than "any entry at all". That is why it pages for one visitor whose
|
|
137
|
+
// webview rewrote the DOM.
|
|
138
|
+
kind: clampString(event.kind, 32) || 'error',
|
|
139
|
+
context: _extends({
|
|
140
|
+
httpRequest: event.url ? {
|
|
141
|
+
url: event.url
|
|
142
|
+
} : undefined
|
|
143
|
+
}, hasStack ? {} : {
|
|
144
|
+
reportLocation: {
|
|
145
|
+
filePath: event.source || 'unknown',
|
|
146
|
+
lineNumber: (_event_line = event.line) != null ? _event_line : 0,
|
|
147
|
+
functionName: event.kind
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** The log the beacon writes; the alert policy and any triage query key on it. */ export const CLIENT_ERROR_LOG_ID = 'client-errors';
|
|
153
|
+
/**
|
|
154
|
+
* The admin credential and project the beacon writes under.
|
|
155
|
+
*
|
|
156
|
+
* Extracted (AGL-1923) so the heartbeat below mints its token through the
|
|
157
|
+
* SAME path the reporter does. That sharing is the whole value of the
|
|
158
|
+
* heartbeat: the failures it exists to catch — an expired service-account
|
|
159
|
+
* key, a revoked `logging.logEntries.create`, an exhausted Logging quota —
|
|
160
|
+
* are environmental, and a heartbeat that acquired its credential some other
|
|
161
|
+
* way could report healthy while every real report was being dropped.
|
|
162
|
+
*
|
|
163
|
+
* Returns null when either half is missing; the caller decides what that
|
|
164
|
+
* means (the reporter drops the batch, the heartbeat reports degraded).
|
|
165
|
+
*
|
|
166
|
+
* EXPORTED for the Vercel log-drain receiver (AGL-1921, third arm), which
|
|
167
|
+
* writes to `vercel-runtime` through this same credential for the same
|
|
168
|
+
* reason the heartbeat does: the failures that would silence the drain are
|
|
169
|
+
* the environmental ones AGL-1923's heartbeat already probes, and it probes
|
|
170
|
+
* them through exactly this path. A receiver that minted its own token would
|
|
171
|
+
* need its own dead-man's switch.
|
|
172
|
+
*/ export async function beaconLoggingTarget() {
|
|
173
|
+
return (await resolveBeaconLoggingTarget()).target;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The same resolution, saying which of two very different things went wrong
|
|
177
|
+
* (AGL-2713).
|
|
178
|
+
*
|
|
179
|
+
* `no-credential` used to cover both, and the collapse is what made
|
|
180
|
+
* `/api/health/error-beacon` cry wolf: one of the two is permanent and one is
|
|
181
|
+
* a cold lambda's first outbound connection.
|
|
182
|
+
*
|
|
183
|
+
* - **`no-credential`** — there is no admin app, or it has no project, or it
|
|
184
|
+
* has no credential object. `initializeApp` runs at module load and is
|
|
185
|
+
* guarded on `FIREBASE_PRIVATE_KEY` + `FIREBASE_CLIENT_EMAIL` +
|
|
186
|
+
* `NEXT_PUBLIC_FIREBASE_PROJECT_ID`, so this is a STATIC fact about the
|
|
187
|
+
* deployment's environment: it cannot heal on the next request, and every
|
|
188
|
+
* browser error is being dropped for as long as it holds. Red immediately —
|
|
189
|
+
* which is exactly what the remedy text on the staff board already says
|
|
190
|
+
* (`no-credential` is the deployment `FIREBASE_*` env).
|
|
191
|
+
* - **`credential-unavailable`** — the app and the certificate are both there
|
|
192
|
+
* and `getAccessToken()` did not produce a token on THIS attempt. That is a
|
|
193
|
+
* JWT-bearer exchange against `oauth2.googleapis.com`, and on a cold
|
|
194
|
+
* instance it is the first outbound TLS handshake the sandbox has ever made.
|
|
195
|
+
* Nothing was decided, so nothing about the beacon has been established.
|
|
196
|
+
*/ export async function resolveBeaconLoggingTarget() {
|
|
197
|
+
var _app_options_projectId;
|
|
198
|
+
let app;
|
|
199
|
+
try {
|
|
200
|
+
app = getApp();
|
|
201
|
+
} catch (unused) {
|
|
202
|
+
return {
|
|
203
|
+
target: null,
|
|
204
|
+
code: 'no-credential'
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const projectId = (_app_options_projectId = app.options.projectId) != null ? _app_options_projectId : process.env['NEXT_PUBLIC_FIREBASE_PROJECT_ID'];
|
|
208
|
+
const credential = app.options.credential;
|
|
209
|
+
if (!projectId || !credential) return {
|
|
210
|
+
target: null,
|
|
211
|
+
code: 'no-credential'
|
|
212
|
+
};
|
|
213
|
+
let token;
|
|
214
|
+
try {
|
|
215
|
+
var _this;
|
|
216
|
+
token = (_this = await credential.getAccessToken()) == null ? void 0 : _this.access_token;
|
|
217
|
+
} catch (unused) {
|
|
218
|
+
token = undefined;
|
|
219
|
+
}
|
|
220
|
+
if (!token) return {
|
|
221
|
+
target: null,
|
|
222
|
+
code: 'credential-unavailable'
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
target: {
|
|
226
|
+
token,
|
|
227
|
+
projectId
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Forwards events under the admin service account — via Cloud LOGGING, not
|
|
233
|
+
* the Error Reporting `events:report` API, and the transport is the point:
|
|
234
|
+
* a log entry whose payload carries the `ReportedErrorEvent` `@type` is
|
|
235
|
+
* ingested by Error Reporting automatically (grouping, the Error Reporting
|
|
236
|
+
* console, version tracking), while ALSO being a real log entry that a
|
|
237
|
+
* Cloud Monitoring log-match alert policy can page on — `events:report`
|
|
238
|
+
* produces no log entry and nothing standard to alert from. One batched
|
|
239
|
+
* write per request instead of one call per event is the bonus.
|
|
240
|
+
*
|
|
241
|
+
* Returns how many events were written; failures are logged (one line) and
|
|
242
|
+
* swallowed.
|
|
243
|
+
*/ export async function reportClientErrors(events, options) {
|
|
244
|
+
var _deploymentCommitRef;
|
|
245
|
+
if (!events.length) return 0;
|
|
246
|
+
/*==========================================
|
|
247
|
+
* A LAPTOP IS NOT A DEPLOYMENT (AGL-1925, option 1).
|
|
248
|
+
*
|
|
249
|
+
* Measured 2026-08-18 over a trailing week of Error Reporting `groupStats`:
|
|
250
|
+
* 13 of 17 events — 76% — were not production, and the single highest-ranked
|
|
251
|
+
* "production error group" in the project was a dev-only artifact with
|
|
252
|
+
* `http://localhost:4200` stack frames. Ranking is the whole point of Error
|
|
253
|
+
* Reporting, so a majority-noise stream does not merely cost storage: it
|
|
254
|
+
* aims triage at the wrong thing, and the `Client error beacon` policy
|
|
255
|
+
* notifies its single human recipient for every uncaught error on any
|
|
256
|
+
* developer's machine.
|
|
257
|
+
*
|
|
258
|
+
* The credential is why localhost reaches production at all: `firebase-admin`
|
|
259
|
+
* is configured from the root `.env`, which names the platform project.
|
|
260
|
+
*
|
|
261
|
+
* Unlike the server half this stream carries NO `environment` stamp — the
|
|
262
|
+
* only marker was `serviceContext.version` being absent off Vercel, which
|
|
263
|
+
* did that job by accident and stopped doing it once `deploymentCommitRef`
|
|
264
|
+
* started resolving a commit for self-hosted builds. So refusing the write
|
|
265
|
+
* is not one of two options here; it is the only one that works at all.
|
|
266
|
+
*
|
|
267
|
+
* `isDeployedRuntime`, matching `reportServerError`: an operator's own
|
|
268
|
+
* container reports its visitors' errors, and a developer's console already
|
|
269
|
+
* shows them the error it would have sent.
|
|
270
|
+
*==========================================*/ if (!isDeployedRuntime()) return 0;
|
|
271
|
+
const target = await beaconLoggingTarget();
|
|
272
|
+
const token = target == null ? void 0 : target.token;
|
|
273
|
+
const projectId = target == null ? void 0 : target.projectId;
|
|
274
|
+
if (!token || !projectId) {
|
|
275
|
+
console.warn(JSON.stringify({
|
|
276
|
+
tag: 'AGL-1538:error-beacon',
|
|
277
|
+
drop: events.length,
|
|
278
|
+
reason: 'no-credential'
|
|
279
|
+
}));
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
/*
|
|
283
|
+
* Which BUILD this report came from, resolved the way `/api/health` resolves
|
|
284
|
+
* it — `BUILD_ID`, then `COMMIT_REF`, then Vercel's own.
|
|
285
|
+
*
|
|
286
|
+
* This read `VERCEL_GIT_COMMIT_SHA` directly, which is set on Aglyn's cloud
|
|
287
|
+
* and nowhere else, so every error a self-hosted deployment reported arrived
|
|
288
|
+
* with no version at all while the health endpoint beside it named the
|
|
289
|
+
* commit correctly. An error report you cannot tie to a build is an error
|
|
290
|
+
* report you cannot act on.
|
|
291
|
+
*
|
|
292
|
+
* `undefined`, not `null`: this becomes Error Reporting's
|
|
293
|
+
* `serviceContext.version`, where an absent key is omitted and an empty one
|
|
294
|
+
* is a version.
|
|
295
|
+
*/ const version = (_deploymentCommitRef = deploymentCommitRef()) != null ? _deploymentCommitRef : undefined;
|
|
296
|
+
try {
|
|
297
|
+
const response = await fetch('https://logging.googleapis.com/v2/entries:write', {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
headers: {
|
|
300
|
+
Authorization: `Bearer ${token}`,
|
|
301
|
+
'Content-Type': 'application/json'
|
|
302
|
+
},
|
|
303
|
+
body: JSON.stringify({
|
|
304
|
+
logName: `projects/${projectId}/logs/${CLIENT_ERROR_LOG_ID}`,
|
|
305
|
+
resource: {
|
|
306
|
+
type: 'global'
|
|
307
|
+
},
|
|
308
|
+
entries: events.map((event)=>({
|
|
309
|
+
severity: 'ERROR',
|
|
310
|
+
jsonPayload: toReportedEvent(event, options.service, version)
|
|
311
|
+
}))
|
|
312
|
+
}),
|
|
313
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
|
|
314
|
+
});
|
|
315
|
+
if (!response.ok) {
|
|
316
|
+
console.warn(JSON.stringify({
|
|
317
|
+
tag: 'AGL-1538:error-beacon',
|
|
318
|
+
status: response.status,
|
|
319
|
+
drop: events.length
|
|
320
|
+
}));
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
return events.length;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.warn(JSON.stringify({
|
|
326
|
+
tag: 'AGL-1538:error-beacon',
|
|
327
|
+
transport: String(error).slice(0, 200)
|
|
328
|
+
}));
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* The log the SERVER half writes to (AGL-1921).
|
|
334
|
+
*
|
|
335
|
+
* DELIBERATELY NOT `client-errors`. The `Client error beacon` policy is a
|
|
336
|
+
* log-match on that log id, and a server 5xx is a different incident with a
|
|
337
|
+
* different first move — a browser error is one visitor's broken render, a
|
|
338
|
+
* 500 rate is every visitor's. Merging the streams would make the existing
|
|
339
|
+
* policy fire for both and force triage to start by asking which it was.
|
|
340
|
+
* A sibling log id also lets the counter this whole issue is about key on
|
|
341
|
+
* server errors ALONE.
|
|
342
|
+
*/ export const SERVER_ERROR_LOG_ID = 'server-errors';
|
|
343
|
+
/**
|
|
344
|
+
* Per-instance write budget (AGL-1921).
|
|
345
|
+
*
|
|
346
|
+
* The failure this exists to observe is a SPIKE, and a spike is exactly when
|
|
347
|
+
* an unbounded reporter turns one incident into two — a 500 on a hot path
|
|
348
|
+
* can run at request rate, and every entry is a billable Logging write on an
|
|
349
|
+
* account whose whole monitoring budget is $20/month. The budget is per
|
|
350
|
+
* instance and per minute, so the shape of the spike still reaches Logging
|
|
351
|
+
* (the counter only needs to cross a threshold, not be exact) while the cost
|
|
352
|
+
* stays bounded no matter how hard the incident pushes.
|
|
353
|
+
*
|
|
354
|
+
* Suppression is REPORTED, never silent: when the window rolls over, one
|
|
355
|
+
* summary line records how many were dropped. A monitoring path that hides
|
|
356
|
+
* its own lossiness is the thing this repo keeps finding.
|
|
357
|
+
*/ const SERVER_ERROR_BUDGET_PER_WINDOW = 60;
|
|
358
|
+
const SERVER_ERROR_WINDOW_MS = 60000;
|
|
359
|
+
let serverErrorWindowStartedAt = 0;
|
|
360
|
+
let serverErrorsWritten = 0;
|
|
361
|
+
let serverErrorsSuppressed = 0;
|
|
362
|
+
/**
|
|
363
|
+
* Forwards ONE server-side error to Cloud Logging under the admin credential.
|
|
364
|
+
*
|
|
365
|
+
* This is AGL-1921's fallback arm, and it is a fallback on purpose: it cannot
|
|
366
|
+
* see an error that kills the process before the handler runs, nor a
|
|
367
|
+
* platform-level 5xx that never reaches our code at all. A Vercel log drain
|
|
368
|
+
* would see both. What this does buy is the case that matters most and is
|
|
369
|
+
* most likely — our own route handlers and renders throwing — reported
|
|
370
|
+
* through a transport that already works, on an account we already pay for.
|
|
371
|
+
* `docs/UPTIME_AND_SLA.md` carries the blind spots in writing, because
|
|
372
|
+
* partial visibility whose gaps are documented beats none.
|
|
373
|
+
*
|
|
374
|
+
* It shares `beaconLoggingTarget` with the client reporter deliberately: that
|
|
375
|
+
* makes AGL-1923's heartbeat a dead-man's switch for THIS path too. The
|
|
376
|
+
* failures that would silence server-error reporting — expired key, revoked
|
|
377
|
+
* `logging.logEntries.create`, exhausted quota — are the same environmental
|
|
378
|
+
* ones the heartbeat already probes, and it probes them through this exact
|
|
379
|
+
* credential.
|
|
380
|
+
*
|
|
381
|
+
* Fail-soft and never throws: this runs inside `onRequestError`, so throwing
|
|
382
|
+
* here would turn an observed error into a second one during an incident.
|
|
383
|
+
*/ export async function reportServerError(event, options) {
|
|
384
|
+
var _deploymentCommitRef;
|
|
385
|
+
const message = clampString(event.message, MAX_MESSAGE);
|
|
386
|
+
if (!message) return 'dropped';
|
|
387
|
+
/*==========================================
|
|
388
|
+
* A LAPTOP IS NOT A DEPLOYMENT, AND THIS IS THE ONLY GATE THAT CAN SAY SO.
|
|
389
|
+
*
|
|
390
|
+
* ABOVE the marker below, unlike every other gate here, and the difference
|
|
391
|
+
* is what is being refused. Those gates are about a write that FAILED and
|
|
392
|
+
* must still be counted. This one is about a process that should not be
|
|
393
|
+
* reporting at all: the count is as wrong as the log entry, because
|
|
394
|
+
* `/api/health/server-errors` reads that marker out of the platform project
|
|
395
|
+
* and would answer for a developer's terminal.
|
|
396
|
+
*
|
|
397
|
+
* The credential makes this reachable. `firebase-admin` is configured from
|
|
398
|
+
* the root `.env`, which names the platform project, so `nx dev` writes to
|
|
399
|
+
* production Cloud Logging under the same key production uses. Measured
|
|
400
|
+
* 2026-08-31: three parse errors from an interrupted rebase — carrying
|
|
401
|
+
* `file:///Users/.../aglyn/apps/...` stacks — landed in `server-errors` and
|
|
402
|
+
* opened the `Server errors: uncaught 5xx` policy, which was still open two
|
|
403
|
+
* days later while all thirteen uptime checks read 100%.
|
|
404
|
+
*
|
|
405
|
+
* `environment` was stamped for this (see the field below), but a stamp only
|
|
406
|
+
* helps a reader who filters on it, and the one reader this log can have is
|
|
407
|
+
* a hand-made alert policy that did not. Refusing the write is the half that
|
|
408
|
+
* does not depend on somebody else's configuration.
|
|
409
|
+
*
|
|
410
|
+
* `isDeployedRuntime` and not `isProductionDeployment`, deliberately: a
|
|
411
|
+
* preview's 5xx is a real 5xx served by a real deployment, and the stamp
|
|
412
|
+
* already separates it for anyone who wants to.
|
|
413
|
+
*==========================================*/ if (!isDeployedRuntime()) return 'dropped';
|
|
414
|
+
/*==========================================
|
|
415
|
+
* COUNT IT WHERE SOMETHING CAN READ THE COUNT (AGL-1921, second pass).
|
|
416
|
+
*
|
|
417
|
+
* FIRST, above every gate below, and that ordering is the whole point.
|
|
418
|
+
*
|
|
419
|
+
* The Logging entry this function goes on to write is the right record for
|
|
420
|
+
* triage and the wrong one for an alarm we can ship: measured 2026-08-24,
|
|
421
|
+
* the production credential is refused `entries:list` on that log with
|
|
422
|
+
* `403 Permission denied for all log views`, so nothing in this repo can
|
|
423
|
+
* read back what it wrote. The only reader the log can ever have is a GCP
|
|
424
|
+
* alert policy somebody creates by hand.
|
|
425
|
+
*
|
|
426
|
+
* The marker is readable today, and putting it above the budget gate, the
|
|
427
|
+
* credential check and the fetch means the count survives every way the
|
|
428
|
+
* Logging arm can fail. That is not tidiness — a beacon whose transport is
|
|
429
|
+
* dead reports ZERO errors, which is the measured-zero shape this repo keeps
|
|
430
|
+
* finding, and it would report it during exactly the incident that killed
|
|
431
|
+
* the transport.
|
|
432
|
+
*
|
|
433
|
+
* Never awaited: this runs inside `onRequestError`, and the marker coalesces
|
|
434
|
+
* in process anyway (at most one write per five seconds per instance).
|
|
435
|
+
*==========================================*/ recordServerError(options.service);
|
|
436
|
+
const now = Date.now();
|
|
437
|
+
if (now - serverErrorWindowStartedAt >= SERVER_ERROR_WINDOW_MS) {
|
|
438
|
+
if (serverErrorsSuppressed > 0) {
|
|
439
|
+
console.warn(JSON.stringify({
|
|
440
|
+
tag: 'AGL-1921:server-error-beacon',
|
|
441
|
+
suppressed: serverErrorsSuppressed,
|
|
442
|
+
written: serverErrorsWritten,
|
|
443
|
+
windowMs: SERVER_ERROR_WINDOW_MS
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
446
|
+
serverErrorWindowStartedAt = now;
|
|
447
|
+
serverErrorsWritten = 0;
|
|
448
|
+
serverErrorsSuppressed = 0;
|
|
449
|
+
}
|
|
450
|
+
if (serverErrorsWritten >= SERVER_ERROR_BUDGET_PER_WINDOW) {
|
|
451
|
+
serverErrorsSuppressed += 1;
|
|
452
|
+
return 'suppressed';
|
|
453
|
+
}
|
|
454
|
+
const target = await beaconLoggingTarget();
|
|
455
|
+
if (!target) {
|
|
456
|
+
console.warn(JSON.stringify({
|
|
457
|
+
tag: 'AGL-1921:server-error-beacon',
|
|
458
|
+
drop: 1,
|
|
459
|
+
reason: 'no-credential'
|
|
460
|
+
}));
|
|
461
|
+
return 'dropped';
|
|
462
|
+
}
|
|
463
|
+
const stack = clampString(event.stack, MAX_STACK);
|
|
464
|
+
const hasStack = stack.includes('\n');
|
|
465
|
+
const version = (_deploymentCommitRef = deploymentCommitRef()) != null ? _deploymentCommitRef : undefined;
|
|
466
|
+
try {
|
|
467
|
+
const response = await fetch('https://logging.googleapis.com/v2/entries:write', {
|
|
468
|
+
method: 'POST',
|
|
469
|
+
headers: {
|
|
470
|
+
Authorization: `Bearer ${target.token}`,
|
|
471
|
+
'Content-Type': 'application/json'
|
|
472
|
+
},
|
|
473
|
+
body: JSON.stringify({
|
|
474
|
+
logName: `projects/${target.projectId}/logs/${SERVER_ERROR_LOG_ID}`,
|
|
475
|
+
resource: {
|
|
476
|
+
type: 'global'
|
|
477
|
+
},
|
|
478
|
+
entries: [
|
|
479
|
+
{
|
|
480
|
+
severity: 'ERROR',
|
|
481
|
+
jsonPayload: {
|
|
482
|
+
'@type': 'type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent',
|
|
483
|
+
serviceContext: {
|
|
484
|
+
service: options.service,
|
|
485
|
+
version
|
|
486
|
+
},
|
|
487
|
+
// Same rule as the client half: Error Reporting groups by
|
|
488
|
+
// parsing `message` as a stack trace, and an event without one
|
|
489
|
+
// must carry `context.reportLocation` or ingestion drops it.
|
|
490
|
+
message: hasStack ? stack : message,
|
|
491
|
+
context: _extends({}, hasStack ? {} : {
|
|
492
|
+
reportLocation: {
|
|
493
|
+
filePath: clampString(event.route, MAX_URL) || 'unknown',
|
|
494
|
+
lineNumber: 0,
|
|
495
|
+
functionName: clampString(event.routeType, 32) || 'server'
|
|
496
|
+
}
|
|
497
|
+
}),
|
|
498
|
+
// Outside `serviceContext` so Error Reporting ignores them and
|
|
499
|
+
// a log-match policy can still filter on them.
|
|
500
|
+
route: clampString(event.route, MAX_URL) || undefined,
|
|
501
|
+
method: clampString(event.method, 16) || undefined,
|
|
502
|
+
digest: clampString(event.digest, 64) || undefined,
|
|
503
|
+
// WHICH DEPLOYMENT THREW, and the log-match policy has no other
|
|
504
|
+
// way to ask. The credential comes from the environment, so a
|
|
505
|
+
// developer serving the console locally against the platform
|
|
506
|
+
// project writes here under the same key as production and
|
|
507
|
+
// lands in the same log — a throw on a laptop is then
|
|
508
|
+
// indistinguishable from a 500 served to a customer. The
|
|
509
|
+
// heartbeat beside this already stamps it for that reason.
|
|
510
|
+
environment: deploymentEnvironmentLabel()
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
}),
|
|
515
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
|
|
516
|
+
});
|
|
517
|
+
if (!response.ok) {
|
|
518
|
+
console.warn(JSON.stringify({
|
|
519
|
+
tag: 'AGL-1921:server-error-beacon',
|
|
520
|
+
status: response.status,
|
|
521
|
+
drop: 1
|
|
522
|
+
}));
|
|
523
|
+
return 'dropped';
|
|
524
|
+
}
|
|
525
|
+
serverErrorsWritten += 1;
|
|
526
|
+
return 'written';
|
|
527
|
+
} catch (error) {
|
|
528
|
+
console.warn(JSON.stringify({
|
|
529
|
+
tag: 'AGL-1921:server-error-beacon',
|
|
530
|
+
transport: String(error).slice(0, 200)
|
|
531
|
+
}));
|
|
532
|
+
return 'dropped';
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* The log the beacon's heartbeat is written to (AGL-1923).
|
|
537
|
+
*
|
|
538
|
+
* DELIBERATELY NOT `client-errors`. The `Client error beacon` alert policy is
|
|
539
|
+
* a log-match on `logName="…/client-errors" AND severity>=ERROR`, so a
|
|
540
|
+
* heartbeat written there would page the on-call owner on every probe — building the
|
|
541
|
+
* alert-fatigue mechanism the heartbeat exists to protect against. A separate
|
|
542
|
+
* log id at INFO keeps the two streams from ever being confused, and keeps
|
|
543
|
+
* the payload out of Error Reporting (which ingests on the
|
|
544
|
+
* `ReportedErrorEvent` `@type`, absent here on purpose).
|
|
545
|
+
*/ export const BEACON_HEARTBEAT_LOG_ID = 'client-error-beacon-heartbeat';
|
|
546
|
+
/**
|
|
547
|
+
* Write one heartbeat entry through the beacon's own transport (AGL-1923).
|
|
548
|
+
*
|
|
549
|
+
* ## Why this exists
|
|
550
|
+
*
|
|
551
|
+
* `reportClientErrors` is fail-soft by design and must stay that way — it may
|
|
552
|
+
* never turn an observed error into a served one. But every one of its
|
|
553
|
+
* failure paths ends in a `console.warn` to the Vercel runtime log, which
|
|
554
|
+
* retains about an hour and drains nowhere (AGL-1799). So the beacon can fail
|
|
555
|
+
* completely and the only readings anyone has are "Error Reporting shows zero
|
|
556
|
+
* errors" and "the log-match policy is silent" — which are exactly the
|
|
557
|
+
* readings a clean launch produces. **A dead beacon is indistinguishable from
|
|
558
|
+
* zero errors.**
|
|
559
|
+
*
|
|
560
|
+
* A log-match policy can only ever report presence. The fix is a signal whose
|
|
561
|
+
* ABSENCE is detectable, and the cheapest one that needs no new vendor is a
|
|
562
|
+
* write that a health endpoint can grade synchronously: this returns whether
|
|
563
|
+
* the write landed, `/api/health/error-beacon` turns that into the 200/503
|
|
564
|
+
* contract the sibling health checks already speak, and the existing uptime
|
|
565
|
+
* check + alert + email path becomes the listener. The uptime probe is what
|
|
566
|
+
* winds the dead-man's switch, so there is no cron to forget.
|
|
567
|
+
*
|
|
568
|
+
* ## What it can and cannot prove
|
|
569
|
+
*
|
|
570
|
+
* It proves the credential mints, the IAM grant still covers
|
|
571
|
+
* `logging.logEntries.create`, the Logging API answers, and the quota is not
|
|
572
|
+
* exhausted — the whole environmental failure class AGL-1923 names. It does
|
|
573
|
+
* NOT prove a browser can reach `/api/errors`, and it does not prove the
|
|
574
|
+
* payload shape Error Reporting ingests on, because it deliberately does not
|
|
575
|
+
* write that shape.
|
|
576
|
+
*
|
|
577
|
+
* ## Clearing
|
|
578
|
+
*
|
|
579
|
+
* The condition it raises is an EVENT, not a state: the next successful write
|
|
580
|
+
* clears it, within one probe TTL. Nothing here can latch (AGL-1843).
|
|
581
|
+
*
|
|
582
|
+
* Never throws. A heartbeat that could 500 a health endpoint would be a
|
|
583
|
+
* monitoring probe that causes the outage it reports.
|
|
584
|
+
*/ export async function writeBeaconHeartbeat(options) {
|
|
585
|
+
const { target, code } = await resolveBeaconLoggingTarget();
|
|
586
|
+
if (!target) return {
|
|
587
|
+
ok: false,
|
|
588
|
+
code: code != null ? code : 'no-credential'
|
|
589
|
+
};
|
|
590
|
+
try {
|
|
591
|
+
const response = await fetch('https://logging.googleapis.com/v2/entries:write', {
|
|
592
|
+
method: 'POST',
|
|
593
|
+
headers: {
|
|
594
|
+
Authorization: `Bearer ${target.token}`,
|
|
595
|
+
'Content-Type': 'application/json'
|
|
596
|
+
},
|
|
597
|
+
body: JSON.stringify({
|
|
598
|
+
logName: `projects/${target.projectId}/logs/${BEACON_HEARTBEAT_LOG_ID}`,
|
|
599
|
+
resource: {
|
|
600
|
+
type: 'global'
|
|
601
|
+
},
|
|
602
|
+
entries: [
|
|
603
|
+
{
|
|
604
|
+
severity: 'INFO',
|
|
605
|
+
jsonPayload: {
|
|
606
|
+
// No `@type`: this must not be ingested by Error Reporting.
|
|
607
|
+
tag: 'AGL-1923:beacon-heartbeat',
|
|
608
|
+
service: options.service,
|
|
609
|
+
version: deploymentCommitRef(),
|
|
610
|
+
environment: deploymentEnvironmentLabel()
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
]
|
|
614
|
+
}),
|
|
615
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
|
|
616
|
+
});
|
|
617
|
+
// The STATUS only. The health body is public and a Google error message
|
|
618
|
+
// can carry project ids and resource paths — the same rule the backups
|
|
619
|
+
// probe follows.
|
|
620
|
+
return response.ok ? {
|
|
621
|
+
ok: true
|
|
622
|
+
} : {
|
|
623
|
+
ok: false,
|
|
624
|
+
code: `http-${response.status}`
|
|
625
|
+
};
|
|
626
|
+
} catch (error) {
|
|
627
|
+
var _ref;
|
|
628
|
+
// Codes, not messages: `TimeoutError` for the 4s abort, whatever the
|
|
629
|
+
// fetch layer names for a transport failure.
|
|
630
|
+
return {
|
|
631
|
+
ok: false,
|
|
632
|
+
code: `transport-${String((_ref = error == null ? void 0 : error.name) != null ? _ref : 'unknown')}`
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* How long the probe waits before its second attempt (AGL-2713).
|
|
638
|
+
*
|
|
639
|
+
* Long enough for a JWT-bearer exchange that lost its first connection to get
|
|
640
|
+
* another one, short enough that it is invisible next to a probe already
|
|
641
|
+
* budgeted at 4s for the Logging write. It is a delay, not a backoff schedule:
|
|
642
|
+
* a second attempt either establishes something or it does not, and a third
|
|
643
|
+
* would only spend a public endpoint's time proving the same point.
|
|
644
|
+
*/ export const BEACON_HEARTBEAT_RETRY_DELAY_MS = 250;
|
|
645
|
+
/**
|
|
646
|
+
* How long a missed heartbeat is forgiven after the last one that landed.
|
|
647
|
+
*
|
|
648
|
+
* One monitor interval. The uptime workflow reads this door every 15 minutes,
|
|
649
|
+
* so a grace of exactly that means the FIRST probe after a miss can be
|
|
650
|
+
* forgiven and the second cannot — "tolerate a single miss, red on a
|
|
651
|
+
* sustained one", stated in the only unit the reader actually samples in.
|
|
652
|
+
*
|
|
653
|
+
* The bound this puts on a real outage: the marker is refreshed by every
|
|
654
|
+
* successful probe, and this endpoint is polled every 5 minutes, so the last
|
|
655
|
+
* landing is at most one probe TTL old when a sustained failure starts. Worst
|
|
656
|
+
* case the door reds 20 minutes later than it does today, and the 15-minute
|
|
657
|
+
* workflow sees it on its next run.
|
|
658
|
+
*/ export const BEACON_HEARTBEAT_GRACE_MS = 15 * 60000;
|
|
659
|
+
/**
|
|
660
|
+
* One heartbeat probe, graded — the whole of what both doors do (AGL-2713).
|
|
661
|
+
*
|
|
662
|
+
* ## Why this is one function and not two copies
|
|
663
|
+
*
|
|
664
|
+
* `apps/console` and `apps/tenant` each own a copy of
|
|
665
|
+
* `/api/health/error-beacon`, and they must: the deployments carry different
|
|
666
|
+
* admin credentials, so a console heartbeat proves nothing about the tenant
|
|
667
|
+
* one. What they do NOT need is two copies of the policy. Before this the two
|
|
668
|
+
* route files were byte-identical apart from a service name, which is the
|
|
669
|
+
* arrangement that guarantees a fix reaches one door and not the other. The
|
|
670
|
+
* per-deployment part (which credential, which service name, which memo) stays
|
|
671
|
+
* in the routes; the grading lives here, once.
|
|
672
|
+
*
|
|
673
|
+
* ## The sequence, and why each step is where it is
|
|
674
|
+
*
|
|
675
|
+
* 1. Write. If it lands, record the durable marker and report green — the
|
|
676
|
+
* marker being the evidence a LATER miss will be forgiven with.
|
|
677
|
+
* 2. If it did not land and the code is a refusal — `http-401`/`http-403`
|
|
678
|
+
* (a lost `logging.logEntries.create` grant), `http-429` (quota),
|
|
679
|
+
* `no-credential` (the deployment env) — report red NOW, with no retry and
|
|
680
|
+
* no grace. Those are permanent until a person acts, and re-asking a
|
|
681
|
+
* credential that has been revoked only delays the page.
|
|
682
|
+
* 3. Otherwise nothing was decided, so ask once more after a short delay. On a
|
|
683
|
+
* cold lambda this is the step that actually fixes the measured incident:
|
|
684
|
+
* the memo is per-instance, so a fresh instance has no history to be
|
|
685
|
+
* forgiving WITH, and the only tolerance available inside the request is
|
|
686
|
+
* another attempt.
|
|
687
|
+
* 4. If the second attempt misses too, forgive only against the durable
|
|
688
|
+
* marker — see {@link beaconHealth}, which will not forgive without proof.
|
|
689
|
+
*
|
|
690
|
+
* `attempts` and `ms` ride in the body on every outcome, including green ones,
|
|
691
|
+
* because a retry that is quietly always needed is a credential on its way out
|
|
692
|
+
* and a door that hid it would be back where it started.
|
|
693
|
+
*/ export async function beaconHeartbeatProbe(options) {
|
|
694
|
+
var _options_now, _options_sleep, _options_graceMs, _options_write, _options_retryDelayMs;
|
|
695
|
+
const clock = (_options_now = options.now) != null ? _options_now : Date.now;
|
|
696
|
+
const pause = (_options_sleep = options.sleep) != null ? _options_sleep : (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
697
|
+
const graceMs = (_options_graceMs = options.graceMs) != null ? _options_graceMs : BEACON_HEARTBEAT_GRACE_MS;
|
|
698
|
+
const startedAt = clock();
|
|
699
|
+
const service = options.service;
|
|
700
|
+
const write = (_options_write = options.write) != null ? _options_write : writeBeaconHeartbeat;
|
|
701
|
+
const attempt = async ()=>{
|
|
702
|
+
try {
|
|
703
|
+
return await write({
|
|
704
|
+
service
|
|
705
|
+
});
|
|
706
|
+
} catch (unused) {
|
|
707
|
+
// `writeBeaconHeartbeat` is documented never to throw; this is the belt
|
|
708
|
+
// that keeps a monitoring probe from ever being the outage it reports.
|
|
709
|
+
// A null result is degraded by contract — "we could not determine
|
|
710
|
+
// whether the beacon works" IS the condition this endpoint exists to
|
|
711
|
+
// catch, and unlike a missed write it is a broken contract rather than
|
|
712
|
+
// a quiet network, so it is never one of the tolerated shapes.
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
const graded = (write, attempts, landedAtMs)=>beaconHealth(write, BEACON_HEARTBEAT_LOG_ID, service, clock() - startedAt, {
|
|
717
|
+
attempts,
|
|
718
|
+
landedAtMs,
|
|
719
|
+
graceMs,
|
|
720
|
+
now: clock()
|
|
721
|
+
});
|
|
722
|
+
const first = await attempt();
|
|
723
|
+
if (first == null ? void 0 : first.ok) {
|
|
724
|
+
const landedAtMs = clock();
|
|
725
|
+
recordBeaconHeartbeat(service, {
|
|
726
|
+
now: landedAtMs,
|
|
727
|
+
firestore: options.firestore
|
|
728
|
+
});
|
|
729
|
+
return graded(first, 1, landedAtMs);
|
|
730
|
+
}
|
|
731
|
+
if (first === null || !isTransientBeaconCode(first.code)) {
|
|
732
|
+
return graded(first, 1, null);
|
|
733
|
+
}
|
|
734
|
+
await pause((_options_retryDelayMs = options.retryDelayMs) != null ? _options_retryDelayMs : BEACON_HEARTBEAT_RETRY_DELAY_MS);
|
|
735
|
+
const second = await attempt();
|
|
736
|
+
if (second == null ? void 0 : second.ok) {
|
|
737
|
+
const landedAtMs = clock();
|
|
738
|
+
recordBeaconHeartbeat(service, {
|
|
739
|
+
now: landedAtMs,
|
|
740
|
+
firestore: options.firestore
|
|
741
|
+
});
|
|
742
|
+
return graded(second, 2, landedAtMs);
|
|
743
|
+
}
|
|
744
|
+
if (second === null || !isTransientBeaconCode(second.code)) {
|
|
745
|
+
return graded(second, 2, null);
|
|
746
|
+
}
|
|
747
|
+
// Both attempts came back with nothing decided. The only thing that can
|
|
748
|
+
// forgive that is proof another instance landed a heartbeat recently, and
|
|
749
|
+
// a store that cannot answer is not proof.
|
|
750
|
+
return graded(second, 2, await readBeaconHeartbeat(service, {
|
|
751
|
+
firestore: options.firestore
|
|
752
|
+
}));
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
//# sourceMappingURL=client-error-report.js.map
|