@aglyn/aglyn 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 +11 -0
- package/package.json +66 -0
- package/src/index.d.ts +25 -0
- package/src/index.js +26 -0
- package/src/index.js.map +1 -0
- package/src/lib/aglyn.d.ts +31 -0
- package/src/lib/aglyn.js +114 -0
- package/src/lib/aglyn.js.map +1 -0
- package/src/lib/app-utils/abuse-report.d.ts +193 -0
- package/src/lib/app-utils/abuse-report.js +321 -0
- package/src/lib/app-utils/abuse-report.js.map +1 -0
- package/src/lib/app-utils/accept-negotiation.d.ts +129 -0
- package/src/lib/app-utils/accept-negotiation.js +266 -0
- package/src/lib/app-utils/accept-negotiation.js.map +1 -0
- package/src/lib/app-utils/account-emails.d.ts +263 -0
- package/src/lib/app-utils/account-emails.js +264 -0
- package/src/lib/app-utils/account-emails.js.map +1 -0
- package/src/lib/app-utils/actions.d.ts +717 -0
- package/src/lib/app-utils/actions.js +1031 -0
- package/src/lib/app-utils/actions.js.map +1 -0
- package/src/lib/app-utils/activity-presenter.d.ts +152 -0
- package/src/lib/app-utils/activity-presenter.js +326 -0
- package/src/lib/app-utils/activity-presenter.js.map +1 -0
- package/src/lib/app-utils/advertising-tag-mounts.d.ts +133 -0
- package/src/lib/app-utils/advertising-tag-mounts.js +118 -0
- package/src/lib/app-utils/advertising-tag-mounts.js.map +1 -0
- package/src/lib/app-utils/advertising-tags.d.ts +307 -0
- package/src/lib/app-utils/advertising-tags.js +525 -0
- package/src/lib/app-utils/advertising-tags.js.map +1 -0
- package/src/lib/app-utils/agent-api-catalog.d.ts +94 -0
- package/src/lib/app-utils/agent-api-catalog.js +106 -0
- package/src/lib/app-utils/agent-api-catalog.js.map +1 -0
- package/src/lib/app-utils/agent-openapi.d.ts +103 -0
- package/src/lib/app-utils/agent-openapi.js +1128 -0
- package/src/lib/app-utils/agent-openapi.js.map +1 -0
- package/src/lib/app-utils/analytics-beacon.d.ts +154 -0
- package/src/lib/app-utils/analytics-beacon.js +152 -0
- package/src/lib/app-utils/analytics-beacon.js.map +1 -0
- package/src/lib/app-utils/analytics-environment.d.ts +133 -0
- package/src/lib/app-utils/analytics-environment.js +152 -0
- package/src/lib/app-utils/analytics-environment.js.map +1 -0
- package/src/lib/app-utils/analytics-events.d.ts +744 -0
- package/src/lib/app-utils/analytics-events.js +650 -0
- package/src/lib/app-utils/analytics-events.js.map +1 -0
- package/src/lib/app-utils/analytics-link-clicks.d.ts +102 -0
- package/src/lib/app-utils/analytics-link-clicks.js +267 -0
- package/src/lib/app-utils/analytics-link-clicks.js.map +1 -0
- package/src/lib/app-utils/analytics-path-key.d.ts +46 -0
- package/src/lib/app-utils/analytics-path-key.js +46 -0
- package/src/lib/app-utils/analytics-path-key.js.map +1 -0
- package/src/lib/app-utils/api-adapter.d.ts +47 -0
- package/src/lib/app-utils/api-adapter.js +367 -0
- package/src/lib/app-utils/api-adapter.js.map +1 -0
- package/src/lib/app-utils/api-idempotency.d.ts +169 -0
- package/src/lib/app-utils/api-idempotency.js +213 -0
- package/src/lib/app-utils/api-idempotency.js.map +1 -0
- package/src/lib/app-utils/api-plugins.d.ts +210 -0
- package/src/lib/app-utils/api-plugins.js +221 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -0
- package/src/lib/app-utils/assist-credits.d.ts +482 -0
- package/src/lib/app-utils/assist-credits.js +509 -0
- package/src/lib/app-utils/assist-credits.js.map +1 -0
- package/src/lib/app-utils/attribution-attribute.d.ts +34 -0
- package/src/lib/app-utils/attribution-attribute.js +34 -0
- package/src/lib/app-utils/attribution-attribute.js.map +1 -0
- package/src/lib/app-utils/attribution-guard.d.ts +130 -0
- package/src/lib/app-utils/attribution-guard.js +410 -0
- package/src/lib/app-utils/attribution-guard.js.map +1 -0
- package/src/lib/app-utils/author-css.d.ts +123 -0
- package/src/lib/app-utils/author-css.js +275 -0
- package/src/lib/app-utils/author-css.js.map +1 -0
- package/src/lib/app-utils/author-html-markdown.d.ts +31 -0
- package/src/lib/app-utils/author-html-markdown.js +497 -0
- package/src/lib/app-utils/author-html-markdown.js.map +1 -0
- package/src/lib/app-utils/author-html.d.ts +135 -0
- package/src/lib/app-utils/author-html.js +1037 -0
- package/src/lib/app-utils/author-html.js.map +1 -0
- package/src/lib/app-utils/automation-placeholders.d.ts +72 -0
- package/src/lib/app-utils/automation-placeholders.js +207 -0
- package/src/lib/app-utils/automation-placeholders.js.map +1 -0
- package/src/lib/app-utils/bandwidth-cap.d.ts +183 -0
- package/src/lib/app-utils/bandwidth-cap.js +193 -0
- package/src/lib/app-utils/bandwidth-cap.js.map +1 -0
- package/src/lib/app-utils/binding-token-catalog.d.ts +82 -0
- package/src/lib/app-utils/binding-token-catalog.js +293 -0
- package/src/lib/app-utils/binding-token-catalog.js.map +1 -0
- package/src/lib/app-utils/binding-tokens.d.ts +157 -0
- package/src/lib/app-utils/binding-tokens.js +251 -0
- package/src/lib/app-utils/binding-tokens.js.map +1 -0
- package/src/lib/app-utils/breadcrumb-json-ld.d.ts +57 -0
- package/src/lib/app-utils/breadcrumb-json-ld.js +67 -0
- package/src/lib/app-utils/breadcrumb-json-ld.js.map +1 -0
- package/src/lib/app-utils/campaign-attribution.d.ts +117 -0
- package/src/lib/app-utils/campaign-attribution.js +162 -0
- package/src/lib/app-utils/campaign-attribution.js.map +1 -0
- package/src/lib/app-utils/campaign-forwarding.d.ts +220 -0
- package/src/lib/app-utils/campaign-forwarding.js +332 -0
- package/src/lib/app-utils/campaign-forwarding.js.map +1 -0
- package/src/lib/app-utils/campaign-membership.d.ts +109 -0
- package/src/lib/app-utils/campaign-membership.js +176 -0
- package/src/lib/app-utils/campaign-membership.js.map +1 -0
- package/src/lib/app-utils/campaign-touch.d.ts +206 -0
- package/src/lib/app-utils/campaign-touch.js +326 -0
- package/src/lib/app-utils/campaign-touch.js.map +1 -0
- package/src/lib/app-utils/child-contract-compose.d.ts +28 -0
- package/src/lib/app-utils/child-contract-compose.js +140 -0
- package/src/lib/app-utils/child-contract-compose.js.map +1 -0
- package/src/lib/app-utils/child-contract.d.ts +129 -0
- package/src/lib/app-utils/child-contract.js +153 -0
- package/src/lib/app-utils/child-contract.js.map +1 -0
- package/src/lib/app-utils/collection-delete.d.ts +113 -0
- package/src/lib/app-utils/collection-delete.js +120 -0
- package/src/lib/app-utils/collection-delete.js.map +1 -0
- package/src/lib/app-utils/collection-entries.d.ts +833 -0
- package/src/lib/app-utils/collection-entries.js +1285 -0
- package/src/lib/app-utils/collection-entries.js.map +1 -0
- package/src/lib/app-utils/collection-entry-date.d.ts +129 -0
- package/src/lib/app-utils/collection-entry-date.js +183 -0
- package/src/lib/app-utils/collection-entry-date.js.map +1 -0
- package/src/lib/app-utils/collection-kind.d.ts +55 -0
- package/src/lib/app-utils/collection-kind.js +69 -0
- package/src/lib/app-utils/collection-kind.js.map +1 -0
- package/src/lib/app-utils/collection-slug.d.ts +79 -0
- package/src/lib/app-utils/collection-slug.js +70 -0
- package/src/lib/app-utils/collection-slug.js.map +1 -0
- package/src/lib/app-utils/compose-layout-nodes.d.ts +147 -0
- package/src/lib/app-utils/compose-layout-nodes.js +261 -0
- package/src/lib/app-utils/compose-layout-nodes.js.map +1 -0
- package/src/lib/app-utils/compose-layout-props.d.ts +71 -0
- package/src/lib/app-utils/compose-layout-props.js +61 -0
- package/src/lib/app-utils/compose-layout-props.js.map +1 -0
- package/src/lib/app-utils/compose-reusable-components.d.ts +504 -0
- package/src/lib/app-utils/compose-reusable-components.js +1366 -0
- package/src/lib/app-utils/compose-reusable-components.js.map +1 -0
- package/src/lib/app-utils/compress.d.ts +35 -0
- package/src/lib/app-utils/compress.js +38 -0
- package/src/lib/app-utils/compress.js.map +1 -0
- package/src/lib/app-utils/consent-banner-ui.d.ts +226 -0
- package/src/lib/app-utils/consent-banner-ui.js +583 -0
- package/src/lib/app-utils/consent-banner-ui.js.map +1 -0
- package/src/lib/app-utils/consent-groups.d.ts +189 -0
- package/src/lib/app-utils/consent-groups.js +219 -0
- package/src/lib/app-utils/consent-groups.js.map +1 -0
- package/src/lib/app-utils/consent-preferences-dialog.d.ts +58 -0
- package/src/lib/app-utils/consent-preferences-dialog.js +106 -0
- package/src/lib/app-utils/consent-preferences-dialog.js.map +1 -0
- package/src/lib/app-utils/console-record-links.d.ts +163 -0
- package/src/lib/app-utils/console-record-links.js +178 -0
- package/src/lib/app-utils/console-record-links.js.map +1 -0
- package/src/lib/app-utils/console-routes.d.ts +658 -0
- package/src/lib/app-utils/console-routes.js +489 -0
- package/src/lib/app-utils/console-routes.js.map +1 -0
- package/src/lib/app-utils/console-widget-slot-context.d.ts +46 -0
- package/src/lib/app-utils/console-widget-slot-context.js +23 -0
- package/src/lib/app-utils/console-widget-slot-context.js.map +1 -0
- package/src/lib/app-utils/contact-custom-fields.d.ts +191 -0
- package/src/lib/app-utils/contact-custom-fields.js +349 -0
- package/src/lib/app-utils/contact-custom-fields.js.map +1 -0
- package/src/lib/app-utils/contact-holder.d.ts +52 -0
- package/src/lib/app-utils/contact-holder.js +98 -0
- package/src/lib/app-utils/contact-holder.js.map +1 -0
- package/src/lib/app-utils/contact-merge.d.ts +136 -0
- package/src/lib/app-utils/contact-merge.js +379 -0
- package/src/lib/app-utils/contact-merge.js.map +1 -0
- package/src/lib/app-utils/contacts.d.ts +554 -0
- package/src/lib/app-utils/contacts.js +401 -0
- package/src/lib/app-utils/contacts.js.map +1 -0
- package/src/lib/app-utils/content-author-profile.d.ts +68 -0
- package/src/lib/app-utils/content-author-profile.js +118 -0
- package/src/lib/app-utils/content-author-profile.js.map +1 -0
- package/src/lib/app-utils/content-authors.d.ts +662 -0
- package/src/lib/app-utils/content-authors.js +771 -0
- package/src/lib/app-utils/content-authors.js.map +1 -0
- package/src/lib/app-utils/content-schema-type.d.ts +81 -0
- package/src/lib/app-utils/content-schema-type.js +87 -0
- package/src/lib/app-utils/content-schema-type.js.map +1 -0
- package/src/lib/app-utils/contexts.d.ts +35 -0
- package/src/lib/app-utils/contexts.js +35 -0
- package/src/lib/app-utils/contexts.js.map +1 -0
- package/src/lib/app-utils/create-resource-uid.d.ts +18 -0
- package/src/lib/app-utils/create-resource-uid.js +23 -0
- package/src/lib/app-utils/create-resource-uid.js.map +1 -0
- package/src/lib/app-utils/crm-booking.d.ts +75 -0
- package/src/lib/app-utils/crm-booking.js +133 -0
- package/src/lib/app-utils/crm-booking.js.map +1 -0
- package/src/lib/app-utils/crm-csv.d.ts +232 -0
- package/src/lib/app-utils/crm-csv.js +432 -0
- package/src/lib/app-utils/crm-csv.js.map +1 -0
- package/src/lib/app-utils/crm-digest.d.ts +179 -0
- package/src/lib/app-utils/crm-digest.js +271 -0
- package/src/lib/app-utils/crm-digest.js.map +1 -0
- package/src/lib/app-utils/crm-email-templates.d.ts +172 -0
- package/src/lib/app-utils/crm-email-templates.js +382 -0
- package/src/lib/app-utils/crm-email-templates.js.map +1 -0
- package/src/lib/app-utils/crm-inbound.d.ts +245 -0
- package/src/lib/app-utils/crm-inbound.js +407 -0
- package/src/lib/app-utils/crm-inbound.js.map +1 -0
- package/src/lib/app-utils/crm-kinds.d.ts +73 -0
- package/src/lib/app-utils/crm-kinds.js +92 -0
- package/src/lib/app-utils/crm-kinds.js.map +1 -0
- package/src/lib/app-utils/crm-next-activity.d.ts +107 -0
- package/src/lib/app-utils/crm-next-activity.js +140 -0
- package/src/lib/app-utils/crm-next-activity.js.map +1 -0
- package/src/lib/app-utils/crm-reports.d.ts +479 -0
- package/src/lib/app-utils/crm-reports.js +680 -0
- package/src/lib/app-utils/crm-reports.js.map +1 -0
- package/src/lib/app-utils/crm-task-reminders.d.ts +129 -0
- package/src/lib/app-utils/crm-task-reminders.js +139 -0
- package/src/lib/app-utils/crm-task-reminders.js.map +1 -0
- package/src/lib/app-utils/crm.d.ts +1546 -0
- package/src/lib/app-utils/crm.js +1814 -0
- package/src/lib/app-utils/crm.js.map +1 -0
- package/src/lib/app-utils/csp-report.d.ts +84 -0
- package/src/lib/app-utils/csp-report.js +177 -0
- package/src/lib/app-utils/csp-report.js.map +1 -0
- package/src/lib/app-utils/csv-import.d.ts +192 -0
- package/src/lib/app-utils/csv-import.js +279 -0
- package/src/lib/app-utils/csv-import.js.map +1 -0
- package/src/lib/app-utils/dataset-csv.d.ts +109 -0
- package/src/lib/app-utils/dataset-csv.js +235 -0
- package/src/lib/app-utils/dataset-csv.js.map +1 -0
- package/src/lib/app-utils/dataset-models.d.ts +194 -0
- package/src/lib/app-utils/dataset-models.js +409 -0
- package/src/lib/app-utils/dataset-models.js.map +1 -0
- package/src/lib/app-utils/dataset-query.d.ts +57 -0
- package/src/lib/app-utils/dataset-query.js +126 -0
- package/src/lib/app-utils/dataset-query.js.map +1 -0
- package/src/lib/app-utils/datasets.d.ts +136 -0
- package/src/lib/app-utils/datasets.js +177 -0
- package/src/lib/app-utils/datasets.js.map +1 -0
- package/src/lib/app-utils/decompress.d.ts +36 -0
- package/src/lib/app-utils/decompress.js +32 -0
- package/src/lib/app-utils/decompress.js.map +1 -0
- package/src/lib/app-utils/dedicated-sending-domain.d.ts +47 -0
- package/src/lib/app-utils/dedicated-sending-domain.js +114 -0
- package/src/lib/app-utils/dedicated-sending-domain.js.map +1 -0
- package/src/lib/app-utils/deferred-image.d.ts +86 -0
- package/src/lib/app-utils/deferred-image.js +83 -0
- package/src/lib/app-utils/deferred-image.js.map +1 -0
- package/src/lib/app-utils/definition-canvas-tree.d.ts +61 -0
- package/src/lib/app-utils/definition-canvas-tree.js +126 -0
- package/src/lib/app-utils/definition-canvas-tree.js.map +1 -0
- package/src/lib/app-utils/deployment-shape.d.ts +167 -0
- package/src/lib/app-utils/deployment-shape.js +238 -0
- package/src/lib/app-utils/deployment-shape.js.map +1 -0
- package/src/lib/app-utils/detect-template-placeholders.d.ts +30 -0
- package/src/lib/app-utils/detect-template-placeholders.js +53 -0
- package/src/lib/app-utils/detect-template-placeholders.js.map +1 -0
- package/src/lib/app-utils/dmca-counter-notice.d.ts +185 -0
- package/src/lib/app-utils/dmca-counter-notice.js +277 -0
- package/src/lib/app-utils/dmca-counter-notice.js.map +1 -0
- package/src/lib/app-utils/docs-deflection.d.ts +156 -0
- package/src/lib/app-utils/docs-deflection.js +547 -0
- package/src/lib/app-utils/docs-deflection.js.map +1 -0
- package/src/lib/app-utils/docs-help.d.ts +57 -0
- package/src/lib/app-utils/docs-help.generated.d.ts +303 -0
- package/src/lib/app-utils/docs-help.generated.js +867 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -0
- package/src/lib/app-utils/docs-help.js +46 -0
- package/src/lib/app-utils/docs-help.js.map +1 -0
- package/src/lib/app-utils/docs-index.generated.d.ts +29 -0
- package/src/lib/app-utils/docs-index.generated.js +8388 -0
- package/src/lib/app-utils/docs-index.generated.js.map +1 -0
- package/src/lib/app-utils/docs-retrieval.d.ts +67 -0
- package/src/lib/app-utils/docs-retrieval.js +188 -0
- package/src/lib/app-utils/docs-retrieval.js.map +1 -0
- package/src/lib/app-utils/document-landmark.d.ts +50 -0
- package/src/lib/app-utils/document-landmark.js +100 -0
- package/src/lib/app-utils/document-landmark.js.map +1 -0
- package/src/lib/app-utils/duplicate-resource.d.ts +101 -0
- package/src/lib/app-utils/duplicate-resource.js +206 -0
- package/src/lib/app-utils/duplicate-resource.js.map +1 -0
- package/src/lib/app-utils/dynamic-list-rule.d.ts +471 -0
- package/src/lib/app-utils/dynamic-list-rule.js +914 -0
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -0
- package/src/lib/app-utils/element-animation.d.ts +200 -0
- package/src/lib/app-utils/element-animation.js +233 -0
- package/src/lib/app-utils/element-animation.js.map +1 -0
- package/src/lib/app-utils/element-hidden-style.d.ts +46 -0
- package/src/lib/app-utils/element-hidden-style.js +48 -0
- package/src/lib/app-utils/element-hidden-style.js.map +1 -0
- package/src/lib/app-utils/element-ui.d.ts +243 -0
- package/src/lib/app-utils/element-ui.js +459 -0
- package/src/lib/app-utils/element-ui.js.map +1 -0
- package/src/lib/app-utils/email-starter-policy.d.ts +160 -0
- package/src/lib/app-utils/email-starter-policy.js +327 -0
- package/src/lib/app-utils/email-starter-policy.js.map +1 -0
- package/src/lib/app-utils/email-topics.d.ts +310 -0
- package/src/lib/app-utils/email-topics.js +336 -0
- package/src/lib/app-utils/email-topics.js.map +1 -0
- package/src/lib/app-utils/enabled-plugins-context.d.ts +100 -0
- package/src/lib/app-utils/enabled-plugins-context.js +113 -0
- package/src/lib/app-utils/enabled-plugins-context.js.map +1 -0
- package/src/lib/app-utils/ensure-canvas-root.d.ts +38 -0
- package/src/lib/app-utils/ensure-canvas-root.js +61 -0
- package/src/lib/app-utils/ensure-canvas-root.js.map +1 -0
- package/src/lib/app-utils/entity-picker-context.d.ts +199 -0
- package/src/lib/app-utils/entity-picker-context.js +93 -0
- package/src/lib/app-utils/entity-picker-context.js.map +1 -0
- package/src/lib/app-utils/error-beacon.d.ts +107 -0
- package/src/lib/app-utils/error-beacon.js +296 -0
- package/src/lib/app-utils/error-beacon.js.map +1 -0
- package/src/lib/app-utils/expand-repeatables.d.ts +172 -0
- package/src/lib/app-utils/expand-repeatables.js +299 -0
- package/src/lib/app-utils/expand-repeatables.js.map +1 -0
- package/src/lib/app-utils/foreign-script.d.ts +82 -0
- package/src/lib/app-utils/foreign-script.js +102 -0
- package/src/lib/app-utils/foreign-script.js.map +1 -0
- package/src/lib/app-utils/form-abuse-ceiling.d.ts +146 -0
- package/src/lib/app-utils/form-abuse-ceiling.js +158 -0
- package/src/lib/app-utils/form-abuse-ceiling.js.map +1 -0
- package/src/lib/app-utils/form-contract.d.ts +198 -0
- package/src/lib/app-utils/form-contract.js +282 -0
- package/src/lib/app-utils/form-contract.js.map +1 -0
- package/src/lib/app-utils/form-dataset-binding.d.ts +50 -0
- package/src/lib/app-utils/form-dataset-binding.js +74 -0
- package/src/lib/app-utils/form-dataset-binding.js.map +1 -0
- package/src/lib/app-utils/forms.d.ts +727 -0
- package/src/lib/app-utils/forms.js +777 -0
- package/src/lib/app-utils/forms.js.map +1 -0
- package/src/lib/app-utils/functions.d.ts +188 -0
- package/src/lib/app-utils/functions.js +529 -0
- package/src/lib/app-utils/functions.js.map +1 -0
- package/src/lib/app-utils/guarded-version-save.d.ts +91 -0
- package/src/lib/app-utils/guarded-version-save.js +131 -0
- package/src/lib/app-utils/guarded-version-save.js.map +1 -0
- package/src/lib/app-utils/health-report.d.ts +1766 -0
- package/src/lib/app-utils/health-report.js +2072 -0
- package/src/lib/app-utils/health-report.js.map +1 -0
- package/src/lib/app-utils/host-naming.d.ts +135 -0
- package/src/lib/app-utils/host-naming.js +188 -0
- package/src/lib/app-utils/host-naming.js.map +1 -0
- package/src/lib/app-utils/host-permissions.d.ts +76 -0
- package/src/lib/app-utils/host-permissions.js +119 -0
- package/src/lib/app-utils/host-permissions.js.map +1 -0
- package/src/lib/app-utils/host-tokens.d.ts +228 -0
- package/src/lib/app-utils/host-tokens.js +304 -0
- package/src/lib/app-utils/host-tokens.js.map +1 -0
- package/src/lib/app-utils/idp-profile.d.ts +83 -0
- package/src/lib/app-utils/idp-profile.js +268 -0
- package/src/lib/app-utils/idp-profile.js.map +1 -0
- package/src/lib/app-utils/image-upload-types.d.ts +49 -0
- package/src/lib/app-utils/image-upload-types.js +86 -0
- package/src/lib/app-utils/image-upload-types.js.map +1 -0
- package/src/lib/app-utils/index.d.ts +18 -0
- package/src/lib/app-utils/index.js +23 -0
- package/src/lib/app-utils/index.js.map +1 -0
- package/src/lib/app-utils/internal-traffic.d.ts +197 -0
- package/src/lib/app-utils/internal-traffic.js +288 -0
- package/src/lib/app-utils/internal-traffic.js.map +1 -0
- package/src/lib/app-utils/lineal-order.d.ts +62 -0
- package/src/lib/app-utils/lineal-order.js +65 -0
- package/src/lib/app-utils/lineal-order.js.map +1 -0
- package/src/lib/app-utils/link-references.d.ts +36 -0
- package/src/lib/app-utils/link-references.js +89 -0
- package/src/lib/app-utils/link-references.js.map +1 -0
- package/src/lib/app-utils/link-target-search-context.d.ts +123 -0
- package/src/lib/app-utils/link-target-search-context.js +221 -0
- package/src/lib/app-utils/link-target-search-context.js.map +1 -0
- package/src/lib/app-utils/list-assignment-policy.d.ts +169 -0
- package/src/lib/app-utils/list-assignment-policy.js +170 -0
- package/src/lib/app-utils/list-assignment-policy.js.map +1 -0
- package/src/lib/app-utils/list-import.d.ts +158 -0
- package/src/lib/app-utils/list-import.js +332 -0
- package/src/lib/app-utils/list-import.js.map +1 -0
- package/src/lib/app-utils/llms-txt.d.ts +104 -0
- package/src/lib/app-utils/llms-txt.js +144 -0
- package/src/lib/app-utils/llms-txt.js.map +1 -0
- package/src/lib/app-utils/local-storage-budget.d.ts +75 -0
- package/src/lib/app-utils/local-storage-budget.js +153 -0
- package/src/lib/app-utils/local-storage-budget.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +698 -0
- package/src/lib/app-utils/lockdown.js +929 -0
- package/src/lib/app-utils/lockdown.js.map +1 -0
- package/src/lib/app-utils/margin-utilization.d.ts +267 -0
- package/src/lib/app-utils/margin-utilization.js +351 -0
- package/src/lib/app-utils/margin-utilization.js.map +1 -0
- package/src/lib/app-utils/markdown-lite.d.ts +297 -0
- package/src/lib/app-utils/markdown-lite.js +705 -0
- package/src/lib/app-utils/markdown-lite.js.map +1 -0
- package/src/lib/app-utils/marketing-consent.d.ts +704 -0
- package/src/lib/app-utils/marketing-consent.js +783 -0
- package/src/lib/app-utils/marketing-consent.js.map +1 -0
- package/src/lib/app-utils/marketplace-listing-visibility.d.ts +120 -0
- package/src/lib/app-utils/marketplace-listing-visibility.js +139 -0
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -0
- package/src/lib/app-utils/marketplace-overrides.d.ts +271 -0
- package/src/lib/app-utils/marketplace-overrides.js +462 -0
- package/src/lib/app-utils/marketplace-overrides.js.map +1 -0
- package/src/lib/app-utils/marketplace-provenance.d.ts +150 -0
- package/src/lib/app-utils/marketplace-provenance.js +136 -0
- package/src/lib/app-utils/marketplace-provenance.js.map +1 -0
- package/src/lib/app-utils/marketplace-theme.d.ts +237 -0
- package/src/lib/app-utils/marketplace-theme.js +525 -0
- package/src/lib/app-utils/marketplace-theme.js.map +1 -0
- package/src/lib/app-utils/marketplace-update-state.d.ts +119 -0
- package/src/lib/app-utils/marketplace-update-state.js +132 -0
- package/src/lib/app-utils/marketplace-update-state.js.map +1 -0
- package/src/lib/app-utils/marketplace-verification.d.ts +95 -0
- package/src/lib/app-utils/marketplace-verification.js +81 -0
- package/src/lib/app-utils/marketplace-verification.js.map +1 -0
- package/src/lib/app-utils/measure-node-map.d.ts +76 -0
- package/src/lib/app-utils/measure-node-map.js +111 -0
- package/src/lib/app-utils/measure-node-map.js.map +1 -0
- package/src/lib/app-utils/media-alt.d.ts +55 -0
- package/src/lib/app-utils/media-alt.js +58 -0
- package/src/lib/app-utils/media-alt.js.map +1 -0
- package/src/lib/app-utils/media-asset-facts.d.ts +125 -0
- package/src/lib/app-utils/media-asset-facts.js +467 -0
- package/src/lib/app-utils/media-asset-facts.js.map +1 -0
- package/src/lib/app-utils/media-cdn-scope.d.ts +65 -0
- package/src/lib/app-utils/media-cdn-scope.js +75 -0
- package/src/lib/app-utils/media-cdn-scope.js.map +1 -0
- package/src/lib/app-utils/media-folders.d.ts +161 -0
- package/src/lib/app-utils/media-folders.js +230 -0
- package/src/lib/app-utils/media-folders.js.map +1 -0
- package/src/lib/app-utils/media-metadata.d.ts +277 -0
- package/src/lib/app-utils/media-metadata.js +388 -0
- package/src/lib/app-utils/media-metadata.js.map +1 -0
- package/src/lib/app-utils/media-picker-context.d.ts +93 -0
- package/src/lib/app-utils/media-picker-context.js +26 -0
- package/src/lib/app-utils/media-picker-context.js.map +1 -0
- package/src/lib/app-utils/media-quarantine.d.ts +247 -0
- package/src/lib/app-utils/media-quarantine.js +323 -0
- package/src/lib/app-utils/media-quarantine.js.map +1 -0
- package/src/lib/app-utils/media-ref.d.ts +661 -0
- package/src/lib/app-utils/media-ref.js +1055 -0
- package/src/lib/app-utils/media-ref.js.map +1 -0
- package/src/lib/app-utils/media-takedown-reach.d.ts +126 -0
- package/src/lib/app-utils/media-takedown-reach.js +143 -0
- package/src/lib/app-utils/media-takedown-reach.js.map +1 -0
- package/src/lib/app-utils/member-email-aliases.d.ts +180 -0
- package/src/lib/app-utils/member-email-aliases.js +217 -0
- package/src/lib/app-utils/member-email-aliases.js.map +1 -0
- package/src/lib/app-utils/merge-node-sx.d.ts +47 -0
- package/src/lib/app-utils/merge-node-sx.js +123 -0
- package/src/lib/app-utils/merge-node-sx.js.map +1 -0
- package/src/lib/app-utils/name-match.d.ts +155 -0
- package/src/lib/app-utils/name-match.js +175 -0
- package/src/lib/app-utils/name-match.js.map +1 -0
- package/src/lib/app-utils/name-search.d.ts +117 -0
- package/src/lib/app-utils/name-search.js +145 -0
- package/src/lib/app-utils/name-search.js.map +1 -0
- package/src/lib/app-utils/node-capabilities.d.ts +85 -0
- package/src/lib/app-utils/node-capabilities.js +88 -0
- package/src/lib/app-utils/node-capabilities.js.map +1 -0
- package/src/lib/app-utils/node-definition-sanitizer.d.ts +164 -0
- package/src/lib/app-utils/node-definition-sanitizer.js +402 -0
- package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -0
- package/src/lib/app-utils/node-identity.d.ts +47 -0
- package/src/lib/app-utils/node-identity.js +50 -0
- package/src/lib/app-utils/node-identity.js.map +1 -0
- package/src/lib/app-utils/node-interactions.d.ts +102 -0
- package/src/lib/app-utils/node-interactions.js +192 -0
- package/src/lib/app-utils/node-interactions.js.map +1 -0
- package/src/lib/app-utils/node-url-policy.d.ts +54 -0
- package/src/lib/app-utils/node-url-policy.js +72 -0
- package/src/lib/app-utils/node-url-policy.js.map +1 -0
- package/src/lib/app-utils/notifications.d.ts +71 -0
- package/src/lib/app-utils/notifications.js +101 -0
- package/src/lib/app-utils/notifications.js.map +1 -0
- package/src/lib/app-utils/onboarding-deep-link.d.ts +120 -0
- package/src/lib/app-utils/onboarding-deep-link.js +142 -0
- package/src/lib/app-utils/onboarding-deep-link.js.map +1 -0
- package/src/lib/app-utils/operator-identity.d.ts +207 -0
- package/src/lib/app-utils/operator-identity.js +186 -0
- package/src/lib/app-utils/operator-identity.js.map +1 -0
- package/src/lib/app-utils/org-billing-doc.d.ts +231 -0
- package/src/lib/app-utils/org-billing-doc.js +238 -0
- package/src/lib/app-utils/org-billing-doc.js.map +1 -0
- package/src/lib/app-utils/org-override-reason.d.ts +100 -0
- package/src/lib/app-utils/org-override-reason.js +116 -0
- package/src/lib/app-utils/org-override-reason.js.map +1 -0
- package/src/lib/app-utils/org-permissions.d.ts +194 -0
- package/src/lib/app-utils/org-permissions.js +357 -0
- package/src/lib/app-utils/org-permissions.js.map +1 -0
- package/src/lib/app-utils/org-roles.d.ts +72 -0
- package/src/lib/app-utils/org-roles.js +131 -0
- package/src/lib/app-utils/org-roles.js.map +1 -0
- package/src/lib/app-utils/organizations.d.ts +407 -0
- package/src/lib/app-utils/organizations.js +511 -0
- package/src/lib/app-utils/organizations.js.map +1 -0
- package/src/lib/app-utils/page-header-actions-context.d.ts +83 -0
- package/src/lib/app-utils/page-header-actions-context.js +58 -0
- package/src/lib/app-utils/page-header-actions-context.js.map +1 -0
- package/src/lib/app-utils/page-header-record-context.d.ts +80 -0
- package/src/lib/app-utils/page-header-record-context.js +64 -0
- package/src/lib/app-utils/page-header-record-context.js.map +1 -0
- package/src/lib/app-utils/page-markdown.d.ts +103 -0
- package/src/lib/app-utils/page-markdown.js +385 -0
- package/src/lib/app-utils/page-markdown.js.map +1 -0
- package/src/lib/app-utils/paid-media-source.d.ts +90 -0
- package/src/lib/app-utils/paid-media-source.js +184 -0
- package/src/lib/app-utils/paid-media-source.js.map +1 -0
- package/src/lib/app-utils/palette-sx.d.ts +34 -0
- package/src/lib/app-utils/palette-sx.js +150 -0
- package/src/lib/app-utils/palette-sx.js.map +1 -0
- package/src/lib/app-utils/password-policy.d.ts +47 -0
- package/src/lib/app-utils/password-policy.js +46 -0
- package/src/lib/app-utils/password-policy.js.map +1 -0
- package/src/lib/app-utils/payments-configured.d.ts +84 -0
- package/src/lib/app-utils/payments-configured.js +88 -0
- package/src/lib/app-utils/payments-configured.js.map +1 -0
- package/src/lib/app-utils/person-erasure.d.ts +126 -0
- package/src/lib/app-utils/person-erasure.js +121 -0
- package/src/lib/app-utils/person-erasure.js.map +1 -0
- package/src/lib/app-utils/person-key.d.ts +32 -0
- package/src/lib/app-utils/person-key.js +62 -0
- package/src/lib/app-utils/person-key.js.map +1 -0
- package/src/lib/app-utils/plan-entitlements.d.ts +2756 -0
- package/src/lib/app-utils/plan-entitlements.js +4085 -0
- package/src/lib/app-utils/plan-entitlements.js.map +1 -0
- package/src/lib/app-utils/platform-ad-conversions.d.ts +122 -0
- package/src/lib/app-utils/platform-ad-conversions.js +174 -0
- package/src/lib/app-utils/platform-ad-conversions.js.map +1 -0
- package/src/lib/app-utils/platform-advertising-tags.d.ts +139 -0
- package/src/lib/app-utils/platform-advertising-tags.js +203 -0
- package/src/lib/app-utils/platform-advertising-tags.js.map +1 -0
- package/src/lib/app-utils/platform-brand.d.ts +151 -0
- package/src/lib/app-utils/platform-brand.js +175 -0
- package/src/lib/app-utils/platform-brand.js.map +1 -0
- package/src/lib/app-utils/platform-consent-default.d.ts +142 -0
- package/src/lib/app-utils/platform-consent-default.js +295 -0
- package/src/lib/app-utils/platform-consent-default.js.map +1 -0
- package/src/lib/app-utils/platform-marketing-consent.d.ts +151 -0
- package/src/lib/app-utils/platform-marketing-consent.js +182 -0
- package/src/lib/app-utils/platform-marketing-consent.js.map +1 -0
- package/src/lib/app-utils/platform-marketing-host.d.ts +33 -0
- package/src/lib/app-utils/platform-marketing-host.js +97 -0
- package/src/lib/app-utils/platform-marketing-host.js.map +1 -0
- package/src/lib/app-utils/platform-visitor-consent.d.ts +252 -0
- package/src/lib/app-utils/platform-visitor-consent.js +533 -0
- package/src/lib/app-utils/platform-visitor-consent.js.map +1 -0
- package/src/lib/app-utils/plugin-api-cross-origin.d.ts +61 -0
- package/src/lib/app-utils/plugin-api-cross-origin.js +202 -0
- package/src/lib/app-utils/plugin-api-cross-origin.js.map +1 -0
- package/src/lib/app-utils/plugin-api-rate-limit.d.ts +192 -0
- package/src/lib/app-utils/plugin-api-rate-limit.js +270 -0
- package/src/lib/app-utils/plugin-api-rate-limit.js.map +1 -0
- package/src/lib/app-utils/plugin-artifact-path.d.ts +32 -0
- package/src/lib/app-utils/plugin-artifact-path.js +33 -0
- package/src/lib/app-utils/plugin-artifact-path.js.map +1 -0
- package/src/lib/app-utils/plugin-bundle-checks.d.ts +148 -0
- package/src/lib/app-utils/plugin-bundle-checks.js +989 -0
- package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -0
- package/src/lib/app-utils/plugin-manifest.d.ts +516 -0
- package/src/lib/app-utils/plugin-manifest.js +687 -0
- package/src/lib/app-utils/plugin-manifest.js.map +1 -0
- package/src/lib/app-utils/published-legal-pages.d.ts +71 -0
- package/src/lib/app-utils/published-legal-pages.js +90 -0
- package/src/lib/app-utils/published-legal-pages.js.map +1 -0
- package/src/lib/app-utils/publisher-agreement.d.ts +254 -0
- package/src/lib/app-utils/publisher-agreement.js +274 -0
- package/src/lib/app-utils/publisher-agreement.js.map +1 -0
- package/src/lib/app-utils/publisher-attestation.d.ts +99 -0
- package/src/lib/app-utils/publisher-attestation.js +158 -0
- package/src/lib/app-utils/publisher-attestation.js.map +1 -0
- package/src/lib/app-utils/recipient-email-render.d.ts +216 -0
- package/src/lib/app-utils/recipient-email-render.js +221 -0
- package/src/lib/app-utils/recipient-email-render.js.map +1 -0
- package/src/lib/app-utils/redispatch-caught-error.d.ts +38 -0
- package/src/lib/app-utils/redispatch-caught-error.js +46 -0
- package/src/lib/app-utils/redispatch-caught-error.js.map +1 -0
- package/src/lib/app-utils/registering-plugin.d.ts +19 -0
- package/src/lib/app-utils/registering-plugin.js +38 -0
- package/src/lib/app-utils/registering-plugin.js.map +1 -0
- package/src/lib/app-utils/release-flags.d.ts +283 -0
- package/src/lib/app-utils/release-flags.js +518 -0
- package/src/lib/app-utils/release-flags.js.map +1 -0
- package/src/lib/app-utils/repair-canvas-nodes.d.ts +130 -0
- package/src/lib/app-utils/repair-canvas-nodes.js +298 -0
- package/src/lib/app-utils/repair-canvas-nodes.js.map +1 -0
- package/src/lib/app-utils/repeat-infringer.d.ts +166 -0
- package/src/lib/app-utils/repeat-infringer.js +188 -0
- package/src/lib/app-utils/repeat-infringer.js.map +1 -0
- package/src/lib/app-utils/repeat-sources.d.ts +115 -0
- package/src/lib/app-utils/repeat-sources.js +86 -0
- package/src/lib/app-utils/repeat-sources.js.map +1 -0
- package/src/lib/app-utils/request-geo.d.ts +114 -0
- package/src/lib/app-utils/request-geo.js +247 -0
- package/src/lib/app-utils/request-geo.js.map +1 -0
- package/src/lib/app-utils/request-ip.d.ts +213 -0
- package/src/lib/app-utils/request-ip.js +344 -0
- package/src/lib/app-utils/request-ip.js.map +1 -0
- package/src/lib/app-utils/resolve-named-tokens.d.ts +26 -0
- package/src/lib/app-utils/resolve-named-tokens.js +59 -0
- package/src/lib/app-utils/resolve-named-tokens.js.map +1 -0
- package/src/lib/app-utils/responsive-typography-sx.d.ts +77 -0
- package/src/lib/app-utils/responsive-typography-sx.js +122 -0
- package/src/lib/app-utils/responsive-typography-sx.js.map +1 -0
- package/src/lib/app-utils/reusable-component-keys.d.ts +135 -0
- package/src/lib/app-utils/reusable-component-keys.js +140 -0
- package/src/lib/app-utils/reusable-component-keys.js.map +1 -0
- package/src/lib/app-utils/reusable-prop-values.d.ts +199 -0
- package/src/lib/app-utils/reusable-prop-values.js +494 -0
- package/src/lib/app-utils/reusable-prop-values.js.map +1 -0
- package/src/lib/app-utils/safe-json-ld.d.ts +29 -0
- package/src/lib/app-utils/safe-json-ld.js +37 -0
- package/src/lib/app-utils/safe-json-ld.js.map +1 -0
- package/src/lib/app-utils/sanitize-svg.d.ts +55 -0
- package/src/lib/app-utils/sanitize-svg.js +460 -0
- package/src/lib/app-utils/sanitize-svg.js.map +1 -0
- package/src/lib/app-utils/scheme-sx.d.ts +84 -0
- package/src/lib/app-utils/scheme-sx.js +148 -0
- package/src/lib/app-utils/scheme-sx.js.map +1 -0
- package/src/lib/app-utils/scope-tokens.d.ts +249 -0
- package/src/lib/app-utils/scope-tokens.js +331 -0
- package/src/lib/app-utils/scope-tokens.js.map +1 -0
- package/src/lib/app-utils/screen-link-context-value.d.ts +57 -0
- package/src/lib/app-utils/screen-link-context-value.js +43 -0
- package/src/lib/app-utils/screen-link-context-value.js.map +1 -0
- package/src/lib/app-utils/screen-link-context.d.ts +185 -0
- package/src/lib/app-utils/screen-link-context.js +304 -0
- package/src/lib/app-utils/screen-link-context.js.map +1 -0
- package/src/lib/app-utils/screen-link-value.d.ts +269 -0
- package/src/lib/app-utils/screen-link-value.js +384 -0
- package/src/lib/app-utils/screen-link-value.js.map +1 -0
- package/src/lib/app-utils/screen-route.d.ts +669 -0
- package/src/lib/app-utils/screen-route.js +751 -0
- package/src/lib/app-utils/screen-route.js.map +1 -0
- package/src/lib/app-utils/screen-seo-fields.d.ts +37 -0
- package/src/lib/app-utils/screen-seo-fields.js +42 -0
- package/src/lib/app-utils/screen-seo-fields.js.map +1 -0
- package/src/lib/app-utils/search-indexing.d.ts +162 -0
- package/src/lib/app-utils/search-indexing.js +207 -0
- package/src/lib/app-utils/search-indexing.js.map +1 -0
- package/src/lib/app-utils/seo-listing-fields.d.ts +70 -0
- package/src/lib/app-utils/seo-listing-fields.js +75 -0
- package/src/lib/app-utils/seo-listing-fields.js.map +1 -0
- package/src/lib/app-utils/seo-locale.d.ts +119 -0
- package/src/lib/app-utils/seo-locale.js +133 -0
- package/src/lib/app-utils/seo-locale.js.map +1 -0
- package/src/lib/app-utils/seo-title-variables.d.ts +149 -0
- package/src/lib/app-utils/seo-title-variables.js +190 -0
- package/src/lib/app-utils/seo-title-variables.js.map +1 -0
- package/src/lib/app-utils/seo-title.d.ts +70 -0
- package/src/lib/app-utils/seo-title.js +117 -0
- package/src/lib/app-utils/seo-title.js.map +1 -0
- package/src/lib/app-utils/server.d.ts +174 -0
- package/src/lib/app-utils/server.js +314 -0
- package/src/lib/app-utils/server.js.map +1 -0
- package/src/lib/app-utils/site-context.d.ts +106 -0
- package/src/lib/app-utils/site-context.js +100 -0
- package/src/lib/app-utils/site-context.js.map +1 -0
- package/src/lib/app-utils/sitemap.d.ts +117 -0
- package/src/lib/app-utils/sitemap.js +183 -0
- package/src/lib/app-utils/sitemap.js.map +1 -0
- package/src/lib/app-utils/social-image.d.ts +112 -0
- package/src/lib/app-utils/social-image.js +191 -0
- package/src/lib/app-utils/social-image.js.map +1 -0
- package/src/lib/app-utils/starter-templates.d.ts +99 -0
- package/src/lib/app-utils/starter-templates.js +579 -0
- package/src/lib/app-utils/starter-templates.js.map +1 -0
- package/src/lib/app-utils/stored-nodes.d.ts +120 -0
- package/src/lib/app-utils/stored-nodes.js +174 -0
- package/src/lib/app-utils/stored-nodes.js.map +1 -0
- package/src/lib/app-utils/strip-undefined.d.ts +59 -0
- package/src/lib/app-utils/strip-undefined.js +95 -0
- package/src/lib/app-utils/strip-undefined.js.map +1 -0
- package/src/lib/app-utils/stripe-deployment-mode.d.ts +47 -0
- package/src/lib/app-utils/stripe-deployment-mode.js +70 -0
- package/src/lib/app-utils/stripe-deployment-mode.js.map +1 -0
- package/src/lib/app-utils/support-tiers.d.ts +108 -0
- package/src/lib/app-utils/support-tiers.js +201 -0
- package/src/lib/app-utils/support-tiers.js.map +1 -0
- package/src/lib/app-utils/timezone-geo-hint.d.ts +36 -0
- package/src/lib/app-utils/timezone-geo-hint.js +180 -0
- package/src/lib/app-utils/timezone-geo-hint.js.map +1 -0
- package/src/lib/app-utils/upload-cors.d.ts +155 -0
- package/src/lib/app-utils/upload-cors.js +254 -0
- package/src/lib/app-utils/upload-cors.js.map +1 -0
- package/src/lib/app-utils/upload-inspection.d.ts +159 -0
- package/src/lib/app-utils/upload-inspection.js +539 -0
- package/src/lib/app-utils/upload-inspection.js.map +1 -0
- package/src/lib/app-utils/url-slug.d.ts +40 -0
- package/src/lib/app-utils/url-slug.js +42 -0
- package/src/lib/app-utils/url-slug.js.map +1 -0
- package/src/lib/app-utils/usage-budget.d.ts +429 -0
- package/src/lib/app-utils/usage-budget.js +421 -0
- package/src/lib/app-utils/usage-budget.js.map +1 -0
- package/src/lib/app-utils/variables.d.ts +96 -0
- package/src/lib/app-utils/variables.js +233 -0
- package/src/lib/app-utils/variables.js.map +1 -0
- package/src/lib/app-utils/video-asset-facts.d.ts +61 -0
- package/src/lib/app-utils/video-asset-facts.js +70 -0
- package/src/lib/app-utils/video-asset-facts.js.map +1 -0
- package/src/lib/app-utils/video-object.d.ts +89 -0
- package/src/lib/app-utils/video-object.js +245 -0
- package/src/lib/app-utils/video-object.js.map +1 -0
- package/src/lib/app-utils/visitor-consent.d.ts +757 -0
- package/src/lib/app-utils/visitor-consent.js +981 -0
- package/src/lib/app-utils/visitor-consent.js.map +1 -0
- package/src/lib/app-utils/visitor-record-ceiling.d.ts +223 -0
- package/src/lib/app-utils/visitor-record-ceiling.js +215 -0
- package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -0
- package/src/lib/app-utils/web-vitals-rum.d.ts +53 -0
- package/src/lib/app-utils/web-vitals-rum.js +278 -0
- package/src/lib/app-utils/web-vitals-rum.js.map +1 -0
- package/src/lib/app-utils/webhook-delivery.d.ts +324 -0
- package/src/lib/app-utils/webhook-delivery.js +413 -0
- package/src/lib/app-utils/webhook-delivery.js.map +1 -0
- package/src/lib/app-utils/where-used.d.ts +57 -0
- package/src/lib/app-utils/where-used.js +59 -0
- package/src/lib/app-utils/where-used.js.map +1 -0
- package/src/lib/app-utils/wistia-embed.d.ts +86 -0
- package/src/lib/app-utils/wistia-embed.js +119 -0
- package/src/lib/app-utils/wistia-embed.js.map +1 -0
- package/src/lib/app-utils/workflows.d.ts +136 -0
- package/src/lib/app-utils/workflows.js +346 -0
- package/src/lib/app-utils/workflows.js.map +1 -0
- package/src/lib/canvas-manager/canvas-manager-tree.d.ts +109 -0
- package/src/lib/canvas-manager/canvas-manager-tree.js +139 -0
- package/src/lib/canvas-manager/canvas-manager-tree.js.map +1 -0
- package/src/lib/canvas-manager/canvas-manager.d.ts +549 -0
- package/src/lib/canvas-manager/canvas-manager.js +1311 -0
- package/src/lib/canvas-manager/canvas-manager.js.map +1 -0
- package/src/lib/canvas-manager/index.d.ts +20 -0
- package/src/lib/canvas-manager/index.js +21 -0
- package/src/lib/canvas-manager/index.js.map +1 -0
- package/src/lib/canvas-manager/stores/index.d.ts +17 -0
- package/src/lib/canvas-manager/stores/index.js +18 -0
- package/src/lib/canvas-manager/stores/index.js.map +1 -0
- package/src/lib/canvas-manager/stores/node.store.d.ts +30 -0
- package/src/lib/canvas-manager/stores/node.store.js +45 -0
- package/src/lib/canvas-manager/stores/node.store.js.map +1 -0
- package/src/lib/components-manager/components-manager.d.ts +56 -0
- package/src/lib/components-manager/components-manager.js +191 -0
- package/src/lib/components-manager/components-manager.js.map +1 -0
- package/src/lib/components-manager/index.d.ts +18 -0
- package/src/lib/components-manager/index.js +19 -0
- package/src/lib/components-manager/index.js.map +1 -0
- package/src/lib/emit-manager/emit-manager.d.ts +56 -0
- package/src/lib/emit-manager/emit-manager.js +60 -0
- package/src/lib/emit-manager/emit-manager.js.map +1 -0
- package/src/lib/emit-manager/index.d.ts +18 -0
- package/src/lib/emit-manager/index.js +19 -0
- package/src/lib/emit-manager/index.js.map +1 -0
- package/src/lib/foundation/constants/_internal.d.ts +18 -0
- package/src/lib/foundation/constants/_internal.js +62 -0
- package/src/lib/foundation/constants/_internal.js.map +1 -0
- package/src/lib/foundation/constants/app.d.ts +23 -0
- package/src/lib/foundation/constants/app.js +25 -0
- package/src/lib/foundation/constants/app.js.map +1 -0
- package/src/lib/foundation/constants/canvas.d.ts +18 -0
- package/src/lib/foundation/constants/canvas.js +18 -0
- package/src/lib/foundation/constants/canvas.js.map +1 -0
- package/src/lib/foundation/constants/components.d.ts +54 -0
- package/src/lib/foundation/constants/components.js +61 -0
- package/src/lib/foundation/constants/components.js.map +1 -0
- package/src/lib/foundation/constants/shared.d.ts +50 -0
- package/src/lib/foundation/constants/shared.js +49 -0
- package/src/lib/foundation/constants/shared.js.map +1 -0
- package/src/lib/foundation/constants/symbol.d.ts +63 -0
- package/src/lib/foundation/constants/symbol.js +75 -0
- package/src/lib/foundation/constants/symbol.js.map +1 -0
- package/src/lib/foundation/definitions/components.types.d.ts +372 -0
- package/src/lib/foundation/definitions/components.types.js +180 -0
- package/src/lib/foundation/definitions/components.types.js.map +1 -0
- package/src/lib/foundation/definitions/contact.types.d.ts +73 -0
- package/src/lib/foundation/definitions/contact.types.js +107 -0
- package/src/lib/foundation/definitions/contact.types.js.map +1 -0
- package/src/lib/foundation/definitions/host-content-collections.d.ts +137 -0
- package/src/lib/foundation/definitions/host-content-collections.js +219 -0
- package/src/lib/foundation/definitions/host-content-collections.js.map +1 -0
- package/src/lib/foundation/definitions/org-billing.types.d.ts +1361 -0
- package/src/lib/foundation/definitions/org-billing.types.js +77 -0
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -0
- package/src/lib/foundation/definitions/organization.types.d.ts +307 -0
- package/src/lib/foundation/definitions/organization.types.js +42 -0
- package/src/lib/foundation/definitions/organization.types.js.map +1 -0
- package/src/lib/foundation/definitions/platform.types.d.ts +1326 -0
- package/src/lib/foundation/definitions/platform.types.js +124 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -0
- package/src/lib/foundation/definitions/property-kinds.d.ts +189 -0
- package/src/lib/foundation/definitions/property-kinds.js +565 -0
- package/src/lib/foundation/definitions/property-kinds.js.map +1 -0
- package/src/lib/foundation/definitions/shared.d.ts +23 -0
- package/src/lib/foundation/definitions/shared.js +18 -0
- package/src/lib/foundation/definitions/shared.js.map +1 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +249 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js +499 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -0
- package/src/lib/foundation/index.d.ts +32 -0
- package/src/lib/foundation/index.js +67 -0
- package/src/lib/foundation/index.js.map +1 -0
- package/src/lib/lifecycle.d.ts +24 -0
- package/src/lib/lifecycle.js +39 -0
- package/src/lib/lifecycle.js.map +1 -0
- package/src/lib/mst/common.d.ts +17 -0
- package/src/lib/mst/common.js +23 -0
- package/src/lib/mst/common.js.map +1 -0
- package/src/lib/mst/index.d.ts +19 -0
- package/src/lib/mst/index.js +20 -0
- package/src/lib/mst/index.js.map +1 -0
- package/src/lib/mst/json.d.ts +22 -0
- package/src/lib/mst/json.js +23 -0
- package/src/lib/mst/json.js.map +1 -0
- package/src/lib/mst/types.d.ts +20 -0
- package/src/lib/mst/types.js +18 -0
- package/src/lib/mst/types.js.map +1 -0
- package/src/lib/plugin-manager/billing-webhook-hooks.d.ts +77 -0
- package/src/lib/plugin-manager/billing-webhook-hooks.js +55 -0
- package/src/lib/plugin-manager/billing-webhook-hooks.js.map +1 -0
- package/src/lib/plugin-manager/commerce-zone-props.d.ts +172 -0
- package/src/lib/plugin-manager/commerce-zone-props.js +31 -0
- package/src/lib/plugin-manager/commerce-zone-props.js.map +1 -0
- package/src/lib/plugin-manager/custom-fields.d.ts +60 -0
- package/src/lib/plugin-manager/custom-fields.js +39 -0
- package/src/lib/plugin-manager/custom-fields.js.map +1 -0
- package/src/lib/plugin-manager/editor-sessions.d.ts +109 -0
- package/src/lib/plugin-manager/editor-sessions.js +137 -0
- package/src/lib/plugin-manager/editor-sessions.js.map +1 -0
- package/src/lib/plugin-manager/enabled-plugins.d.ts +438 -0
- package/src/lib/plugin-manager/enabled-plugins.js +403 -0
- package/src/lib/plugin-manager/enabled-plugins.js.map +1 -0
- package/src/lib/plugin-manager/feature-plugins.d.ts +1314 -0
- package/src/lib/plugin-manager/feature-plugins.js +609 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +11 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +149 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -0
- package/src/lib/plugin-manager/index.d.ts +42 -0
- package/src/lib/plugin-manager/index.js +46 -0
- package/src/lib/plugin-manager/index.js.map +1 -0
- package/src/lib/plugin-manager/install-presets.d.ts +93 -0
- package/src/lib/plugin-manager/install-presets.js +86 -0
- package/src/lib/plugin-manager/install-presets.js.map +1 -0
- package/src/lib/plugin-manager/media-delivery-provider.d.ts +124 -0
- package/src/lib/plugin-manager/media-delivery-provider.js +53 -0
- package/src/lib/plugin-manager/media-delivery-provider.js.map +1 -0
- package/src/lib/plugin-manager/order-fulfilment.d.ts +142 -0
- package/src/lib/plugin-manager/order-fulfilment.js +104 -0
- package/src/lib/plugin-manager/order-fulfilment.js.map +1 -0
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +122 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js +140 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -0
- package/src/lib/plugin-manager/plugin-bridge.d.ts +98 -0
- package/src/lib/plugin-manager/plugin-bridge.js +154 -0
- package/src/lib/plugin-manager/plugin-bridge.js.map +1 -0
- package/src/lib/plugin-manager/plugin-config.d.ts +148 -0
- package/src/lib/plugin-manager/plugin-config.js +228 -0
- package/src/lib/plugin-manager/plugin-config.js.map +1 -0
- package/src/lib/plugin-manager/plugin-console-crons.d.ts +139 -0
- package/src/lib/plugin-manager/plugin-console-crons.js +177 -0
- package/src/lib/plugin-manager/plugin-console-crons.js.map +1 -0
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +220 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js +89 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -0
- package/src/lib/plugin-manager/plugin-contributions.d.ts +244 -0
- package/src/lib/plugin-manager/plugin-contributions.js +283 -0
- package/src/lib/plugin-manager/plugin-contributions.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlement-keys.d.ts +169 -0
- package/src/lib/plugin-manager/plugin-entitlement-keys.js +84 -0
- package/src/lib/plugin-manager/plugin-entitlement-keys.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +181 -0
- package/src/lib/plugin-manager/plugin-entitlements.js +188 -0
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -0
- package/src/lib/plugin-manager/plugin-events.d.ts +145 -0
- package/src/lib/plugin-manager/plugin-events.js +79 -0
- package/src/lib/plugin-manager/plugin-events.js.map +1 -0
- package/src/lib/plugin-manager/plugin-figures.d.ts +251 -0
- package/src/lib/plugin-manager/plugin-figures.js +252 -0
- package/src/lib/plugin-manager/plugin-figures.js.map +1 -0
- package/src/lib/plugin-manager/plugin-host-collections.d.ts +146 -0
- package/src/lib/plugin-manager/plugin-host-collections.js +116 -0
- package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -0
- package/src/lib/plugin-manager/plugin-jobs.d.ts +130 -0
- package/src/lib/plugin-manager/plugin-jobs.js +182 -0
- package/src/lib/plugin-manager/plugin-jobs.js.map +1 -0
- package/src/lib/plugin-manager/plugin-loader.d.ts +86 -0
- package/src/lib/plugin-manager/plugin-loader.js +223 -0
- package/src/lib/plugin-manager/plugin-loader.js.map +1 -0
- package/src/lib/plugin-manager/plugin-manager.d.ts +95 -0
- package/src/lib/plugin-manager/plugin-manager.js +343 -0
- package/src/lib/plugin-manager/plugin-manager.js.map +1 -0
- package/src/lib/plugin-manager/plugin-metered-lines.d.ts +97 -0
- package/src/lib/plugin-manager/plugin-metered-lines.js +123 -0
- package/src/lib/plugin-manager/plugin-metered-lines.js.map +1 -0
- package/src/lib/plugin-manager/plugin-org-erasure.d.ts +52 -0
- package/src/lib/plugin-manager/plugin-org-erasure.js +100 -0
- package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -0
- package/src/lib/plugin-manager/plugin-permissions.d.ts +69 -0
- package/src/lib/plugin-manager/plugin-permissions.js +77 -0
- package/src/lib/plugin-manager/plugin-permissions.js.map +1 -0
- package/src/lib/plugin-manager/plugin-person-erasure.d.ts +58 -0
- package/src/lib/plugin-manager/plugin-person-erasure.js +109 -0
- package/src/lib/plugin-manager/plugin-person-erasure.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-cards.d.ts +99 -0
- package/src/lib/plugin-manager/plugin-record-cards.js +59 -0
- package/src/lib/plugin-manager/plugin-record-cards.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-facts.d.ts +111 -0
- package/src/lib/plugin-manager/plugin-record-facts.js +51 -0
- package/src/lib/plugin-manager/plugin-record-facts.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-routes.d.ts +129 -0
- package/src/lib/plugin-manager/plugin-record-routes.js +93 -0
- package/src/lib/plugin-manager/plugin-record-routes.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-timeline.d.ts +147 -0
- package/src/lib/plugin-manager/plugin-record-timeline.js +39 -0
- package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -0
- package/src/lib/plugin-manager/plugin-resource-drafts.d.ts +137 -0
- package/src/lib/plugin-manager/plugin-resource-drafts.js +51 -0
- package/src/lib/plugin-manager/plugin-resource-drafts.js.map +1 -0
- package/src/lib/plugin-manager/plugin-services.d.ts +94 -0
- package/src/lib/plugin-manager/plugin-services.js +153 -0
- package/src/lib/plugin-manager/plugin-services.js.map +1 -0
- package/src/lib/plugin-manager/plugin-styles-ui.d.ts +71 -0
- package/src/lib/plugin-manager/plugin-styles-ui.js +80 -0
- package/src/lib/plugin-manager/plugin-styles-ui.js.map +1 -0
- package/src/lib/plugin-manager/plugin-styles.d.ts +176 -0
- package/src/lib/plugin-manager/plugin-styles.js +186 -0
- package/src/lib/plugin-manager/plugin-styles.js.map +1 -0
- package/src/lib/plugin-manager/plugin-subprocessors.d.ts +135 -0
- package/src/lib/plugin-manager/plugin-subprocessors.js +129 -0
- package/src/lib/plugin-manager/plugin-subprocessors.js.map +1 -0
- package/src/lib/plugin-manager/plugin-tax-profile.d.ts +84 -0
- package/src/lib/plugin-manager/plugin-tax-profile.js +39 -0
- package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -0
- package/src/lib/plugin-manager/plugin-user-erasure.d.ts +53 -0
- package/src/lib/plugin-manager/plugin-user-erasure.js +84 -0
- package/src/lib/plugin-manager/plugin-user-erasure.js.map +1 -0
- package/src/lib/plugin-manager/plugin-zones.d.ts +121 -0
- package/src/lib/plugin-manager/plugin-zones.js +74 -0
- package/src/lib/plugin-manager/plugin-zones.js.map +1 -0
- package/src/lib/plugin-manager/realm-plugins.d.ts +112 -0
- package/src/lib/plugin-manager/realm-plugins.js +196 -0
- package/src/lib/plugin-manager/realm-plugins.js.map +1 -0
- package/src/lib/plugin-manager/realm-server.d.ts +54 -0
- package/src/lib/plugin-manager/realm-server.js +88 -0
- package/src/lib/plugin-manager/realm-server.js.map +1 -0
- package/src/lib/plugin-manager/record-zone-props.d.ts +126 -0
- package/src/lib/plugin-manager/record-zone-props.js +31 -0
- package/src/lib/plugin-manager/record-zone-props.js.map +1 -0
- package/src/lib/plugin-manager/site-page-hooks.d.ts +102 -0
- package/src/lib/plugin-manager/site-page-hooks.js +114 -0
- package/src/lib/plugin-manager/site-page-hooks.js.map +1 -0
- package/src/lib/plugin-manager/site-runtime.d.ts +57 -0
- package/src/lib/plugin-manager/site-runtime.js +25 -0
- package/src/lib/plugin-manager/site-runtime.js.map +1 -0
- package/src/lib/plugin-manager/usage-alert-contributors.d.ts +115 -0
- package/src/lib/plugin-manager/usage-alert-contributors.js +106 -0
- package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -0
- package/src/lib/types/ast.d.ts +199 -0
- package/src/lib/types/ast.js +20 -0
- package/src/lib/types/ast.js.map +1 -0
- package/src/lib/types/index.d.ts +20 -0
- package/src/lib/types/index.js +21 -0
- package/src/lib/types/index.js.map +1 -0
- package/src/lib/types/nodes.d.ts +608 -0
- package/src/lib/types/nodes.js +28 -0
- package/src/lib/types/nodes.js.map +1 -0
- package/src/lib/types/routing.d.ts +17 -0
- package/src/lib/types/routing.js +18 -0
- package/src/lib/types/routing.js.map +1 -0
- package/src/lib/types/screen.d.ts +23 -0
- package/src/lib/types/screen.js +18 -0
- package/src/lib/types/screen.js.map +1 -0
- package/src/lib/utils.d.ts +25 -0
- package/src/lib/utils.js +42 -0
- package/src/lib/utils.js.map +1 -0
- package/src/server.d.ts +37 -0
- package/src/server.js +55 -0
- package/src/server.js.map +1 -0
|
@@ -0,0 +1,2072 @@
|
|
|
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
|
+
* Shared shape for the `/api/health` endpoints (AGL-1102).
|
|
18
|
+
*
|
|
19
|
+
* Both the console and the tenant runtime answer this, and they must answer it
|
|
20
|
+
* IDENTICALLY — an uptime series is only comparable across services if the
|
|
21
|
+
* status codes and the field names agree. Two near-identical route files would
|
|
22
|
+
* have drifted the first time one of them was touched.
|
|
23
|
+
*
|
|
24
|
+
* The rules encoded here, each of which is a way health checks routinely lie:
|
|
25
|
+
*
|
|
26
|
+
* **The status code is the contract.** Most uptime monitors read nothing else,
|
|
27
|
+
* so a degraded dependency has to be a 503. A 200 whose body says "degraded"
|
|
28
|
+
* is a signal nobody receives.
|
|
29
|
+
*
|
|
30
|
+
* **Never cacheable.** A cached health check returns 200 from the edge while
|
|
31
|
+
* the origin is on fire, and the graph stays green straight through the
|
|
32
|
+
* outage. Three separate caches have faked a green check in this repo already,
|
|
33
|
+
* so this sets `no-store` on every response including the failures — an error
|
|
34
|
+
* response is the one you least want served from a cache.
|
|
35
|
+
*
|
|
36
|
+
* **No internals in the body.** It is public and unauthenticated, so a failing
|
|
37
|
+
* check reports a CODE and never the underlying message, which can carry
|
|
38
|
+
* project ids, paths or credentials fragments.
|
|
39
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
40
|
+
export const HEALTH_NO_STORE = 'no-store, no-cache, must-revalidate, max-age=0';
|
|
41
|
+
/** Degraded if ANY check failed. There is no partial credit for uptime. */ export function healthStatus(checks) {
|
|
42
|
+
return Object.values(checks).every((check)=>check.ok) ? 'ok' : 'degraded';
|
|
43
|
+
}
|
|
44
|
+
export function healthBody(report) {
|
|
45
|
+
var _report_commit, _report_version, _report_environment, _report_region, _report_at;
|
|
46
|
+
return {
|
|
47
|
+
status: healthStatus(report.checks),
|
|
48
|
+
service: report.service,
|
|
49
|
+
checks: report.checks,
|
|
50
|
+
commit: (_report_commit = report.commit) != null ? _report_commit : null,
|
|
51
|
+
version: (_report_version = report.version) != null ? _report_version : null,
|
|
52
|
+
environment: (_report_environment = report.environment) != null ? _report_environment : null,
|
|
53
|
+
region: (_report_region = report.region) != null ? _report_region : null,
|
|
54
|
+
at: (_report_at = report.at) != null ? _report_at : new Date().toISOString()
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function healthHeaders(status) {
|
|
58
|
+
return _extends({
|
|
59
|
+
'Cache-Control': HEALTH_NO_STORE,
|
|
60
|
+
// Readable from any origin, deliberately.
|
|
61
|
+
//
|
|
62
|
+
// A status page must not live on the thing it reports on, so it is served
|
|
63
|
+
// from a DIFFERENT deployment and reads these cross-origin. Without this
|
|
64
|
+
// header the browser blocks the read and the page renders every service as
|
|
65
|
+
// down during a perfectly healthy day — a status page that cries outage is
|
|
66
|
+
// worse than none.
|
|
67
|
+
//
|
|
68
|
+
// Safe to open: the body is already public and unauthenticated, carries no
|
|
69
|
+
// secrets, and a failing check reports a code rather than a message.
|
|
70
|
+
'Access-Control-Allow-Origin': '*'
|
|
71
|
+
}, status === 'ok' ? {} : {
|
|
72
|
+
'Retry-After': '30'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function healthHttpStatus(status) {
|
|
76
|
+
return status === 'ok' ? 200 : 503;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A HEAD response for a health endpoint: the SAME status and headers its GET
|
|
80
|
+
* would return, with no body (AGL-1148).
|
|
81
|
+
*
|
|
82
|
+
* ## Why this exists
|
|
83
|
+
*
|
|
84
|
+
* Every health route used to answer HEAD with a hardcoded `200` and the
|
|
85
|
+
* comment "cheap liveness for monitors that only issue HEAD. Touches
|
|
86
|
+
* nothing." Touching nothing is precisely the problem: **a HEAD that knows
|
|
87
|
+
* nothing is a health check that cannot go red.** A monitor configured with
|
|
88
|
+
* HEAD — which several uptime providers use by default — would have reported
|
|
89
|
+
* green through any outage that left the function able to boot, which is most
|
|
90
|
+
* of them. `/api/health/crons` sat at 503 for fifty-one hours; a HEAD monitor
|
|
91
|
+
* pointed at it would have agreed with the green board the whole time.
|
|
92
|
+
*
|
|
93
|
+
* It also violated the contract this file opens with. "The status code is the
|
|
94
|
+
* contract" cannot hold for one method and not another, and HTTP requires a
|
|
95
|
+
* HEAD response to carry the same status and headers its GET would.
|
|
96
|
+
*
|
|
97
|
+
* ## The cost argument, which is why this is safe
|
|
98
|
+
*
|
|
99
|
+
* Delegating to GET means HEAD runs the dependency probe. Every route
|
|
100
|
+
* memoizes that probe (`memoizeWithTtl`, 15s on the root and 5 minutes on the
|
|
101
|
+
* subsystems) explicitly so a public unauthenticated endpoint cannot be made
|
|
102
|
+
* to read in a loop — the same bound that already lets a fifteen-minute probe
|
|
103
|
+
* hit seven endpoints for nearly nothing. HEAD now costs exactly what GET
|
|
104
|
+
* costs, which is a memo lookup between refreshes, and it serializes no body.
|
|
105
|
+
*
|
|
106
|
+
* Headers are carried over rather than rebuilt so `Cache-Control: no-store`,
|
|
107
|
+
* the open CORS origin and `Retry-After` reach a HEAD client identically — a
|
|
108
|
+
* HEAD response that was cacheable while its GET was not would be the same
|
|
109
|
+
* lie one layer down.
|
|
110
|
+
*/ export async function healthHeadOf(get) {
|
|
111
|
+
const response = await get();
|
|
112
|
+
return new Response(null, {
|
|
113
|
+
status: response.status,
|
|
114
|
+
headers: response.headers
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Weekly Sunday schedule + one day of slack. A healthy cadence never exceeds
|
|
119
|
+
* 7; day 8 means the last run produced nothing usable.
|
|
120
|
+
*/ export const MAX_BACKUP_AGE_DAYS = 8;
|
|
121
|
+
/**
|
|
122
|
+
* States in which a backup has not FAILED — it is simply not usable yet.
|
|
123
|
+
*
|
|
124
|
+
* `STATE_UNSPECIFIED` is a real member of the API's `Backup.State` enum ("The
|
|
125
|
+
* state is unspecified"), and proto3 JSON omits a default-valued field, so a
|
|
126
|
+
* backup in it arrives with **no `state` key at all**. Treating that absence
|
|
127
|
+
* as a failure is the `strictNullChecks`-off trap this file is otherwise
|
|
128
|
+
* careful about: a missing field must never fold into a red.
|
|
129
|
+
*/ const BACKUP_IN_FLIGHT = new Set([
|
|
130
|
+
'CREATING',
|
|
131
|
+
'STATE_UNSPECIFIED'
|
|
132
|
+
]);
|
|
133
|
+
/**
|
|
134
|
+
* `backups` is the listing, or `null` when it could not be read at all.
|
|
135
|
+
*
|
|
136
|
+
* `options.unreachable` is `ListBackupsResponse.unreachable` — the locations
|
|
137
|
+
* the API could not reach. Non-empty means the listing is a PARTIAL result
|
|
138
|
+
* set. `options.code` is the code to report when `backups` is `null`.
|
|
139
|
+
*/ export function backupsHealth(backups, ms, now = Date.now(), options = {}) {
|
|
140
|
+
var _options_unreachable;
|
|
141
|
+
// ## THE THREE STATES (AGL-1843, second pass)
|
|
142
|
+
//
|
|
143
|
+
// This check used to have two: healthy, and failed. Everything it could not
|
|
144
|
+
// establish — an upstream that errored, a partial listing, a run that had
|
|
145
|
+
// not finished — collapsed into "backup failed", which is the loudest thing
|
|
146
|
+
// it can say and was, in every one of those cases, false. It reported 503
|
|
147
|
+
// `backup-failed` continuously for four and a half days while the backups
|
|
148
|
+
// were fine, against a monitor that emails on a five-minute interval.
|
|
149
|
+
//
|
|
150
|
+
// ### Why `NOT_AVAILABLE` is not a failure — measured, not assumed
|
|
151
|
+
//
|
|
152
|
+
// The API documents the state as "The backup is not available **at this
|
|
153
|
+
// moment**", and that temporal wording is literal. The same three backup
|
|
154
|
+
// ids, read from `gcloud firestore backups list` on three dates:
|
|
155
|
+
//
|
|
156
|
+
// id 2026-08-13 2026-08-17/18 2026-08-24
|
|
157
|
+
// 3b5238df… NOT_AVAILABLE NOT_AVAILABLE READY
|
|
158
|
+
// eb4d21e3… READY NOT_AVAILABLE READY
|
|
159
|
+
// d14ce827… — READY READY
|
|
160
|
+
//
|
|
161
|
+
// `NOT_AVAILABLE` flips in BOTH directions, on backups whose `expireTime`
|
|
162
|
+
// is months away, independent of age. The 2026-08-02 backup that AGL-1490
|
|
163
|
+
// was filed about — "half the Firestore backups are NOT_AVAILABLE" — is
|
|
164
|
+
// READY today, twenty-two days old. It was never broken. Neither was the
|
|
165
|
+
// 2026-08-09 one, which went READY → NOT_AVAILABLE → READY. And the
|
|
166
|
+
// predicted permanent day-7 degradation did not happen: d14ce827 is READY
|
|
167
|
+
// at day 8. So every rule that reads a `NOT_AVAILABLE` count as damage is
|
|
168
|
+
// reading a maintenance window as an outage.
|
|
169
|
+
//
|
|
170
|
+
// ### What the check actually needs to answer
|
|
171
|
+
//
|
|
172
|
+
// "Can we restore, and is the restore point recent?" That question is
|
|
173
|
+
// answered ENTIRELY by the age of the newest READY backup. No other row in
|
|
174
|
+
// the listing can make a fresh READY backup un-restorable. So a fresh READY
|
|
175
|
+
// backup is unconditionally green, and everything below it is the taxonomy
|
|
176
|
+
// of why we could not find one.
|
|
177
|
+
//
|
|
178
|
+
// ### Why indeterminate answers 200, and why that is not fail-open
|
|
179
|
+
//
|
|
180
|
+
// Three things bound it, because a backups check that cannot go red is
|
|
181
|
+
// worse than no backups check:
|
|
182
|
+
// 1. It is time-bounded. Indeterminacy is only tolerated while a backup
|
|
183
|
+
// younger than `MAX_BACKUP_AGE_DAYS` exists. A schedule that stops, or
|
|
184
|
+
// that never produces anything usable, crosses the budget and goes red
|
|
185
|
+
// within eight days whatever the states say.
|
|
186
|
+
// 2. It never overrides staleness. If a READY backup exists and is over
|
|
187
|
+
// budget, that is `backup-stale` — a newer CREATING or NOT_AVAILABLE
|
|
188
|
+
// row earns no freshness credit, so a run that hangs every week cannot
|
|
189
|
+
// hold the check green.
|
|
190
|
+
// 3. The route classifies PERMANENT upstream failures (no credential,
|
|
191
|
+
// 401/403/404) as hard red rather than indeterminate, so a revoked IAM
|
|
192
|
+
// role cannot silence this forever. And `checks.exports` in the same
|
|
193
|
+
// endpoint is an independent, still-hard-red probe of the GCS copy:
|
|
194
|
+
// "we cannot read the managed backups" answers 200 only while we can
|
|
195
|
+
// still prove a fresh independent copy exists.
|
|
196
|
+
//
|
|
197
|
+
// State 3a — the listing could not be read AT ALL. That is not a verdict;
|
|
198
|
+
// it is the absence of one, and reporting it as `backup-failed` is the
|
|
199
|
+
// defect this pass exists to remove.
|
|
200
|
+
if (backups === null) {
|
|
201
|
+
var _options_code;
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
ms,
|
|
205
|
+
code: (_options_code = options.code) != null ? _options_code : 'backups-unreadable',
|
|
206
|
+
states: {},
|
|
207
|
+
newestReadyAgeDays: null,
|
|
208
|
+
determinate: false
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const states = {};
|
|
212
|
+
for (const backup of backups){
|
|
213
|
+
var _backup_state, _states_state;
|
|
214
|
+
const state = (_backup_state = backup.state) != null ? _backup_state : 'STATE_UNSPECIFIED';
|
|
215
|
+
states[state] = ((_states_state = states[state]) != null ? _states_state : 0) + 1;
|
|
216
|
+
}
|
|
217
|
+
const snapshotMsOf = (backup)=>backup.snapshotTime ? Date.parse(backup.snapshotTime) : Number.NaN;
|
|
218
|
+
const newestOf = (keep)=>backups.filter(keep).map(snapshotMsOf).filter((time)=>Number.isFinite(time)).reduce((newest, time)=>Math.max(newest != null ? newest : time, time), null);
|
|
219
|
+
const ageDaysOf = (time)=>Math.round((now - time) / 86400000 * 10) / 10;
|
|
220
|
+
const newestReadyMs = newestOf((backup)=>backup.state === 'READY');
|
|
221
|
+
const newestAnyMs = newestOf(()=>true);
|
|
222
|
+
const newestReadyAgeDays = newestReadyMs === null ? null : ageDaysOf(newestReadyMs);
|
|
223
|
+
const newestAnyAgeDays = newestAnyMs === null ? null : ageDaysOf(newestAnyMs);
|
|
224
|
+
// State 1 — HEALTHY. A restore point exists and it is recent. Nothing else
|
|
225
|
+
// in the listing can falsify that, so nothing else is consulted.
|
|
226
|
+
if (newestReadyAgeDays !== null && newestReadyAgeDays <= MAX_BACKUP_AGE_DAYS) {
|
|
227
|
+
return {
|
|
228
|
+
ok: true,
|
|
229
|
+
ms,
|
|
230
|
+
states,
|
|
231
|
+
newestReadyAgeDays
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// State 2a — FAILED. A backup that is neither READY nor in flight and that
|
|
235
|
+
// carries no usable `snapshotTime` cannot be aged at all, so we cannot even
|
|
236
|
+
// say when the last run was. That is positive evidence of something wrong,
|
|
237
|
+
// and "assume it aged out" is the assumption that hides a broken run.
|
|
238
|
+
const undateable = backups.some((backup)=>{
|
|
239
|
+
var _backup_state;
|
|
240
|
+
return backup.state !== 'READY' && !BACKUP_IN_FLIGHT.has((_backup_state = backup.state) != null ? _backup_state : 'STATE_UNSPECIFIED') && !Number.isFinite(snapshotMsOf(backup));
|
|
241
|
+
});
|
|
242
|
+
if (undateable) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
ms,
|
|
246
|
+
code: 'backup-failed',
|
|
247
|
+
states,
|
|
248
|
+
newestReadyAgeDays
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// State 3b — the listing is a PARTIAL result set. `ListBackupsResponse`
|
|
252
|
+
// carries `unreachable`: rather than failing the request when one location
|
|
253
|
+
// cannot be reached, the API returns the backups it could see and names the
|
|
254
|
+
// ones it could not. Positive evidence survives that (state 1 above already
|
|
255
|
+
// returned); negative evidence does not — "we saw no fresh READY backup" is
|
|
256
|
+
// not a finding when we did not see everything.
|
|
257
|
+
if (((_options_unreachable = options.unreachable) != null ? _options_unreachable : []).length > 0) {
|
|
258
|
+
return {
|
|
259
|
+
ok: true,
|
|
260
|
+
ms,
|
|
261
|
+
code: 'backups-partial',
|
|
262
|
+
states,
|
|
263
|
+
newestReadyAgeDays,
|
|
264
|
+
determinate: false
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// State 3c — a recent run exists, it simply is not usable AT THIS MOMENT:
|
|
268
|
+
// mid-creation on a Sunday, or inside one of the `NOT_AVAILABLE` windows
|
|
269
|
+
// measured above. Only reachable when NOTHING is READY — a stale READY
|
|
270
|
+
// backup falls through to `backup-stale` below, so this can never buy a
|
|
271
|
+
// hanging schedule more than the eight-day budget.
|
|
272
|
+
if (newestReadyAgeDays === null && newestAnyAgeDays !== null && newestAnyAgeDays <= MAX_BACKUP_AGE_DAYS) {
|
|
273
|
+
return {
|
|
274
|
+
ok: true,
|
|
275
|
+
ms,
|
|
276
|
+
code: 'backups-not-ready-yet',
|
|
277
|
+
states,
|
|
278
|
+
newestReadyAgeDays,
|
|
279
|
+
determinate: false
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
// State 2b — FAILED on freshness. Either nothing has ever been READY and
|
|
283
|
+
// nothing recent exists to be waiting on, or the newest READY backup is
|
|
284
|
+
// past the budget. Both mean the same operational thing: there is no recent
|
|
285
|
+
// restore point, which is the only fact this check exists to protect.
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
ms,
|
|
289
|
+
code: newestReadyAgeDays === null ? 'no-ready-backup' : 'backup-stale',
|
|
290
|
+
states,
|
|
291
|
+
newestReadyAgeDays
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Weekly cadence (Mondays, `scheduled-crons.yml`) + one day of slack — the
|
|
296
|
+
* same budget as `MAX_BACKUP_AGE_DAYS`, for the same reason: a schedule that
|
|
297
|
+
* stops producing is as broken as one that fails.
|
|
298
|
+
*/ export const MAX_EXPORT_AGE_DAYS = 8;
|
|
299
|
+
export function exportsHealth(markers, ms, now = Date.now()) {
|
|
300
|
+
// A failed listing is degraded, not ok — same rule as `signupsHealth` and
|
|
301
|
+
// `rateLimitsHealth`: an alarm that cannot see the thing it watches must
|
|
302
|
+
// say so rather than report calm.
|
|
303
|
+
if (markers === null) {
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
ms,
|
|
307
|
+
code: 'exports-unavailable',
|
|
308
|
+
exportCount: null,
|
|
309
|
+
newestExportAgeDays: null
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const newestMs = markers.map((marker)=>{
|
|
313
|
+
var _marker_timeCreated;
|
|
314
|
+
return Date.parse((_marker_timeCreated = marker.timeCreated) != null ? _marker_timeCreated : '');
|
|
315
|
+
}).filter((time)=>Number.isFinite(time)).reduce((newest, time)=>Math.max(newest != null ? newest : time, time), null);
|
|
316
|
+
const newestExportAgeDays = newestMs === null ? null : Math.round((now - newestMs) / 86400000 * 10) / 10;
|
|
317
|
+
// The cron exists, so "no exports" is a failure, not a fresh start — the
|
|
318
|
+
// first export ran 2026-08-17, before this check first deployed.
|
|
319
|
+
const code = newestExportAgeDays === null ? 'no-export' : newestExportAgeDays > MAX_EXPORT_AGE_DAYS ? 'export-stale' : undefined;
|
|
320
|
+
return _extends({
|
|
321
|
+
ok: code === undefined,
|
|
322
|
+
ms
|
|
323
|
+
}, code === undefined ? {} : {
|
|
324
|
+
code
|
|
325
|
+
}, {
|
|
326
|
+
exportCount: markers.length,
|
|
327
|
+
newestExportAgeDays
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** Trailing window the org-creation count covers. */ export const ORG_CREATION_WINDOW_MINUTES = 60;
|
|
331
|
+
/**
|
|
332
|
+
* Degraded above 10 creations/hour. Calibrated against two facts: production
|
|
333
|
+
* holds 4 orgs TOTAL (2026-08), so the organic baseline is ~zero per hour;
|
|
334
|
+
* and the AGL-1534 per-IP cap is 10/h, so a single maxed-out address can
|
|
335
|
+
* never trip this — 11+/h necessarily means multiple addresses, which is
|
|
336
|
+
* exactly the distributed signature the rate limiter cannot see. If a launch
|
|
337
|
+
* day legitimately beats this, the alert asks a human to glance at the orgs
|
|
338
|
+
* list and enjoy the view; that is the correct outcome, not a false positive.
|
|
339
|
+
*/ export const MAX_ORG_CREATIONS_PER_WINDOW = 10;
|
|
340
|
+
export function signupsHealth(recentOrgCreations, ms, threshold = MAX_ORG_CREATIONS_PER_WINDOW) {
|
|
341
|
+
// A failed count is degraded, not ok — an alarm that cannot see the thing
|
|
342
|
+
// it watches must say so rather than report calm.
|
|
343
|
+
const code = recentOrgCreations === null ? 'count-unavailable' : recentOrgCreations > threshold ? 'signup-wave' : undefined;
|
|
344
|
+
return _extends({
|
|
345
|
+
ok: code === undefined,
|
|
346
|
+
ms
|
|
347
|
+
}, code === undefined ? {} : {
|
|
348
|
+
code
|
|
349
|
+
}, {
|
|
350
|
+
recentOrgCreations,
|
|
351
|
+
windowMinutes: ORG_CREATION_WINDOW_MINUTES,
|
|
352
|
+
threshold
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Trailing window the refusal count covers. One hour, matching the limiter's
|
|
357
|
+
* own window (`org-create` is 3/h and 10/h) so the number is readable against
|
|
358
|
+
* the caps that produced it.
|
|
359
|
+
*/ export const SIGNUP_REFUSAL_WINDOW_MINUTES = 60;
|
|
360
|
+
/**
|
|
361
|
+
* Degraded above 50 refusals/hour.
|
|
362
|
+
*
|
|
363
|
+
* Not zero, unlike the limiter-degradation alarm: a refusal is the system
|
|
364
|
+
* working, and a real person who fumbles a workspace slug four times in an
|
|
365
|
+
* hour produces one. The number that means something is *sustained* refusal.
|
|
366
|
+
* Calibrated the same way AGL-1536's was: production holds single-digit orgs
|
|
367
|
+
* (2026-08), so organic refusals are ~zero per hour, and 50 is far above any
|
|
368
|
+
* plausible human while being reached in under a minute by a script. One
|
|
369
|
+
* determined address can generate this alone — that is intended, because "one
|
|
370
|
+
* address is hammering the signup door" is worth an email even though the
|
|
371
|
+
* limiter is containing it.
|
|
372
|
+
*/ export const MAX_SIGNUP_REFUSALS_PER_WINDOW = 50;
|
|
373
|
+
/**
|
|
374
|
+
* The marker field the blocking function counts blind decisions under
|
|
375
|
+
* (AGL-2583).
|
|
376
|
+
*
|
|
377
|
+
* `signupsCreationVerdict` answers `locked` when a completed read found the
|
|
378
|
+
* lever pulled, `held` when a failed read fell back on an earlier one that
|
|
379
|
+
* had, and — the case this names — admits the account when the read failed
|
|
380
|
+
* and nothing remembered said the lever was pulled. That last one refuses
|
|
381
|
+
* nobody, so it is not a `byReason` entry; it is the platform deciding the
|
|
382
|
+
* signups question without being able to consult it, which is the visible
|
|
383
|
+
* edge of a Firestore read outage on the account-creation path. AGL-2581 was
|
|
384
|
+
* three days of that same read failing, and its only trace was a
|
|
385
|
+
* `logger.warn` nothing alerts on.
|
|
386
|
+
*/ export const SIGNUP_LOCK_UNREADABLE_FIELD = 'unreadable';
|
|
387
|
+
/**
|
|
388
|
+
* Blind signups-lock decisions tolerated per window: NONE.
|
|
389
|
+
*
|
|
390
|
+
* Deliberately not calibrated like `MAX_SIGNUP_REFUSALS_PER_WINDOW` above,
|
|
391
|
+
* which is a volume threshold over refusals that mean the system is working.
|
|
392
|
+
* A blind decision is never routine — a single one says the signups lock was
|
|
393
|
+
* unreadable at least once, and the lock is unenforceable for as long as that
|
|
394
|
+
* lasts. False positives here cost a glance at one endpoint.
|
|
395
|
+
*/ export const MAX_UNREADABLE_SIGNUP_DECISIONS_PER_WINDOW = 0;
|
|
396
|
+
export function signupRefusalsHealth(markers, ms, now = Date.now(), threshold = MAX_SIGNUP_REFUSALS_PER_WINDOW, windowMinutes = SIGNUP_REFUSAL_WINDOW_MINUTES, unreadableThreshold = MAX_UNREADABLE_SIGNUP_DECISIONS_PER_WINDOW) {
|
|
397
|
+
// A failed query is degraded, not ok — the same rule `signupsHealth` and
|
|
398
|
+
// `rateLimitsHealth` follow. An alarm that cannot see the thing it watches
|
|
399
|
+
// must not report calm.
|
|
400
|
+
if (markers === null) {
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
ms,
|
|
404
|
+
code: 'refusals-unavailable',
|
|
405
|
+
refusedSignups: null,
|
|
406
|
+
refusedByReason: null,
|
|
407
|
+
lockUnreadable: null,
|
|
408
|
+
minutesSinceLast: null,
|
|
409
|
+
windowMinutes,
|
|
410
|
+
threshold
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
let refusedSignups = 0;
|
|
414
|
+
let lockUnreadable = 0;
|
|
415
|
+
let newestAtMs = null;
|
|
416
|
+
const refusedByReason = {};
|
|
417
|
+
for (const marker of markers){
|
|
418
|
+
var _marker_refusals, _marker_byReason;
|
|
419
|
+
refusedSignups += (_marker_refusals = marker.refusals) != null ? _marker_refusals : 0;
|
|
420
|
+
// `Number(...) || 0` for the reason the split below gives: a corrupt
|
|
421
|
+
// Firestore field must not turn this into NaN, which compares false
|
|
422
|
+
// against every threshold and would report calm forever.
|
|
423
|
+
lockUnreadable += Number(marker.unreadable) || 0;
|
|
424
|
+
for (const [reason, count] of Object.entries((_marker_byReason = marker.byReason) != null ? _marker_byReason : {})){
|
|
425
|
+
var _refusedByReason_reason;
|
|
426
|
+
// `Number(...) || 0` rather than a bare add: these come out of Firestore
|
|
427
|
+
// and a corrupt field must not turn the total into NaN, which compares
|
|
428
|
+
// false against the threshold and would report calm forever.
|
|
429
|
+
refusedByReason[reason] = ((_refusedByReason_reason = refusedByReason[reason]) != null ? _refusedByReason_reason : 0) + (Number(count) || 0);
|
|
430
|
+
}
|
|
431
|
+
const at = marker.refusedAtMs;
|
|
432
|
+
if (typeof at === 'number' && (newestAtMs === null || at > newestAtMs)) {
|
|
433
|
+
newestAtMs = at;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const over = refusedSignups > threshold;
|
|
437
|
+
// The blind count is graded FIRST and reported over the volume code. A wave
|
|
438
|
+
// and an unreadable lock call for opposite responses — leave the limiter
|
|
439
|
+
// alone and go read the refusal reasons, versus go find out why Firestore
|
|
440
|
+
// is unreadable from the account-creation path — so the code has to name
|
|
441
|
+
// the one that is actually happening.
|
|
442
|
+
const blind = lockUnreadable > unreadableThreshold;
|
|
443
|
+
const code = blind ? 'signups-lock-unreadable' : over ? 'signup-refusal-wave' : undefined;
|
|
444
|
+
return _extends({
|
|
445
|
+
ok: code === undefined,
|
|
446
|
+
ms
|
|
447
|
+
}, code === undefined ? {} : {
|
|
448
|
+
code
|
|
449
|
+
}, {
|
|
450
|
+
refusedSignups,
|
|
451
|
+
refusedByReason,
|
|
452
|
+
lockUnreadable,
|
|
453
|
+
minutesSinceLast: newestAtMs === null ? null : Math.max(0, Math.floor((now - newestAtMs) / 60000)),
|
|
454
|
+
windowMinutes,
|
|
455
|
+
threshold
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Trailing window the drought verdict covers. One hour, matching the sibling
|
|
460
|
+
* checks so the three numbers on this endpoint are all read against the same
|
|
461
|
+
* clock — and long enough that a single slow afternoon does not page anyone.
|
|
462
|
+
*/ export const SIGNUP_DROUGHT_WINDOW_MINUTES = 60;
|
|
463
|
+
/**
|
|
464
|
+
* Attempts in the window below which zero accounts means nothing.
|
|
465
|
+
*
|
|
466
|
+
* Three, against attempts rather than the five it once meant against serves
|
|
467
|
+
* (AGL-2714). The unit changed, so the number had to be re-argued rather than
|
|
468
|
+
* carried over.
|
|
469
|
+
*
|
|
470
|
+
* Not one: a browser can lose a request, and a person can start the form and
|
|
471
|
+
* abandon it between authenticating and the org write. Not ten either — at a
|
|
472
|
+
* handful of signups a week, waiting for ten attempts to fail would mean
|
|
473
|
+
* waiting days to notice a door that stopped opening, which is the failure
|
|
474
|
+
* this check exists to catch inside an hour.
|
|
475
|
+
*
|
|
476
|
+
* Three distinct requests to create an org, none of which produced one and
|
|
477
|
+
* none of which was refused, is a door that does not open. That reading has
|
|
478
|
+
* no quiet-hour interpretation: nobody attempts three times by browsing.
|
|
479
|
+
*/ export const MIN_SIGNUP_TRAFFIC_FOR_DROUGHT = 3;
|
|
480
|
+
export function signupDroughtHealth(markers, attemptMarkers, orgCreations, ms, minimumTraffic = MIN_SIGNUP_TRAFFIC_FOR_DROUGHT, windowMinutes = SIGNUP_DROUGHT_WINDOW_MINUTES) {
|
|
481
|
+
// Serves are context, not the verdict, so they are summed wherever they are
|
|
482
|
+
// readable and reported as null when they are not — a missing context line
|
|
483
|
+
// must not decide anything on its own.
|
|
484
|
+
let signupPagesServed = null;
|
|
485
|
+
if (markers !== null) {
|
|
486
|
+
signupPagesServed = 0;
|
|
487
|
+
for (const marker of markers){
|
|
488
|
+
// `Number(...) || 0` for the reason the sibling gives: a corrupt
|
|
489
|
+
// Firestore field must not turn the total into NaN, which compares
|
|
490
|
+
// false against every threshold and would report calm forever.
|
|
491
|
+
signupPagesServed += Number(marker.serves) || 0;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// A failed ATTEMPT query is degraded, not ok — the same rule the siblings
|
|
495
|
+
// follow. Without the denominator this check has no opinion at all, and a
|
|
496
|
+
// check with no opinion must not spend it saying everything is fine.
|
|
497
|
+
if (attemptMarkers === null) {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
ms,
|
|
501
|
+
code: 'traffic-unavailable',
|
|
502
|
+
signupPagesServed,
|
|
503
|
+
signupAttempts: null,
|
|
504
|
+
orgCreations,
|
|
505
|
+
windowMinutes,
|
|
506
|
+
minimumTraffic
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
let signupAttempts = 0;
|
|
510
|
+
for (const marker of attemptMarkers){
|
|
511
|
+
signupAttempts += Number(marker.attempts) || 0;
|
|
512
|
+
}
|
|
513
|
+
if (orgCreations === null) {
|
|
514
|
+
return {
|
|
515
|
+
ok: false,
|
|
516
|
+
ms,
|
|
517
|
+
code: 'count-unavailable',
|
|
518
|
+
signupPagesServed,
|
|
519
|
+
signupAttempts,
|
|
520
|
+
orgCreations: null,
|
|
521
|
+
windowMinutes,
|
|
522
|
+
minimumTraffic
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
// The whole verdict: people ASKED for an org and NOT ONE of them got it.
|
|
526
|
+
const drought = signupAttempts >= minimumTraffic && orgCreations === 0;
|
|
527
|
+
return _extends({
|
|
528
|
+
ok: !drought,
|
|
529
|
+
ms
|
|
530
|
+
}, drought ? {
|
|
531
|
+
code: 'signup-drought'
|
|
532
|
+
} : {}, {
|
|
533
|
+
signupPagesServed,
|
|
534
|
+
signupAttempts,
|
|
535
|
+
orgCreations,
|
|
536
|
+
windowMinutes,
|
|
537
|
+
minimumTraffic
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Trailing window the degradation counts cover.
|
|
542
|
+
*
|
|
543
|
+
* Bounded from below by the alert path, not by taste. The probe memoizes for
|
|
544
|
+
* `HEALTH_PROBE_TTL_MS` (2 minutes), the slowest check on this door runs
|
|
545
|
+
* every 15, and the alert policy wants a minute of sustained failure — so a
|
|
546
|
+
* window shorter than ~18 minutes can go red and green again before that
|
|
547
|
+
* check tells anyone, which is a check that reports nothing while looking
|
|
548
|
+
* like it works. 30 leaves margin and still clears itself inside half an hour.
|
|
549
|
+
*/ export const RATE_LIMIT_DEGRADED_WINDOW_MINUTES = 30;
|
|
550
|
+
/**
|
|
551
|
+
* Any fallback at all is worth an email. Unlike the signup alarm there is no
|
|
552
|
+
* organic baseline to clear: a healthy deployment produces exactly zero of
|
|
553
|
+
* these, and one is already the statement "for some requests, the global cap
|
|
554
|
+
* was not the cap". Firestore retries transactions internally, so a failure
|
|
555
|
+
* that reaches this code is not a single unlucky contention.
|
|
556
|
+
*/ export const MAX_DEGRADED_CALLS_PER_WINDOW = 0;
|
|
557
|
+
export function rateLimitsHealth(markers, ms, now = Date.now(), threshold = MAX_DEGRADED_CALLS_PER_WINDOW, windowMinutes = RATE_LIMIT_DEGRADED_WINDOW_MINUTES) {
|
|
558
|
+
// A failed query is degraded, not ok — same rule as `signupsHealth`. An
|
|
559
|
+
// alarm that cannot see the thing it watches must not report calm, and this
|
|
560
|
+
// one reads the collection that just failed for the limiter.
|
|
561
|
+
if (markers === null) {
|
|
562
|
+
return {
|
|
563
|
+
ok: false,
|
|
564
|
+
ms,
|
|
565
|
+
code: 'markers-unavailable',
|
|
566
|
+
degradedCalls: null,
|
|
567
|
+
degradedEpisodes: null,
|
|
568
|
+
minutesSinceLast: null,
|
|
569
|
+
windowMinutes,
|
|
570
|
+
threshold
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
const cutoff = now - windowMinutes * 60000;
|
|
574
|
+
// The window is re-applied here rather than trusted from the query, so the
|
|
575
|
+
// rule lives in one spec-covered place and a marker whose `lastAtMs` is
|
|
576
|
+
// missing cannot silently count as recent.
|
|
577
|
+
const recent = markers.filter((marker)=>typeof marker.lastAtMs === 'number' && marker.lastAtMs >= cutoff);
|
|
578
|
+
const degradedCalls = recent.reduce((total, marker)=>total + (Number(marker.calls) || 0), 0);
|
|
579
|
+
const degradedEpisodes = recent.reduce((total, marker)=>total + (Number(marker.episodes) || 0), 0);
|
|
580
|
+
const lastAtMs = recent.reduce((latest, marker)=>Math.max(latest != null ? latest : -Infinity, marker.lastAtMs), null);
|
|
581
|
+
return _extends({
|
|
582
|
+
ok: degradedCalls <= threshold,
|
|
583
|
+
ms
|
|
584
|
+
}, degradedCalls <= threshold ? {} : {
|
|
585
|
+
code: 'rate-limiter-degraded'
|
|
586
|
+
}, {
|
|
587
|
+
degradedCalls,
|
|
588
|
+
degradedEpisodes,
|
|
589
|
+
minutesSinceLast: lastAtMs === null ? null : Math.round((now - lastAtMs) / 60000 * 10) / 10,
|
|
590
|
+
windowMinutes,
|
|
591
|
+
threshold
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Trailing window the error count covers.
|
|
596
|
+
*
|
|
597
|
+
* Bounded from below by the alert path, not by taste — the same arithmetic as
|
|
598
|
+
* `RATE_LIMIT_DEGRADED_WINDOW_MINUTES`: the probe memoizes for 2 minutes, the
|
|
599
|
+
* slowest external check runs every 15, and an alert policy wants a minute of
|
|
600
|
+
* sustained failure, so a window under ~18 minutes can go red and green again
|
|
601
|
+
* before that check tells anyone. 30 leaves margin and still clears itself
|
|
602
|
+
* inside half an hour of the last error.
|
|
603
|
+
*/ export const SERVER_ERROR_WINDOW_MINUTES = 30;
|
|
604
|
+
/**
|
|
605
|
+
* Errors tolerated in the window.
|
|
606
|
+
*
|
|
607
|
+
* NOT zero, unlike the rate-limiter and billing alarms, and the difference is
|
|
608
|
+
* real rather than a matter of nerve. Those two watch controls that a healthy
|
|
609
|
+
* deployment exercises exactly never. This one watches a count that a single
|
|
610
|
+
* cold-start Firestore deadline, or one visitor's malformed request reaching a
|
|
611
|
+
* route that throws instead of 400ing, can make non-zero on a perfectly good
|
|
612
|
+
* day — and an alarm that pages on the first of those gets muted before the
|
|
613
|
+
* one that matters arrives.
|
|
614
|
+
*
|
|
615
|
+
* Five over thirty minutes is chosen against the only two facts available.
|
|
616
|
+
* Production serves single-digit orgs (2026-08), so the organic rate of
|
|
617
|
+
* UNCAUGHT server errors is ~zero — the codebase answers its expected failures
|
|
618
|
+
* with 4xx, and reaching this hook means nothing handled it. And the shape
|
|
619
|
+
* being caught is "a route is failing for real users", which does not produce
|
|
620
|
+
* six errors an hour; it produces six in a minute. There is deliberately no
|
|
621
|
+
* pretence of calibration: the beta window exists to produce a baseline, and
|
|
622
|
+
* `SERVER_ERROR_ALARM_MAX_ERRORS` moves this without a deploy when it does.
|
|
623
|
+
*/ export const MAX_SERVER_ERRORS_PER_WINDOW = 5;
|
|
624
|
+
export function serverErrorsHealth(markers, ms, now = Date.now(), threshold = MAX_SERVER_ERRORS_PER_WINDOW, windowMinutes = SERVER_ERROR_WINDOW_MINUTES) {
|
|
625
|
+
// ⚠️ THE MEASURED ZERO. A failed query is degraded, never calm, and on this
|
|
626
|
+
// check that is not a convention borrowed from its siblings — it is the
|
|
627
|
+
// whole reason the check is trustworthy. "We could not count the errors"
|
|
628
|
+
// and "there were no errors" are the same JSON if this branch is missing,
|
|
629
|
+
// and the first is most likely precisely when the second is false.
|
|
630
|
+
if (markers === null) {
|
|
631
|
+
return {
|
|
632
|
+
ok: false,
|
|
633
|
+
ms,
|
|
634
|
+
code: 'errors-unavailable',
|
|
635
|
+
serverErrors: null,
|
|
636
|
+
byService: null,
|
|
637
|
+
minutesSinceLast: null,
|
|
638
|
+
windowMinutes,
|
|
639
|
+
threshold
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
const cutoff = now - windowMinutes * 60000;
|
|
643
|
+
// The window is re-applied here rather than trusted from the query, so the
|
|
644
|
+
// rule lives in one spec-covered place and a marker whose `erroredAtMs` is
|
|
645
|
+
// missing cannot silently count as recent. `strictNullChecks` is off
|
|
646
|
+
// repo-wide, so an absent field would otherwise compare as 0 and fall
|
|
647
|
+
// outside every window forever — a marker that can never be counted.
|
|
648
|
+
const recent = markers.filter((marker)=>typeof marker.erroredAtMs === 'number' && marker.erroredAtMs >= cutoff);
|
|
649
|
+
const byService = {};
|
|
650
|
+
let serverErrors = 0;
|
|
651
|
+
let lastAtMs = null;
|
|
652
|
+
for (const marker of recent){
|
|
653
|
+
var _marker_byService;
|
|
654
|
+
// `Number(...) || 0` rather than a bare add: these come out of Firestore
|
|
655
|
+
// and a corrupt field must not turn the total into NaN, which compares
|
|
656
|
+
// false against the threshold and would report calm forever.
|
|
657
|
+
serverErrors += Number(marker.errors) || 0;
|
|
658
|
+
for (const [service, count] of Object.entries((_marker_byService = marker.byService) != null ? _marker_byService : {})){
|
|
659
|
+
var _byService_service;
|
|
660
|
+
byService[service] = ((_byService_service = byService[service]) != null ? _byService_service : 0) + (Number(count) || 0);
|
|
661
|
+
}
|
|
662
|
+
const at = marker.erroredAtMs;
|
|
663
|
+
if (lastAtMs === null || at > lastAtMs) lastAtMs = at;
|
|
664
|
+
}
|
|
665
|
+
const over = serverErrors > threshold;
|
|
666
|
+
return _extends({
|
|
667
|
+
ok: !over,
|
|
668
|
+
ms
|
|
669
|
+
}, over ? {
|
|
670
|
+
code: 'server-error-spike'
|
|
671
|
+
} : {}, {
|
|
672
|
+
serverErrors,
|
|
673
|
+
byService,
|
|
674
|
+
minutesSinceLast: lastAtMs === null ? null : Math.max(0, Math.round((now - lastAtMs) / 60000 * 10) / 10),
|
|
675
|
+
windowMinutes,
|
|
676
|
+
threshold
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* How long a health door may reuse its verdict (AGL-2947).
|
|
681
|
+
*
|
|
682
|
+
* Cut from one budget: an outage must reach a person within ten minutes. A
|
|
683
|
+
* green verdict computed just before the onset keeps being served for this
|
|
684
|
+
* long, and only then can a prober see red. The fastest prober on most doors
|
|
685
|
+
* is UptimeRobot, every five minutes, which then confirms before it notifies.
|
|
686
|
+
* So the worst case is this + 5 + that confirmation: at two minutes the budget
|
|
687
|
+
* holds for any confirmation delay up to three. The five minutes every door
|
|
688
|
+
* used before spent the whole ten minutes before a notification could leave.
|
|
689
|
+
*
|
|
690
|
+
* It still bounds what a public door can be made to cost: at most one
|
|
691
|
+
* computation per instance per two minutes, and {@link memoizeWithTtl} shares
|
|
692
|
+
* a computation among the callers that arrive while it runs.
|
|
693
|
+
*/ export const HEALTH_PROBE_TTL_MS = 2 * 60000;
|
|
694
|
+
/**
|
|
695
|
+
* Reuse a probe result for `ttlMs`, per instance.
|
|
696
|
+
*
|
|
697
|
+
* The endpoint is public and unauthenticated, so an unthrottled dependency
|
|
698
|
+
* check is a free amplifier: anyone could turn a loop into database reads on
|
|
699
|
+
* our account. This bounds the cost to one probe per instance per interval
|
|
700
|
+
* however hard it is hit.
|
|
701
|
+
*
|
|
702
|
+
* Callers that arrive while a probe is running share it rather than starting
|
|
703
|
+
* their own. The uptime check's regions, UptimeRobot and a HEAD twin routinely
|
|
704
|
+
* land within the same second of an expired entry, and each used to pay for a
|
|
705
|
+
* full probe.
|
|
706
|
+
*
|
|
707
|
+
* FAILURES ARE CACHED TOO, deliberately. A dependency that is down will be
|
|
708
|
+
* down for the next caller as well, and re-probing per request turns an
|
|
709
|
+
* outage into a stampede against the thing that is already failing. A probe
|
|
710
|
+
* that THROWS is not cached: its callers all see the throw, and the next
|
|
711
|
+
* caller probes again.
|
|
712
|
+
*/ export function memoizeWithTtl(ttlMs, probe, now = Date.now) {
|
|
713
|
+
let cached = null;
|
|
714
|
+
let inFlight = null;
|
|
715
|
+
return async ()=>{
|
|
716
|
+
const at = now();
|
|
717
|
+
const elapsed = cached ? at - cached.at : Infinity;
|
|
718
|
+
// `elapsed >= 0` is not paranoia. A plain `elapsed < ttlMs` treats a
|
|
719
|
+
// BACKWARDS clock as permanently fresh — the entry never expires and the
|
|
720
|
+
// endpoint reports a stale verdict forever, which on a health check means
|
|
721
|
+
// reporting healthy through an outage. Serverless instances outlive more
|
|
722
|
+
// clock weirdness than a local run suggests.
|
|
723
|
+
if (cached && elapsed >= 0 && elapsed < ttlMs) return cached.value;
|
|
724
|
+
if (inFlight) return inFlight;
|
|
725
|
+
const flight = (async ()=>{
|
|
726
|
+
const value = await probe();
|
|
727
|
+
cached = {
|
|
728
|
+
at,
|
|
729
|
+
value
|
|
730
|
+
};
|
|
731
|
+
return value;
|
|
732
|
+
})();
|
|
733
|
+
inFlight = flight;
|
|
734
|
+
// Cleared once settled, and only if it is still this flight. Clearing
|
|
735
|
+
// inside the flight would run BEFORE the assignment above when `probe`
|
|
736
|
+
// throws synchronously, and leave a rejected promise in the slot forever.
|
|
737
|
+
const clear = ()=>{
|
|
738
|
+
if (inFlight === flight) inFlight = null;
|
|
739
|
+
};
|
|
740
|
+
flight.then(clear, clear);
|
|
741
|
+
return flight;
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Codes that mean the heartbeat got no ANSWER, as opposed to a refusal
|
|
746
|
+
* (AGL-2713).
|
|
747
|
+
*
|
|
748
|
+
* The distinction is the whole of the tolerance below, so it is named once
|
|
749
|
+
* and asserted directly rather than being spelled inline at the branch.
|
|
750
|
+
*
|
|
751
|
+
* - `credential-unavailable` — the admin app is configured and its credential
|
|
752
|
+
* simply did not mint a token on this attempt. On a cold lambda that is the
|
|
753
|
+
* first outbound TLS of the instance's life, and it is the measured shape of
|
|
754
|
+
* both 2026-09-09 blips.
|
|
755
|
+
* - `transport-*` — the Logging write never came back inside its 4s deadline,
|
|
756
|
+
* or the fetch layer failed. Same class: nothing was decided.
|
|
757
|
+
*
|
|
758
|
+
* Everything else is a DECISION and must never be tolerated. `http-401` and
|
|
759
|
+
* `http-403` are a lost `logging.logEntries.create` grant, `http-429` is
|
|
760
|
+
* quota, and `no-credential` is a deployment whose `FIREBASE_*` env is absent
|
|
761
|
+
* — every one of them permanent until a person fixes it, and every one of
|
|
762
|
+
* them a state in which browser errors are being collected by nothing. So is
|
|
763
|
+
* `heartbeat-unavailable`, which means a writer documented never to throw
|
|
764
|
+
* threw.
|
|
765
|
+
*/ export function isTransientBeaconCode(code) {
|
|
766
|
+
return code === 'credential-unavailable' || (code != null ? code : '').startsWith('transport-');
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* ## Tolerating ONE miss without becoming a door that cannot red (AGL-2713)
|
|
770
|
+
*
|
|
771
|
+
* `/api/health/error-beacon` on the console answered `503 no-credential` twice
|
|
772
|
+
* on 2026-09-09 — 15:12 and 17:22 UTC, each shortly after new capacity from
|
|
773
|
+
* the 15:08 deploy, each self-clearing within minutes, with the beacon code
|
|
774
|
+
* path changed by one comment line since the previous release. Between them
|
|
775
|
+
* they took the 15-minute uptime workflow red and opened four GCP alert
|
|
776
|
+
* events, on a monitor whose entire value is that somebody believes it.
|
|
777
|
+
*
|
|
778
|
+
* The tolerance is deliberately NOT "forgive a failure". It is three
|
|
779
|
+
* conditions that must all hold, and each one is what keeps the door able to
|
|
780
|
+
* go red:
|
|
781
|
+
*
|
|
782
|
+
* 1. the code says nothing was decided ({@link isTransientBeaconCode});
|
|
783
|
+
* 2. a heartbeat is PROVEN to have landed within `graceMs` — proof being a
|
|
784
|
+
* durable marker a successful write left behind, so a deployment whose
|
|
785
|
+
* credential never worked has nothing to forgive with and reds on its
|
|
786
|
+
* first probe;
|
|
787
|
+
* 3. that proof is not from the future, because a clock that ran backwards
|
|
788
|
+
* must not mint an unbounded grace.
|
|
789
|
+
*
|
|
790
|
+
* Absence of proof reds. A marker store that cannot be read reds. That is the
|
|
791
|
+
* same rule the `*-unavailable` arms of every sibling grader apply, pointed at
|
|
792
|
+
* the evidence for calm rather than at the evidence for alarm.
|
|
793
|
+
*/ export function beaconHealth(write, logId, service, ms, tolerance = {}) {
|
|
794
|
+
var _write_code;
|
|
795
|
+
const { attempts = 1, landedAtMs = null, graceMs = 0, now = Date.now() } = tolerance;
|
|
796
|
+
const sinceLanded = landedAtMs === null ? null : now - landedAtMs;
|
|
797
|
+
const base = {
|
|
798
|
+
ms,
|
|
799
|
+
logId,
|
|
800
|
+
service,
|
|
801
|
+
attempts,
|
|
802
|
+
minutesSinceHeartbeat: sinceLanded === null ? null : Math.max(0, Math.round(sinceLanded / 60000 * 10) / 10),
|
|
803
|
+
graceMinutes: Math.round(graceMs / 60000)
|
|
804
|
+
};
|
|
805
|
+
// A null result is degraded by contract, the same rule `signupsHealth` and
|
|
806
|
+
// `rateLimitsHealth` follow: an alarm that cannot see the thing it watches
|
|
807
|
+
// must not report calm. Here it is stronger than a convention — "we could
|
|
808
|
+
// not determine whether the beacon works" IS the AGL-1923 condition.
|
|
809
|
+
if (write === null) {
|
|
810
|
+
return _extends({}, base, {
|
|
811
|
+
ok: false,
|
|
812
|
+
code: 'heartbeat-unavailable'
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
if (write.ok) return _extends({}, base, {
|
|
816
|
+
ok: true
|
|
817
|
+
});
|
|
818
|
+
const code = (_write_code = write.code) != null ? _write_code : 'heartbeat-failed';
|
|
819
|
+
if (isTransientBeaconCode(code) && sinceLanded !== null && sinceLanded >= 0 && sinceLanded <= graceMs) {
|
|
820
|
+
// Green, and saying so out loud. A tolerance nobody can see on the board
|
|
821
|
+
// is indistinguishable from a door that was widened until it stopped
|
|
822
|
+
// reporting, which is the failure this whole change had to avoid being.
|
|
823
|
+
return _extends({}, base, {
|
|
824
|
+
ok: true,
|
|
825
|
+
code: 'heartbeat-missed'
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
return _extends({}, base, {
|
|
829
|
+
ok: false,
|
|
830
|
+
code
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
export function renderHealth(outcome, host, ms) {
|
|
834
|
+
const base = {
|
|
835
|
+
ms,
|
|
836
|
+
host
|
|
837
|
+
};
|
|
838
|
+
switch(outcome.kind){
|
|
839
|
+
case 'rendered':
|
|
840
|
+
// Both halves are load-bearing. A host that resolved but composed
|
|
841
|
+
// nothing is a blank page served with a 200 — the exact outage a
|
|
842
|
+
// reachability ping calls healthy.
|
|
843
|
+
if (!outcome.hostResolved) {
|
|
844
|
+
return _extends({}, base, {
|
|
845
|
+
ok: false,
|
|
846
|
+
code: 'host-unresolved',
|
|
847
|
+
nodeCount: outcome.nodeCount
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
if (outcome.nodeCount <= 0) {
|
|
851
|
+
return _extends({}, base, {
|
|
852
|
+
ok: false,
|
|
853
|
+
code: 'rendered-empty',
|
|
854
|
+
nodeCount: 0
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
return _extends({}, base, {
|
|
858
|
+
ok: true,
|
|
859
|
+
nodeCount: outcome.nodeCount
|
|
860
|
+
});
|
|
861
|
+
case 'not-found':
|
|
862
|
+
return _extends({}, base, {
|
|
863
|
+
ok: false,
|
|
864
|
+
code: 'not-found',
|
|
865
|
+
nodeCount: 0
|
|
866
|
+
});
|
|
867
|
+
case 'redirect':
|
|
868
|
+
return _extends({}, base, {
|
|
869
|
+
ok: false,
|
|
870
|
+
code: 'redirected',
|
|
871
|
+
nodeCount: 0
|
|
872
|
+
});
|
|
873
|
+
case 'not-configured':
|
|
874
|
+
return _extends({}, base, {
|
|
875
|
+
ok: false,
|
|
876
|
+
code: 'not-configured',
|
|
877
|
+
nodeCount: 0
|
|
878
|
+
});
|
|
879
|
+
case 'unavailable':
|
|
880
|
+
default:
|
|
881
|
+
// Same rule as `beaconHealth`: "we could not determine whether the page
|
|
882
|
+
// renders" is a failure, never calm.
|
|
883
|
+
return _extends({}, base, {
|
|
884
|
+
ok: false,
|
|
885
|
+
code: 'render-unavailable',
|
|
886
|
+
nodeCount: 0
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Trailing window the delivery counts cover.
|
|
892
|
+
*
|
|
893
|
+
* Bounded from below by the alert path, not by taste, exactly like
|
|
894
|
+
* `RATE_LIMIT_DEGRADED_WINDOW_MINUTES`: the probe memoizes for 5 minutes, the
|
|
895
|
+
* uptime check runs every 5, and the policy wants ~10 minutes of sustained
|
|
896
|
+
* failure before it emails — so a window under ~20 minutes can go red and
|
|
897
|
+
* green again before anyone is told. Bounded from ABOVE by Stripe's retry
|
|
898
|
+
* schedule: an event that fails and then succeeds on retry stops counting, so
|
|
899
|
+
* a window much longer than an hour would hold transient blips red past their
|
|
900
|
+
* own recovery.
|
|
901
|
+
*/ export const WEBHOOK_FAILURE_WINDOW_MINUTES = 60;
|
|
902
|
+
/**
|
|
903
|
+
* Failed deliveries tolerated in the window.
|
|
904
|
+
*
|
|
905
|
+
* Zero, and unlike the signup alarm there is no organic baseline to clear: a
|
|
906
|
+
* healthy account produces exactly zero, and one is already the statement
|
|
907
|
+
* "Stripe tried to tell us something about money and could not". Stripe
|
|
908
|
+
* retries on its own, so a failure visible here is not one unlucky packet.
|
|
909
|
+
*/ export const MAX_FAILED_DELIVERIES_PER_WINDOW = 0;
|
|
910
|
+
/**
|
|
911
|
+
* Inert deliveries tolerated in the window (AGL-1954).
|
|
912
|
+
*
|
|
913
|
+
* Zero — and unlike `processed`, the count this check reports but refuses to
|
|
914
|
+
* gate on because no defensible floor exists before the beta produces a
|
|
915
|
+
* baseline, this one's floor follows from the definition. A delivery is
|
|
916
|
+
* counted inert only when a required event type produced neither a committed
|
|
917
|
+
* effect nor a NAMED deliberate skip (`classifyWebhookDelivery`). No
|
|
918
|
+
* legitimate traffic pattern generates one: a tenant shopper's subscription,
|
|
919
|
+
* a marketplace refund, a `won` dispute nobody claimed all name their reason
|
|
920
|
+
* and classify as `ignored`. So any at all is the statement "Stripe told us
|
|
921
|
+
* about money and nothing here moved" — the same "any at all" rule the
|
|
922
|
+
* rate-limiter degradation markers use (AGL-1679).
|
|
923
|
+
*/ export const MAX_INERT_DELIVERIES_PER_WINDOW = 0;
|
|
924
|
+
/**
|
|
925
|
+
* Retried deliveries tolerated in the window (AGL-2039).
|
|
926
|
+
*
|
|
927
|
+
* Zero, and the reasoning is `MAX_FAILED_DELIVERIES_PER_WINDOW`'s: a delivery
|
|
928
|
+
* counted here is one whose FIRST attempt did not get through, which is a
|
|
929
|
+
* failed attempt that happened, not a statistical wobble. The healthy band
|
|
930
|
+
* measured against the live account is 1.0–3.7 seconds against a 120-second
|
|
931
|
+
* bar, so nothing organic sits anywhere near it.
|
|
932
|
+
*
|
|
933
|
+
* Cheap to be wrong in the safe direction, because this cannot latch: the
|
|
934
|
+
* window is trailing and the marker is on the event's own claim, so a single
|
|
935
|
+
* blip reds for at most one window and then clears itself — an event, not a
|
|
936
|
+
* condition (the AGL-1843 rule). It is a parameter rather than a literal so
|
|
937
|
+
* that if beta traffic turns out to produce organic retries, the knob is one
|
|
938
|
+
* argument and not a rewrite.
|
|
939
|
+
*/ export const MAX_RETRIED_DELIVERIES_PER_WINDOW = 0;
|
|
940
|
+
export function billingWebhookHealth(facts, ms, windowMinutes = WEBHOOK_FAILURE_WINDOW_MINUTES, threshold = MAX_FAILED_DELIVERIES_PER_WINDOW, inertThreshold = MAX_INERT_DELIVERIES_PER_WINDOW, retriedThreshold = MAX_RETRIED_DELIVERIES_PER_WINDOW) {
|
|
941
|
+
// A null census is degraded, the same rule `signupsHealth` and
|
|
942
|
+
// `rateLimitsHealth` follow: an alarm that cannot see the thing it watches
|
|
943
|
+
// must not report calm. Here it is also the AGL-1906 lesson — `unknown` was
|
|
944
|
+
// read as `healthy` on 2026-08-14 and the endpoint was 400ing every
|
|
945
|
+
// delivery at the time.
|
|
946
|
+
if (facts === null) {
|
|
947
|
+
return {
|
|
948
|
+
ok: false,
|
|
949
|
+
ms,
|
|
950
|
+
code: 'stripe-unavailable',
|
|
951
|
+
endpointStatus: null,
|
|
952
|
+
undelivered: null,
|
|
953
|
+
emitted: null,
|
|
954
|
+
processed: null,
|
|
955
|
+
inert: null,
|
|
956
|
+
retried: null,
|
|
957
|
+
unsubscribedEvents: null,
|
|
958
|
+
connectEndpoint: null,
|
|
959
|
+
unsubscribedConnectEvents: null,
|
|
960
|
+
windowMinutes
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
const base = {
|
|
964
|
+
ms,
|
|
965
|
+
endpointStatus: facts.endpointStatus,
|
|
966
|
+
undelivered: facts.undelivered,
|
|
967
|
+
emitted: facts.emitted,
|
|
968
|
+
processed: facts.processed,
|
|
969
|
+
inert: facts.inert,
|
|
970
|
+
retried: facts.retried,
|
|
971
|
+
unsubscribedEvents: facts.unsubscribedEvents,
|
|
972
|
+
connectEndpoint: facts.connectEndpoint,
|
|
973
|
+
unsubscribedConnectEvents: facts.unsubscribedConnectEvents,
|
|
974
|
+
windowMinutes
|
|
975
|
+
};
|
|
976
|
+
// Order matters: a missing or disabled destination explains any delivery
|
|
977
|
+
// count that follows, and is the more actionable statement. An endpoint
|
|
978
|
+
// deleted or switched off in the dashboard is TOTAL silent failure — Stripe
|
|
979
|
+
// stops attempting, so `undelivered` would read zero and a delivery-only
|
|
980
|
+
// rule would call it healthy.
|
|
981
|
+
if (facts.endpointStatus === 'missing') {
|
|
982
|
+
return _extends({}, base, {
|
|
983
|
+
ok: false,
|
|
984
|
+
code: 'endpoint-missing'
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
if (facts.endpointStatus === 'disabled') {
|
|
988
|
+
return _extends({}, base, {
|
|
989
|
+
ok: false,
|
|
990
|
+
code: 'endpoint-disabled'
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
// A required event the destination does not carry (AGL-1948 / AGL-1798),
|
|
994
|
+
// ABOVE the delivery counts because it explains a silence they cannot see.
|
|
995
|
+
// An unsubscribed event produces no delivery to fail, no request to reject
|
|
996
|
+
// and no inert handler either — Stripe simply stops sending, and every
|
|
997
|
+
// count below reads a perfectly healthy zero for as long as it lasts. It
|
|
998
|
+
// also outranks `deliveries-failing` on actionability: this one names the
|
|
999
|
+
// exact event to re-add, and `npm run setup:stripe` fixes it in a minute.
|
|
1000
|
+
//
|
|
1001
|
+
// `null` — the endpoint did not state its subscriptions — is deliberately
|
|
1002
|
+
// NOT red. It is a question that went unanswered, not an answer, and the
|
|
1003
|
+
// check already has `stripe-unavailable` for a census it could not take.
|
|
1004
|
+
if (facts.unsubscribedEvents && facts.unsubscribedEvents.length > 0) {
|
|
1005
|
+
return _extends({}, base, {
|
|
1006
|
+
ok: false,
|
|
1007
|
+
code: 'events-unsubscribed'
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
if (facts.undelivered > threshold) {
|
|
1011
|
+
return _extends({}, base, {
|
|
1012
|
+
ok: false,
|
|
1013
|
+
code: 'deliveries-failing'
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
// THE 200-THAT-DID-NOTHING (AGL-1954), and it is LAST on purpose: every
|
|
1017
|
+
// code above explains why a handler would have had nothing to do, so
|
|
1018
|
+
// reporting inertness over one of them would name the symptom instead of
|
|
1019
|
+
// the cause. Reached only when Stripe holds an enabled destination
|
|
1020
|
+
// subscribed to everything we asked for and delivered successfully — at
|
|
1021
|
+
// which point "a delivery moved nothing" has no innocent explanation left.
|
|
1022
|
+
if (facts.inert !== null && facts.inert > inertThreshold) {
|
|
1023
|
+
return _extends({}, base, {
|
|
1024
|
+
ok: false,
|
|
1025
|
+
code: 'handlers-inert'
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
/*==========================================
|
|
1029
|
+
* THE ATTEMPTS NOTHING ELSE HERE CAN COUNT (AGL-2039).
|
|
1030
|
+
*
|
|
1031
|
+
* `undelivered` above is `delivery_success=false`, a TERMINAL-state filter
|
|
1032
|
+
* over EVENTS. An event that 400s three times and then succeeds on the
|
|
1033
|
+
* fourth reads back `true`, so it is zero there and zero in `emitted`,
|
|
1034
|
+
* `processed` and `inert` too — the handler DID eventually run. Every
|
|
1035
|
+
* number on this check reports a healthy window while three real delivery
|
|
1036
|
+
* attempts failed. That is not a hypothetical: it is the exact reading
|
|
1037
|
+
* AGL-1906 produced, 0.00% against the Stripe Dashboard's 30%, and the
|
|
1038
|
+
* three attempts the Dashboard was counting were AGL-1551's.
|
|
1039
|
+
*
|
|
1040
|
+
* BELOW `deliveries-failing` because an event failing every attempt
|
|
1041
|
+
* explains an event that needed several, and below `handlers-inert`
|
|
1042
|
+
* because a delivery that landed late and then moved nothing is worse than
|
|
1043
|
+
* one that landed late and worked. ABOVE the Connect codes because this is
|
|
1044
|
+
* this hour's revenue path and those are a slower rot.
|
|
1045
|
+
*
|
|
1046
|
+
* Null is not red: the marker is ours, so an unread Firestore is an
|
|
1047
|
+
* unanswered question, and `stripe-unavailable` already covers a census we
|
|
1048
|
+
* could not take.
|
|
1049
|
+
*=========================================*/ if (facts.retried !== null && facts.retried > retriedThreshold) {
|
|
1050
|
+
return _extends({}, base, {
|
|
1051
|
+
ok: false,
|
|
1052
|
+
code: 'deliveries-retried'
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
/*==========================================
|
|
1056
|
+
* THE CONNECT DESTINATION (AGL-1948, closing AGL-2122's blind spot).
|
|
1057
|
+
*
|
|
1058
|
+
* A SECOND destination, and every code above is about the first one. The
|
|
1059
|
+
* platform destination can be enabled, fully subscribed, delivering and
|
|
1060
|
+
* acting — the whole check green — while connected-account events reach
|
|
1061
|
+
* nothing at all, because they are delivered only to a destination created
|
|
1062
|
+
* with `connect: true`. AGL-2122 measured exactly that against the live
|
|
1063
|
+
* account on 2026-08-18: one destination, correct URL, all ten events, and
|
|
1064
|
+
* no Connect destination in existence. `account.updated` was handled in two
|
|
1065
|
+
* plugins and could never be delivered, so `syncConnectAccountStatus` never
|
|
1066
|
+
* ran and every merchant's charge-eligibility flag rotted where it stood.
|
|
1067
|
+
*
|
|
1068
|
+
* LAST on purpose, and for a different reason than `handlers-inert` above:
|
|
1069
|
+
* not because the codes above explain it, but because they do not. It is an
|
|
1070
|
+
* independent destination whose failure is slower — merchant readiness
|
|
1071
|
+
* drifting out of date — while everything above is this hour's revenue. The
|
|
1072
|
+
* facts ride in the body either way, so the staff board shows the Connect
|
|
1073
|
+
* state even when a louder code takes the headline; only the single `code`
|
|
1074
|
+
* is exclusive.
|
|
1075
|
+
*
|
|
1076
|
+
* `missing` IS red. Before AGL-2122 that would have been wrong, because
|
|
1077
|
+
* nothing created the destination and a permanently-red check teaches
|
|
1078
|
+
* people to ignore the board. Now that setup-stripe creates it, its absence
|
|
1079
|
+
* is a real regression with a one-line remedy.
|
|
1080
|
+
*=========================================*/ if (facts.connectEndpoint === 'missing') {
|
|
1081
|
+
return _extends({}, base, {
|
|
1082
|
+
ok: false,
|
|
1083
|
+
code: 'connect-endpoint-missing'
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
if (facts.connectEndpoint === 'disabled') {
|
|
1087
|
+
return _extends({}, base, {
|
|
1088
|
+
ok: false,
|
|
1089
|
+
code: 'connect-endpoint-disabled'
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
if (facts.unsubscribedConnectEvents && facts.unsubscribedConnectEvents.length > 0) {
|
|
1093
|
+
return _extends({}, base, {
|
|
1094
|
+
ok: false,
|
|
1095
|
+
code: 'connect-events-unsubscribed'
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
return _extends({}, base, {
|
|
1099
|
+
ok: true
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
export function meteredPricingHealth(facts, ms) {
|
|
1103
|
+
const base = {
|
|
1104
|
+
ms,
|
|
1105
|
+
stripeConfigured: facts.stripeConfigured,
|
|
1106
|
+
monthly: facts.monthly,
|
|
1107
|
+
yearly: facts.yearly
|
|
1108
|
+
};
|
|
1109
|
+
// Unprovisioned: no Stripe, no money, no alarm.
|
|
1110
|
+
if (!facts.stripeConfigured) {
|
|
1111
|
+
return _extends({}, base, {
|
|
1112
|
+
ok: true,
|
|
1113
|
+
code: 'stripe-unconfigured',
|
|
1114
|
+
unbilledInterval: null
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
if (facts.monthly && facts.yearly) {
|
|
1118
|
+
return _extends({}, base, {
|
|
1119
|
+
ok: true,
|
|
1120
|
+
unbilledInterval: null
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
if (!facts.monthly && !facts.yearly) {
|
|
1124
|
+
return _extends({}, base, {
|
|
1125
|
+
ok: false,
|
|
1126
|
+
code: 'metered-price-missing',
|
|
1127
|
+
unbilledInterval: 'both'
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
// Exactly one. The AGL-1931 shape.
|
|
1131
|
+
return _extends({}, base, {
|
|
1132
|
+
ok: false,
|
|
1133
|
+
code: 'metered-price-asymmetric',
|
|
1134
|
+
unbilledInterval: facts.yearly ? 'month' : 'year'
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
/*==========================================
|
|
1138
|
+
* SCHEDULED JOBS, WATCHED BY ABSENCE (AGL-1955).
|
|
1139
|
+
*
|
|
1140
|
+
* ## The failure this exists for
|
|
1141
|
+
*
|
|
1142
|
+
* `Cloud Scheduler job run failed (aglyn-main)` is a log-match on
|
|
1143
|
+
* `resource.type="cloud_scheduler_job" AND severity >= ERROR`, and the
|
|
1144
|
+
* `scheduled-crons.yml` workflow goes red on a non-200. Both of those are
|
|
1145
|
+
* triggered BY A RUN. A job that is deleted, paused, or whose `- cron:` line
|
|
1146
|
+
* is edited away produces no attempt, therefore no error entry, therefore no
|
|
1147
|
+
* red workflow, therefore nothing. Quiet reads as healthy — the same shape
|
|
1148
|
+
* AGL-1923 closed for the error beacon, one subsystem over.
|
|
1149
|
+
*
|
|
1150
|
+
* What is downstream of these jobs is not cosmetic: `report-usage` is the
|
|
1151
|
+
* only writer that meters a closed month into Stripe, `run-erasures` is the
|
|
1152
|
+
* GDPR deletion runner, `audit-archive` moves and then DELETES audit rows,
|
|
1153
|
+
* and `plugin-jobs-beat` is the only thing that makes scheduled publishing
|
|
1154
|
+
* and booking-hold expiry happen at all. A silently unscheduled job means
|
|
1155
|
+
* customers are not billed, or data is not reaped, behind a green board.
|
|
1156
|
+
*
|
|
1157
|
+
* ## The two properties that make this work
|
|
1158
|
+
*
|
|
1159
|
+
* **It does not need the job to be alive to fire.** That is the whole bug.
|
|
1160
|
+
* The verdict is computed by the READER — `/api/health/crons`, wound by the
|
|
1161
|
+
* uptime probe and by any staff member opening the Health page — against
|
|
1162
|
+
* marks the jobs left behind. Nothing here is on a schedule that could
|
|
1163
|
+
* itself stop; that would only move the problem one layer out, which is the
|
|
1164
|
+
* AGL-1923 argument for a graded switch over a `conditionAbsent` policy.
|
|
1165
|
+
*
|
|
1166
|
+
* **It cannot false-alarm on a legitimately idle period.** The mark is
|
|
1167
|
+
* stamped by the INVOCATION, not by the work: a `finish-domain-attachments`
|
|
1168
|
+
* run with no pending host still beats, and a quiet week for
|
|
1169
|
+
* `reap-plugin-artifacts` still beats. And the expected time is computed
|
|
1170
|
+
* from the job's own cron expression rather than from a fixed interval, so
|
|
1171
|
+
* `usage-email` — hourly, but only on the 1st and 2nd of the month — is
|
|
1172
|
+
* green for the twenty-nine days it is deliberately not running. A fixed
|
|
1173
|
+
* "expect one every hour" rule would have paged on the 3rd, every month.
|
|
1174
|
+
*
|
|
1175
|
+
* ## Why an output age rule was not enough on its own
|
|
1176
|
+
*
|
|
1177
|
+
* AGL-1843's `exportsHealth` watches the weekly export by the age of what it
|
|
1178
|
+
* PRODUCES, and that is the better rule where an output exists — it catches
|
|
1179
|
+
* "ran and failed" as well as "never ran". Most of these jobs have no such
|
|
1180
|
+
* output. `backfill-scope` is detect-only and deliberately writes nothing.
|
|
1181
|
+
* `usage-alerts` produces a notification only when somebody is over budget.
|
|
1182
|
+
* `reap-plugin-artifacts` deletes nothing in a normal week. For those, the
|
|
1183
|
+
* only honest thing whose age moves on every run is a mark the run itself
|
|
1184
|
+
* leaves — so that is what this reads. `exportsHealth` stays where it is:
|
|
1185
|
+
* the two are complementary, and `firestore-export` is watched by both.
|
|
1186
|
+
*
|
|
1187
|
+
* ## It must be able to go GREEN
|
|
1188
|
+
*
|
|
1189
|
+
* Nothing latches. A job that missed a fire and then runs again stamps a
|
|
1190
|
+
* fresh mark and reads healthy on the next probe — the AGL-1843 rule, whose
|
|
1191
|
+
* clearing event here is named and is "the next invocation of that job".
|
|
1192
|
+
*=========================================*/ /** Where the marks live. One document per job id, written by the job. */ export const CRON_BEAT_COLLECTION = 'platformCronBeats';
|
|
1193
|
+
/**
|
|
1194
|
+
* The document that records when this deployment STARTED watching.
|
|
1195
|
+
*
|
|
1196
|
+
* Without it a job that has never reported cannot be told apart from a job
|
|
1197
|
+
* that has stopped reporting, and every board would come up red on the day
|
|
1198
|
+
* the feature deploys — for `usage-email`, red until the 1st of the next
|
|
1199
|
+
* month. The rule this enables is stated once, in `cronJobsHealth`: a job
|
|
1200
|
+
* with no mark is only silent once a scheduled fire time has passed SINCE
|
|
1201
|
+
* we started watching.
|
|
1202
|
+
*
|
|
1203
|
+
* Not a reserved id: Firestore reserves `__.*__`, and this is a plain slug
|
|
1204
|
+
* that no job id can collide with (job ids never contain the word `watch`,
|
|
1205
|
+
* and the wiring spec asserts the inventory's ids against the workflow).
|
|
1206
|
+
*/ export const CRON_BEAT_WATCH_DOC = 'watch-window';
|
|
1207
|
+
/**
|
|
1208
|
+
* Where a degraded verdict is recorded, so a red window survives it.
|
|
1209
|
+
*
|
|
1210
|
+
* The response body names the late job, but only to whoever is holding it,
|
|
1211
|
+
* and the uptime probe reads the status and discards the body. Vercel's
|
|
1212
|
+
* runtime log keeps a `console.error` for about an hour, and the Cloud
|
|
1213
|
+
* Logging drain deliberately does not forward one — `isServerErrorEntry`
|
|
1214
|
+
* drops `level: 'error'`, because that is every fail-soft line the beacons
|
|
1215
|
+
* write and forwarding it is what a self-feeding bill is made of. So the
|
|
1216
|
+
* only sink that outlives the window is the one the endpoint already holds
|
|
1217
|
+
* a handle to.
|
|
1218
|
+
*
|
|
1219
|
+
* Written by the READER, like the verdict itself. One document, replaced on
|
|
1220
|
+
* each degraded probe, holding the failing rows and when the window opened.
|
|
1221
|
+
* Same collision argument as the watch document: a plain slug, and no job id
|
|
1222
|
+
* contains `degraded`.
|
|
1223
|
+
*/ export const CRON_BEAT_DEGRADED_DOC = 'last-degraded';
|
|
1224
|
+
/**
|
|
1225
|
+
* Every job's latest mark in one document, `{ beats: { [jobId]: atMs } }`.
|
|
1226
|
+
*
|
|
1227
|
+
* The reader's fast path (AGL-2946). Listing the collection costs one read per
|
|
1228
|
+
* job on every uncached probe — ~23 reads, several hundred times a day, on a
|
|
1229
|
+
* project that pays for reads past the free tier — to answer a question this
|
|
1230
|
+
* one document answers.
|
|
1231
|
+
*
|
|
1232
|
+
* A COPY, never the authority. `writeCronBeat` writes it second, so a job
|
|
1233
|
+
* whose own mark landed can still be missing or stale here, and the reader
|
|
1234
|
+
* confirms any job this document calls late against that job's own mark
|
|
1235
|
+
* before reporting it. Stale costs a read; it never costs a false red.
|
|
1236
|
+
*
|
|
1237
|
+
* Same collision argument as the other two: a plain slug no job id equals.
|
|
1238
|
+
*/ export const CRON_BEAT_SUMMARY_DOC = 'summary';
|
|
1239
|
+
/** The reserved documents in `CRON_BEAT_COLLECTION` that are not job marks. */ export const CRON_BEAT_RESERVED_DOCS = new Set([
|
|
1240
|
+
CRON_BEAT_WATCH_DOC,
|
|
1241
|
+
CRON_BEAT_DEGRADED_DOC,
|
|
1242
|
+
CRON_BEAT_SUMMARY_DOC
|
|
1243
|
+
]);
|
|
1244
|
+
/**
|
|
1245
|
+
* How long a gap may be before the next degraded probe counts as a NEW
|
|
1246
|
+
* window rather than a continuation of the one on file.
|
|
1247
|
+
*
|
|
1248
|
+
* Three probe TTLs. Long enough that an instance answering intermittently
|
|
1249
|
+
* does not split one outage into a dozen, short enough that yesterday's
|
|
1250
|
+
* window is never mistaken for today's. The comparison is against the
|
|
1251
|
+
* failing set as well, so a different set always opens a new window.
|
|
1252
|
+
*/ export const CRON_DEGRADED_WINDOW_CONTINUITY_MS = 15 * 60000;
|
|
1253
|
+
/**
|
|
1254
|
+
* THE INVENTORY.
|
|
1255
|
+
*
|
|
1256
|
+
* Seven GitHub Actions schedules (`.github/workflows/scheduled-crons.yml`) —
|
|
1257
|
+
* the weekly jobs, the month-boundary usage-email sweep and the hourly CRM
|
|
1258
|
+
* task reminders, for which an hour of drift is nothing — and the rows
|
|
1259
|
+
* driven by Cloud Scheduler out of
|
|
1260
|
+
* `cloud/functions/src/index.ts`: `pluginJobsBeat` and `consoleAiJobsBeat`
|
|
1261
|
+
* (every minute), the routes the `consoleFastCrons` job carries every
|
|
1262
|
+
* fifteen (AGL-1617), and one `consoleDailyCron` export per daily job.
|
|
1263
|
+
*
|
|
1264
|
+
* `scheduled-crons-wiring.spec.ts` holds BOTH runners against their source —
|
|
1265
|
+
* the workflow for the `github-actions` rows, the functions file for the
|
|
1266
|
+
* `cloud-scheduler` ones — in both directions. An inventory row for a job
|
|
1267
|
+
* nobody schedules and a scheduled job nobody watches are the same bug seen
|
|
1268
|
+
* from opposite ends, and both fail the build.
|
|
1269
|
+
*
|
|
1270
|
+
* `report-usage` appears TWICE and is two jobs, not one. The 02:00 run
|
|
1271
|
+
* rolls up the closed month and is the only run that ever reaches Stripe;
|
|
1272
|
+
* the 07:00 `?month=current` run writes the in-progress figure every usage
|
|
1273
|
+
* budget reads (AGL-2219). Either can stop without the other, and folding
|
|
1274
|
+
* them into one row would hide exactly that.
|
|
1275
|
+
*/ /*
|
|
1276
|
+
* ⚠️ GRACE IS A PROPERTY OF THE RUNNER, not of the job.
|
|
1277
|
+
*
|
|
1278
|
+
* The five daily console jobs carried a SIX-HOUR grace because GitHub
|
|
1279
|
+
* Actions dispatches scheduled workflows on a best-effort basis and routinely
|
|
1280
|
+
* drifted by an hour or two. That grace bought tolerance and paid for it in
|
|
1281
|
+
* blindness: on 2026-08-27 GitHub dropped the whole day, and three of these
|
|
1282
|
+
* were thirty hours silent before anything said so.
|
|
1283
|
+
*
|
|
1284
|
+
* They run on Cloud Scheduler now, which has been punctual to the minute
|
|
1285
|
+
* since AGL-1617, and each function's own ceiling is 540 seconds. Ninety
|
|
1286
|
+
* minutes is therefore ten times the longest a healthy run can take and still
|
|
1287
|
+
* catches a dead job inside the working day, instead of most of a day later.
|
|
1288
|
+
*/ export const SCHEDULED_JOBS = [
|
|
1289
|
+
{
|
|
1290
|
+
id: 'report-usage',
|
|
1291
|
+
label: 'Metered usage rollup (closed month)',
|
|
1292
|
+
cron: '0 2 * * *',
|
|
1293
|
+
runner: 'cloud-scheduler',
|
|
1294
|
+
target: '/api/billing/report-usage',
|
|
1295
|
+
graceMinutes: 90,
|
|
1296
|
+
drives: 'The only run that meters a closed month into Stripe. If it stops, customers are not billed for what they used.'
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
id: 'audit-archive',
|
|
1300
|
+
label: 'Audit archive',
|
|
1301
|
+
cron: '0 3 * * *',
|
|
1302
|
+
runner: 'cloud-scheduler',
|
|
1303
|
+
target: '/api/admin/audit-archive',
|
|
1304
|
+
graceMinutes: 90,
|
|
1305
|
+
drives: 'Moves adminAudit rows past the 90-day window into Storage and deletes them from Firestore. If it stops, the retention promise is not being kept.'
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
id: 'run-erasures',
|
|
1309
|
+
label: 'GDPR erasure runner',
|
|
1310
|
+
cron: '0 4 * * *',
|
|
1311
|
+
runner: 'cloud-scheduler',
|
|
1312
|
+
target: '/api/admin/run-erasures',
|
|
1313
|
+
graceMinutes: 90,
|
|
1314
|
+
drives: 'Executes accepted erasure requests. If it stops, personal data a customer asked us to delete is still here, and the clock on that request is still running.'
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
id: 'report-usage-current',
|
|
1318
|
+
label: 'Metered usage rollup (current month)',
|
|
1319
|
+
cron: '0 7 * * *',
|
|
1320
|
+
runner: 'cloud-scheduler',
|
|
1321
|
+
target: '/api/billing/report-usage?month=current',
|
|
1322
|
+
graceMinutes: 90,
|
|
1323
|
+
drives: 'Writes the in-progress month figure (AGL-2219). If it stops, every usage budget on the platform is structurally unable to fire and the Billing card stops totalling.'
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
id: 'usage-alerts',
|
|
1327
|
+
label: 'Usage budget alerts',
|
|
1328
|
+
cron: '0 8 * * *',
|
|
1329
|
+
runner: 'cloud-scheduler',
|
|
1330
|
+
target: '/api/billing/usage-alerts',
|
|
1331
|
+
graceMinutes: 90,
|
|
1332
|
+
drives: 'Evaluates usage budgets and notifies. If it stops, an org sails past its budget with no warning and finds out on the invoice.'
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
id: 'reap-sending-domains',
|
|
1336
|
+
label: 'Orphaned sending-domain reaper',
|
|
1337
|
+
// An hour after `run-erasures`, which is what produces most of its work:
|
|
1338
|
+
// an erasure records what it could not release rather than waiting on a
|
|
1339
|
+
// vendor, and this is the only thing that settles that debt.
|
|
1340
|
+
cron: '0 5 * * *',
|
|
1341
|
+
runner: 'cloud-scheduler',
|
|
1342
|
+
target: '/api/admin/reap-sending-domains',
|
|
1343
|
+
graceMinutes: 90,
|
|
1344
|
+
drives: 'Releases the provider domain object and the zone records of every sending domain whose site or workspace is gone. If it stops, each deleted site keeps one of a bounded number of provider domain slots forever — until the ceiling is reached and a site that asks for a domain of its own is refused one, which leaves it on the shared pool rather than stopping its mail — and leaves a live DKIM key in our zone under a label a future site can claim and inherit a stranger’s signature from.'
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
id: 'reap-unverified-orgs',
|
|
1348
|
+
label: 'Unverified signup reaper',
|
|
1349
|
+
// 06:00, after `run-erasures` and the sending-domain sweep and before the
|
|
1350
|
+
// billing pair. It calls `eraseOrg` itself, so it wants the erasure runner
|
|
1351
|
+
// to have already finished with whatever it was holding.
|
|
1352
|
+
cron: '0 6 * * *',
|
|
1353
|
+
runner: 'cloud-scheduler',
|
|
1354
|
+
target: '/api/admin/reap-unverified-orgs?dryRun=0',
|
|
1355
|
+
graceMinutes: 90,
|
|
1356
|
+
drives: 'Erases workspaces whose sole owner never confirmed an email address, releasing the workspace address they took, and promotes the held address of every owner who has since verified. If it stops, a name claimed with a throwaway inbox is held until its reservation expires — and, worse, a real customer who verified keeps a pending address that becomes claimable by anyone on day twenty-one.'
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
id: 'ai-insights-digest',
|
|
1360
|
+
label: 'Weekly insights',
|
|
1361
|
+
// 06:00 and 14:00 every day (AGL-2915): Monday's first run makes the
|
|
1362
|
+
// week's digests and every run after it delivers the ones the AI jobs beat
|
|
1363
|
+
// has written. Served by the AI plugin.
|
|
1364
|
+
cron: '0 6,14 * * *',
|
|
1365
|
+
runner: 'cloud-scheduler',
|
|
1366
|
+
target: '/api/admin/ai-insights-digest',
|
|
1367
|
+
graceMinutes: 90,
|
|
1368
|
+
drives: 'Each Monday, makes the weekly insights people asked for, and delivers them once they are written. If it stops, nobody who asked is sent their insights, and a digest parked for credits is never given up.'
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
id: 'usage-email',
|
|
1372
|
+
label: 'Monthly usage summaries',
|
|
1373
|
+
// Hourly across the FIRST TWO DAYS only (AGL-2409). The idle-period case
|
|
1374
|
+
// this whole check had to get right: for twenty-nine days a month this
|
|
1375
|
+
// job is correctly doing nothing, and must read green while it does.
|
|
1376
|
+
cron: '0 * 1-2 * *',
|
|
1377
|
+
runner: 'github-actions',
|
|
1378
|
+
target: '/api/billing/usage-email',
|
|
1379
|
+
graceMinutes: 360,
|
|
1380
|
+
drives: "Mails each org last month's usage summary, chunked across 48 hourly windows. If it stops, the month's summaries are simply never sent."
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
id: 'crm-task-reminders',
|
|
1384
|
+
label: 'CRM task reminders',
|
|
1385
|
+
// Hourly, all month (AGL-2659). The grace is `usage-email`'s, for the
|
|
1386
|
+
// reason that row gives about this runner: GitHub's dispatch is
|
|
1387
|
+
// best-effort, and a reminder stays owed on the task until a run
|
|
1388
|
+
// handles it, so a dropped hour costs lateness rather than silence.
|
|
1389
|
+
cron: '0 * * * *',
|
|
1390
|
+
runner: 'github-actions',
|
|
1391
|
+
target: '/api/crm/task-reminders',
|
|
1392
|
+
graceMinutes: 360,
|
|
1393
|
+
drives: 'Sends each CRM task’s reminder at its own time — a console notification and an email to the assignee (AGL-2659). If it stops, every reminder a person set in the task drawer is silently late, and nothing but the morning digest says a task was due.'
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
id: 'firestore-export',
|
|
1397
|
+
label: 'Weekly Firestore export',
|
|
1398
|
+
cron: '0 5 * * 1',
|
|
1399
|
+
runner: 'github-actions',
|
|
1400
|
+
target: '/api/admin/firestore-export',
|
|
1401
|
+
graceMinutes: 1440,
|
|
1402
|
+
drives: 'Starts the independent GCS export — the restore point that exists because the managed backups kept flipping to NOT_AVAILABLE (AGL-1843). Also watched by output age on /api/health/backups.'
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
id: 'reap-plugin-artifacts',
|
|
1406
|
+
label: 'Plugin artifact reaper',
|
|
1407
|
+
cron: '30 5 * * 1',
|
|
1408
|
+
runner: 'github-actions',
|
|
1409
|
+
target: '/api/admin/reap-plugin-artifacts',
|
|
1410
|
+
graceMinutes: 1440,
|
|
1411
|
+
drives: 'Deletes orphaned plugin bundles from the artifacts bucket. If it stops, storage grows without bound and nothing says so.'
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
id: 'reverify-plugin-versions',
|
|
1415
|
+
label: 'Plugin verdict re-verification',
|
|
1416
|
+
cron: '0 6 * * 1',
|
|
1417
|
+
runner: 'github-actions',
|
|
1418
|
+
target: '/api/admin/reverify-plugin-versions',
|
|
1419
|
+
graceMinutes: 1440,
|
|
1420
|
+
drives: 'Re-checks stored plugin verdicts against the current verifier (AGL-1086). If it stops, a regression on a live version is never noticed.'
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
id: 'reverify-sso-domains',
|
|
1424
|
+
label: 'SSO domain re-verification',
|
|
1425
|
+
// Mondays 07:30 UTC, after the rest of the weekly block. Weekly is the
|
|
1426
|
+
// right cadence because the threat needs a domain to lapse, clear a
|
|
1427
|
+
// registrar grace period and be re-bought — months, not days — and a
|
|
1428
|
+
// daily sweep would multiply DNS traffic by seven to learn nothing sooner.
|
|
1429
|
+
cron: '30 7 * * 1',
|
|
1430
|
+
runner: 'github-actions',
|
|
1431
|
+
target: '/api/admin/reverify-sso-domains',
|
|
1432
|
+
graceMinutes: 1440,
|
|
1433
|
+
drives: 'Re-checks that every live SSO domain still publishes its DNS ownership token (AGL-1210). If it stops, a domain that changed hands keeps routing another company sign-ins and nothing notices. It reports only — it never revokes, because a revoke logs out an entire enterprise.'
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
id: 'backfill-scope',
|
|
1437
|
+
label: 'Scope-drift detection',
|
|
1438
|
+
cron: '30 6 * * 1',
|
|
1439
|
+
runner: 'github-actions',
|
|
1440
|
+
target: '/api/admin/backfill-scope',
|
|
1441
|
+
graceMinutes: 1440,
|
|
1442
|
+
drives: 'Reports documents with no visibleTo — invisible to every site-scoped read (AGL-1478). Detect-only by construction, so it has no output whose age could stand in for this mark.'
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
id: 'campaigns-process-scheduled',
|
|
1446
|
+
label: 'Scheduled email campaigns',
|
|
1447
|
+
cron: '*/15 * * * *',
|
|
1448
|
+
// MOVED OFF GITHUB ACTIONS (AGL-1617), with the row below it. See the
|
|
1449
|
+
// grace note on `graceMinutes` for why that let the bar go UP.
|
|
1450
|
+
runner: 'cloud-scheduler',
|
|
1451
|
+
target: 'consoleFastCrons → console /api/campaigns/process-scheduled',
|
|
1452
|
+
graceMinutes: 45,
|
|
1453
|
+
drives: 'Claims and sends due campaigns. If it stops, a campaign the composer showed as Scheduled sits there and never sends — the AGL-2134 shape, which is sold on /product/marketing.'
|
|
1454
|
+
},
|
|
1455
|
+
{
|
|
1456
|
+
id: 'lists-materialize',
|
|
1457
|
+
label: 'Dynamic email lists',
|
|
1458
|
+
// Shares the `consoleFastCrons` job with the two rows around it — one
|
|
1459
|
+
// Cloud Scheduler job driving three routes, which is why adding this cost
|
|
1460
|
+
// no scheduler quota.
|
|
1461
|
+
cron: '*/15 * * * *',
|
|
1462
|
+
runner: 'cloud-scheduler',
|
|
1463
|
+
target: 'consoleFastCrons \u2192 console /api/lists/materialize',
|
|
1464
|
+
graceMinutes: 45,
|
|
1465
|
+
drives: 'Re-evaluates every dynamic list against its rule. If it stops, a list keeps whatever membership it last had, and a campaign sent to it reaches an audience that silently stopped tracking the rule the merchant wrote — which looks exactly like a rule that matched nobody new.'
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
id: 'finish-domain-attachments',
|
|
1469
|
+
label: 'Custom domain re-check',
|
|
1470
|
+
// Every fifteen minutes rather than twenty since AGL-1617: it shares one
|
|
1471
|
+
// Cloud Scheduler job with the campaign processor, and AGL-2010's argument
|
|
1472
|
+
// was always that a customer is sitting there waiting for their site.
|
|
1473
|
+
cron: '*/15 * * * *',
|
|
1474
|
+
runner: 'cloud-scheduler',
|
|
1475
|
+
target: 'consoleFastCrons → console /api/admin/finish-domain-attachments',
|
|
1476
|
+
graceMinutes: 45,
|
|
1477
|
+
drives: 'Re-checks pending custom domains after the certificate or DNS settles (AGL-2010). If it stops, a correctly-configured domain stays dark until a human presses Re-attach.'
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
id: 'provision-sending-domains',
|
|
1481
|
+
label: 'Sending domain provisioning',
|
|
1482
|
+
// The fourth route on the `consoleFastCrons` job, sharing its schedule
|
|
1483
|
+
// with the three rows above. It is a console route rather than a job on
|
|
1484
|
+
// the platform beat because issuing a DKIM key needs a full-access
|
|
1485
|
+
// provider credential the tenant runtime must never hold.
|
|
1486
|
+
cron: '*/15 * * * *',
|
|
1487
|
+
runner: 'cloud-scheduler',
|
|
1488
|
+
target: 'consoleFastCrons → console /api/admin/provision-sending-domains',
|
|
1489
|
+
graceMinutes: 45,
|
|
1490
|
+
drives: 'Turns a site’s sending-domain claim into a DKIM key and the DNS records that carry it. If it stops, every site that has ASKED for a domain of its own waits on a claim with no records to publish. Their mail keeps leaving on the shared pool throughout, which makes this a stall in reputation isolation rather than stopped mail — and it is invisible to the merchant, who sees only a domain that never finishes.'
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
id: 'drain-publish-outbox',
|
|
1494
|
+
label: 'Publish outbox drain',
|
|
1495
|
+
// The fifth route on the `consoleFastCrons` job. Fifteen minutes is the
|
|
1496
|
+
// bound on how long a publish whose tab went away can stay invisible,
|
|
1497
|
+
// and it is the schedule this shares rather than one chosen for it —
|
|
1498
|
+
// buying a tighter one would mean a second Cloud Scheduler job against a
|
|
1499
|
+
// free-tier allowance of three.
|
|
1500
|
+
cron: '*/15 * * * *',
|
|
1501
|
+
runner: 'cloud-scheduler',
|
|
1502
|
+
target: 'consoleFastCrons \u2192 console /api/admin/drain-publish-outbox',
|
|
1503
|
+
graceMinutes: 45,
|
|
1504
|
+
drives: 'Fires the cache-drop announce for publishes whose tab closed before it landed (AGL-2575). Publishing is a client Firestore write, so the announce is a fetch from the browser and a closed tab strands it; this is the only thing that finishes one. If it stops, a stranded publish is invisible on the live site for the full hour-long document TTL, and the pending entry that records it is never read by anything.'
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
id: 'plugin-console-crons',
|
|
1508
|
+
label: 'Plugin console jobs',
|
|
1509
|
+
// The sixth route on the `consoleFastCrons` job (AGL-2981). One route for
|
|
1510
|
+
// every job a plugin declares on `plugin-console-crons`, so a plugin's
|
|
1511
|
+
// job needs no route of its own in this list; each declared job has its
|
|
1512
|
+
// own row beside this one, added by `/api/health/crons` from the
|
|
1513
|
+
// declarations. This row answers the question under all of them: is the
|
|
1514
|
+
// route being posted at all.
|
|
1515
|
+
cron: '*/15 * * * *',
|
|
1516
|
+
runner: 'cloud-scheduler',
|
|
1517
|
+
target: 'consoleFastCrons → console /api/admin/plugin-crons',
|
|
1518
|
+
graceMinutes: 45,
|
|
1519
|
+
drives: 'Runs every console job a plugin declares, each with its own row on this board. If it stops, every one of those jobs stops with it — a plugin’s scheduled sends and syncs among them — and each of their rows goes silent too.'
|
|
1520
|
+
},
|
|
1521
|
+
{
|
|
1522
|
+
id: 'ai-jobs-beat',
|
|
1523
|
+
label: 'AI jobs beat',
|
|
1524
|
+
// Its own Cloud Scheduler job, every minute (AGL-3026): the console route
|
|
1525
|
+
// runs steps for most of five minutes at a time, and a page waits a beat
|
|
1526
|
+
// between sections. The grace is the other every-minute job's, below.
|
|
1527
|
+
cron: '* * * * *',
|
|
1528
|
+
runner: 'cloud-scheduler',
|
|
1529
|
+
target: 'consoleAiJobsBeat → console /api/admin/ai-jobs-beat',
|
|
1530
|
+
graceMinutes: 30,
|
|
1531
|
+
drives: 'Runs the AI generation steps the doors leave queued: every plan, every page pass, the passes of a site scaffold, and any step an inline door’s budget handed back (AGL-3026). It runs on the console because only the console holds the AI provider key. If it stops, a generation job that plans never gets its plan and a page is never built — the job sits queued, spending nothing, and never finishes.'
|
|
1532
|
+
},
|
|
1533
|
+
{
|
|
1534
|
+
id: 'plugin-jobs-beat',
|
|
1535
|
+
label: 'Plugin job beat',
|
|
1536
|
+
// Cloud Scheduler says `every 1 minutes`; the equivalent five-field
|
|
1537
|
+
// expression is what this check evaluates against.
|
|
1538
|
+
cron: '* * * * *',
|
|
1539
|
+
runner: 'cloud-scheduler',
|
|
1540
|
+
target: 'firebase-schedule-pluginJobsBeat-us-central1 → tenant /api/plugins/run-jobs',
|
|
1541
|
+
graceMinutes: 30,
|
|
1542
|
+
drives: 'The only thing that runs scheduled publishing and booking-hold expiry (AGL-1159). It is also the one job that can fail by pointing at the WRONG deployment (AGL-2176) — the mark is stamped by the tenant runner that answers, so a beat aimed elsewhere reads as silence here.'
|
|
1543
|
+
}
|
|
1544
|
+
];
|
|
1545
|
+
/**
|
|
1546
|
+
* A cron field, expanded to the set of values it matches.
|
|
1547
|
+
*
|
|
1548
|
+
* Supports a star, `a`, `a-b`, `a,b,c` and a trailing `/n` step on any of
|
|
1549
|
+
* them — every form the inventory and `scheduled-crons.yml` use, and nothing
|
|
1550
|
+
* else. An unparsable field returns null, which the caller treats as "matches
|
|
1551
|
+
* nothing". A schedule that can never fire then produces a null `dueAt` and
|
|
1552
|
+
* a green row rather than a permanent red one, and the wiring spec is what
|
|
1553
|
+
* catches the typo — a monitor that reds on its own parse bug teaches people
|
|
1554
|
+
* to ignore it.
|
|
1555
|
+
*/ function expandCronField(field, min, max) {
|
|
1556
|
+
const values = new Set();
|
|
1557
|
+
for (const part of field.split(',')){
|
|
1558
|
+
const [range, stepRaw] = part.split('/');
|
|
1559
|
+
const step = stepRaw === undefined ? 1 : Number.parseInt(stepRaw, 10);
|
|
1560
|
+
if (!Number.isFinite(step) || step <= 0) return null;
|
|
1561
|
+
let from;
|
|
1562
|
+
let to;
|
|
1563
|
+
if (range === '*') {
|
|
1564
|
+
from = min;
|
|
1565
|
+
to = max;
|
|
1566
|
+
} else if (range.includes('-')) {
|
|
1567
|
+
const [a, b] = range.split('-');
|
|
1568
|
+
from = Number.parseInt(a, 10);
|
|
1569
|
+
to = Number.parseInt(b, 10);
|
|
1570
|
+
} else {
|
|
1571
|
+
from = Number.parseInt(range, 10);
|
|
1572
|
+
to = stepRaw === undefined ? from : max;
|
|
1573
|
+
}
|
|
1574
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) return null;
|
|
1575
|
+
if (from < min || to > max || from > to) return null;
|
|
1576
|
+
for(let value = from; value <= to; value += step)values.add(value);
|
|
1577
|
+
}
|
|
1578
|
+
return values.size ? values : null;
|
|
1579
|
+
}
|
|
1580
|
+
function parseCron(expression) {
|
|
1581
|
+
const fields = expression.trim().split(/\s+/);
|
|
1582
|
+
if (fields.length !== 5) return null;
|
|
1583
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;
|
|
1584
|
+
const parsed = {
|
|
1585
|
+
minute: expandCronField(minute, 0, 59),
|
|
1586
|
+
hour: expandCronField(hour, 0, 23),
|
|
1587
|
+
dayOfMonth: expandCronField(dayOfMonth, 1, 31),
|
|
1588
|
+
month: expandCronField(month, 1, 12),
|
|
1589
|
+
dayOfWeek: expandCronField(dayOfWeek, 0, 6),
|
|
1590
|
+
domRestricted: dayOfMonth !== '*',
|
|
1591
|
+
dowRestricted: dayOfWeek !== '*'
|
|
1592
|
+
};
|
|
1593
|
+
if (!parsed.minute || !parsed.hour || !parsed.dayOfMonth || !parsed.month || !parsed.dayOfWeek) {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
return parsed;
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Does this UTC date fall on a day the expression can fire?
|
|
1600
|
+
*
|
|
1601
|
+
* The day-of-month / day-of-week OR is the standard cron rule: when BOTH are
|
|
1602
|
+
* restricted the job fires on either, not on both. No expression in the
|
|
1603
|
+
* inventory restricts both, and the rule is implemented anyway so that one
|
|
1604
|
+
* added later is not silently misjudged.
|
|
1605
|
+
*/ function cronDayMatches(parsed, date) {
|
|
1606
|
+
if (!parsed.month.has(date.getUTCMonth() + 1)) return false;
|
|
1607
|
+
const domHit = parsed.dayOfMonth.has(date.getUTCDate());
|
|
1608
|
+
const dowHit = parsed.dayOfWeek.has(date.getUTCDay());
|
|
1609
|
+
if (parsed.domRestricted && parsed.dowRestricted) return domHit || dowHit;
|
|
1610
|
+
if (parsed.domRestricted) return domHit;
|
|
1611
|
+
if (parsed.dowRestricted) return dowHit;
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* The latest time at or before `atMs` that `expression` fires, UTC.
|
|
1616
|
+
*
|
|
1617
|
+
* Null when the expression is unparsable, or when it does not fire inside
|
|
1618
|
+
* the lookback — 45 days, which comfortably clears the longest gap any
|
|
1619
|
+
* inventory entry has (`usage-email`'s twenty-nine idle days).
|
|
1620
|
+
*
|
|
1621
|
+
* Whole non-matching DAYS are skipped rather than walked minute by minute,
|
|
1622
|
+
* so the worst case is ~45 day checks plus 1,440 minute checks rather than
|
|
1623
|
+
* 64,800 of them.
|
|
1624
|
+
*/ export function previousCronFire(expression, atMs, lookbackDays = 45) {
|
|
1625
|
+
const parsed = parseCron(expression);
|
|
1626
|
+
if (!parsed) return null;
|
|
1627
|
+
// Floor to the minute: a fire at 05:00:00 counts as due at 05:00:30.
|
|
1628
|
+
let cursor = Math.floor(atMs / 60000) * 60000;
|
|
1629
|
+
const floor = cursor - lookbackDays * 86400000;
|
|
1630
|
+
while(cursor >= floor){
|
|
1631
|
+
const date = new Date(cursor);
|
|
1632
|
+
if (!cronDayMatches(parsed, date)) {
|
|
1633
|
+
// Jump to 23:59 of the previous UTC day.
|
|
1634
|
+
const startOfDay = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
1635
|
+
cursor = startOfDay - 60000;
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
if (parsed.hour.has(date.getUTCHours()) && parsed.minute.has(date.getUTCMinutes())) {
|
|
1639
|
+
return cursor;
|
|
1640
|
+
}
|
|
1641
|
+
cursor -= 60000;
|
|
1642
|
+
}
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* The scheduled jobs, reduced to one health check per job (AGL-1955).
|
|
1647
|
+
*
|
|
1648
|
+
* `beats` null means the marks could not be read at all — degraded for every
|
|
1649
|
+
* row, the same rule `signupsHealth`, `rateLimitsHealth` and `beaconHealth`
|
|
1650
|
+
* follow. An alarm that cannot see the thing it watches must say so rather
|
|
1651
|
+
* than report calm; here it is the literal condition the issue is about.
|
|
1652
|
+
*
|
|
1653
|
+
* `watchStartedAtMs` is when this deployment first read the marks. A job with
|
|
1654
|
+
* no mark is silent only once a fire time has passed SINCE then — otherwise
|
|
1655
|
+
* every row would be red on the day this deploys, and `usage-email` would
|
|
1656
|
+
* stay red until the 1st.
|
|
1657
|
+
*
|
|
1658
|
+
* Pure on purpose, like its siblings: the route reads, this decides, the
|
|
1659
|
+
* spec exercises every branch without a network.
|
|
1660
|
+
*/ export function cronJobsHealth(beats, watchStartedAtMs, ms, now = Date.now(), jobs = SCHEDULED_JOBS) {
|
|
1661
|
+
const byId = new Map((beats != null ? beats : []).map((beat)=>[
|
|
1662
|
+
beat.jobId,
|
|
1663
|
+
beat.atMs
|
|
1664
|
+
]));
|
|
1665
|
+
const checks = {};
|
|
1666
|
+
for (const job of jobs){
|
|
1667
|
+
var _byId_get;
|
|
1668
|
+
const lastBeatMs = (_byId_get = byId.get(job.id)) != null ? _byId_get : null;
|
|
1669
|
+
const lastBeatAgeMinutes = lastBeatMs === null ? null : Math.round((now - lastBeatMs) / 60000);
|
|
1670
|
+
// The most recent fire time that is already past its grace. Everything
|
|
1671
|
+
// more recent than that is a run we are still waiting for, on time.
|
|
1672
|
+
const dueMs = previousCronFire(job.cron, now - job.graceMinutes * 60000);
|
|
1673
|
+
const base = {
|
|
1674
|
+
ms,
|
|
1675
|
+
schedule: job.cron,
|
|
1676
|
+
runner: job.runner,
|
|
1677
|
+
lastBeatAgeMinutes,
|
|
1678
|
+
dueAt: dueMs === null ? null : new Date(dueMs).toISOString(),
|
|
1679
|
+
graceMinutes: job.graceMinutes
|
|
1680
|
+
};
|
|
1681
|
+
if (beats === null) {
|
|
1682
|
+
checks[job.id] = _extends({}, base, {
|
|
1683
|
+
ok: false,
|
|
1684
|
+
code: 'beats-unavailable'
|
|
1685
|
+
});
|
|
1686
|
+
continue;
|
|
1687
|
+
}
|
|
1688
|
+
if (dueMs === null) {
|
|
1689
|
+
// Nothing was due inside the lookback. Green, and honestly so.
|
|
1690
|
+
checks[job.id] = _extends({}, base, {
|
|
1691
|
+
ok: true
|
|
1692
|
+
});
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (lastBeatMs === null) {
|
|
1696
|
+
// Never reported. Only a defect once a fire time has passed since we
|
|
1697
|
+
// started watching — before that it is a job we have not met yet.
|
|
1698
|
+
const overdue = dueMs > watchStartedAtMs;
|
|
1699
|
+
checks[job.id] = overdue ? _extends({}, base, {
|
|
1700
|
+
ok: false,
|
|
1701
|
+
code: 'job-never-reported'
|
|
1702
|
+
}) : _extends({}, base, {
|
|
1703
|
+
ok: true,
|
|
1704
|
+
code: 'awaiting-first-run'
|
|
1705
|
+
});
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
checks[job.id] = lastBeatMs < dueMs ? _extends({}, base, {
|
|
1709
|
+
ok: false,
|
|
1710
|
+
code: 'job-silent'
|
|
1711
|
+
}) : _extends({}, base, {
|
|
1712
|
+
ok: true
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
return checks;
|
|
1716
|
+
}
|
|
1717
|
+
export async function writeCronBeat(store, jobId, now = Date.now()) {
|
|
1718
|
+
try {
|
|
1719
|
+
const beats = store.collection(CRON_BEAT_COLLECTION);
|
|
1720
|
+
await beats.doc(jobId).set({
|
|
1721
|
+
jobId,
|
|
1722
|
+
atMs: now,
|
|
1723
|
+
at: new Date(now).toISOString()
|
|
1724
|
+
}, {
|
|
1725
|
+
merge: true
|
|
1726
|
+
});
|
|
1727
|
+
try {
|
|
1728
|
+
await beats.doc(CRON_BEAT_SUMMARY_DOC).set({
|
|
1729
|
+
beats: {
|
|
1730
|
+
[jobId]: now
|
|
1731
|
+
}
|
|
1732
|
+
}, {
|
|
1733
|
+
merge: true
|
|
1734
|
+
});
|
|
1735
|
+
} catch (unused) {
|
|
1736
|
+
// The reader confirms a stale summary entry against the mark above.
|
|
1737
|
+
}
|
|
1738
|
+
return true;
|
|
1739
|
+
} catch (unused) {
|
|
1740
|
+
return false;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* The beats a summary document holds, ignoring anything malformed.
|
|
1745
|
+
*
|
|
1746
|
+
* `raw` is the document's `beats` field as Firestore returns it — a plain map,
|
|
1747
|
+
* or undefined when the document does not exist yet.
|
|
1748
|
+
*/ export function cronBeatsFromSummary(raw) {
|
|
1749
|
+
if (!raw || typeof raw !== 'object') return [];
|
|
1750
|
+
return Object.entries(raw).map(([jobId, atMs])=>({
|
|
1751
|
+
jobId,
|
|
1752
|
+
atMs: Number(atMs)
|
|
1753
|
+
})).filter((beat)=>!CRON_BEAT_RESERVED_DOCS.has(beat.jobId)).filter((beat)=>Number.isFinite(beat.atMs));
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* How old the last successful walk may be before this reds.
|
|
1757
|
+
*
|
|
1758
|
+
* Two hours against an hourly schedule — one missed run is tolerated, two are
|
|
1759
|
+
* not. A canary that runs hourly and reds at 61 minutes would page on every
|
|
1760
|
+
* GitHub Actions queue delay, and an alarm that cries wolf on infrastructure
|
|
1761
|
+
* latency is one nobody reads by the time signup actually breaks. Two misses
|
|
1762
|
+
* is a pattern rather than a hiccup.
|
|
1763
|
+
*
|
|
1764
|
+
* This is deliberately NOT generous enough to sleep through AGL-2581: three
|
|
1765
|
+
* days of refused signups would red this inside two hours.
|
|
1766
|
+
*/ /**
|
|
1767
|
+
* Workspace-slug prefix the signup canary owns, and the one piece of the
|
|
1768
|
+
* product that has to know the canary exists (AGL-2715).
|
|
1769
|
+
*
|
|
1770
|
+
* ## Why an exclusion is not optional
|
|
1771
|
+
*
|
|
1772
|
+
* The canary creates a real org and then deletes it. `signupDrought` compares
|
|
1773
|
+
* durable ATTEMPT markers against a live COUNT of orgs — and a deleted org
|
|
1774
|
+
* leaves the count while its attempt marker stays. So every canary walk adds
|
|
1775
|
+
* 1 to the denominator and 0 to the numerator, and after three walks the
|
|
1776
|
+
* platform reports `signup-drought` with nothing wrong.
|
|
1777
|
+
*
|
|
1778
|
+
* That is not theoretical. It fired on 2026-09-09 at six attempts against
|
|
1779
|
+
* zero creations — six walks that had each just proved, end to end, that a
|
|
1780
|
+
* stranger could sign up. A monitor that manufactures the outage it watches
|
|
1781
|
+
* for is worse than no monitor, and it is the same alarm fatigue AGL-2714
|
|
1782
|
+
* removed, arriving from inside the house.
|
|
1783
|
+
*
|
|
1784
|
+
* ## Why the slug, and what that costs
|
|
1785
|
+
*
|
|
1786
|
+
* `/api/orgs/create` receives the requested slug before it does anything, so
|
|
1787
|
+
* this needs no secret, no header and no second code path — the canary is
|
|
1788
|
+
* excluded by being what it says it is.
|
|
1789
|
+
*
|
|
1790
|
+
* The cost is that the prefix is caller-supplied: someone naming their
|
|
1791
|
+
* workspace `signup-canary-…` would not be counted in the drought's
|
|
1792
|
+
* denominator. That is bounded to one uncounted attempt per such workspace,
|
|
1793
|
+
* and it cannot hide an outage, because an outage is many attempts by many
|
|
1794
|
+
* people and only the oddly-named ones would go missing.
|
|
1795
|
+
*
|
|
1796
|
+
* The canary declares the same prefix in `tools/e2e/signup-canary.mjs`, which
|
|
1797
|
+
* cannot import this file — it runs outside the workspace. They are held
|
|
1798
|
+
* together by `apps/console/specs/signup-canary-marker-wiring.spec.ts`.
|
|
1799
|
+
*/ export const SIGNUP_CANARY_ORG_SLUG_PREFIX = 'signup-canary-';
|
|
1800
|
+
/**
|
|
1801
|
+
* Is this org creation the canary's own?
|
|
1802
|
+
*
|
|
1803
|
+
* Exported as a function rather than leaving callers to compare strings, so
|
|
1804
|
+
* the one place that decides is the one place to read when asking why a
|
|
1805
|
+
* number looks wrong.
|
|
1806
|
+
*/ export function isSignupCanaryOrgSlug(slug) {
|
|
1807
|
+
return typeof slug === 'string' && slug.startsWith(SIGNUP_CANARY_ORG_SLUG_PREFIX);
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* How old the last walk may be before the check goes red.
|
|
1811
|
+
*
|
|
1812
|
+
* Six hours, far wider than the schedule that writes it, and deliberately so.
|
|
1813
|
+
* GitHub Actions serves `schedule` from a best-effort queue that drops most
|
|
1814
|
+
* requests under load: measured 2026-09-10, `uptime-probe.yml` asks for every
|
|
1815
|
+
* fifteen minutes and actually fired at gaps of 1h55m to 4h38m across the
|
|
1816
|
+
* preceding day and a half. A window sized to the cron expression rather than
|
|
1817
|
+
* to that behavior reds on ordinary queue depth.
|
|
1818
|
+
*
|
|
1819
|
+
* Widening it costs no detection speed, because staleness is not how a broken
|
|
1820
|
+
* signup surfaces. A walk that cannot sign up FAILS and still writes its
|
|
1821
|
+
* marker, which reads `signup-walk-failed` on the next poll. Staleness can
|
|
1822
|
+
* only mean the walker never reported — a CI condition, not a product one —
|
|
1823
|
+
* so this window's whole job is to notice a canary that has genuinely stopped,
|
|
1824
|
+
* and it must not fire before then: this check grades the monitor that must
|
|
1825
|
+
* never be muted.
|
|
1826
|
+
*/ export const SIGNUP_CANARY_STALE_AFTER_MS = 6 * 60 * 60 * 1000;
|
|
1827
|
+
/**
|
|
1828
|
+
* Grade the last recorded signup walk.
|
|
1829
|
+
*
|
|
1830
|
+
* Pure, like every sibling: the route reads the marker, this decides. Ordered
|
|
1831
|
+
* by which mistake costs most — a missing marker is treated as failure, never
|
|
1832
|
+
* as calm, because "no evidence" and "evidence of success" are the two
|
|
1833
|
+
* readings this whole issue exists to stop anyone from confusing.
|
|
1834
|
+
*/ export function signupCanaryHealth(marker, ms, nowMs = Date.now(), staleAfterMs = SIGNUP_CANARY_STALE_AFTER_MS) {
|
|
1835
|
+
const base = {
|
|
1836
|
+
ms,
|
|
1837
|
+
ageMs: null,
|
|
1838
|
+
failedStep: null,
|
|
1839
|
+
walkMs: null,
|
|
1840
|
+
reapedCleanly: null,
|
|
1841
|
+
staleAfterMs
|
|
1842
|
+
};
|
|
1843
|
+
// No marker at all, or a store that would not answer. The canary has never
|
|
1844
|
+
// run, or cannot be read — either way nothing here has demonstrated that a
|
|
1845
|
+
// stranger can sign up, and this check exists to say so out loud.
|
|
1846
|
+
if (!marker || typeof marker.walkedAtMs !== 'number') {
|
|
1847
|
+
return _extends({}, base, {
|
|
1848
|
+
ok: false,
|
|
1849
|
+
code: 'canary-unavailable'
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
const ageMs = Math.max(0, nowMs - marker.walkedAtMs);
|
|
1853
|
+
const walkMs = typeof marker.elapsedMs === 'number' ? marker.elapsedMs : null;
|
|
1854
|
+
const reapedCleanly = typeof marker.reapedCleanly === 'boolean' ? marker.reapedCleanly : null;
|
|
1855
|
+
const failedStep = typeof marker.failedStep === 'string' ? marker.failedStep : null;
|
|
1856
|
+
const carried = _extends({}, base, {
|
|
1857
|
+
ageMs,
|
|
1858
|
+
walkMs,
|
|
1859
|
+
reapedCleanly,
|
|
1860
|
+
failedStep
|
|
1861
|
+
});
|
|
1862
|
+
// A stale PASS is not a pass. A walk that succeeded long enough ago says
|
|
1863
|
+
// nothing about the door now, and the whole point of a canary is currency.
|
|
1864
|
+
// Checked before the verdict so a stale failure reports as stale rather
|
|
1865
|
+
// than as a failure that may since have healed.
|
|
1866
|
+
if (ageMs > staleAfterMs) {
|
|
1867
|
+
return _extends({}, carried, {
|
|
1868
|
+
ok: false,
|
|
1869
|
+
code: 'canary-stale'
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
if (marker.ok !== true) {
|
|
1873
|
+
return _extends({}, carried, {
|
|
1874
|
+
ok: false,
|
|
1875
|
+
code: 'signup-walk-failed'
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
// Walked fine, did not clean up. Red anyway — see the docblock.
|
|
1879
|
+
if (reapedCleanly === false) {
|
|
1880
|
+
return _extends({}, carried, {
|
|
1881
|
+
ok: false,
|
|
1882
|
+
code: 'canary-left-residue'
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
return _extends({}, carried, {
|
|
1886
|
+
ok: true
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* How long metered page views may sit still before the edge is presumed shut.
|
|
1891
|
+
*
|
|
1892
|
+
* Twelve hours, and the width is deliberate rather than lazy.
|
|
1893
|
+
*
|
|
1894
|
+
* The quietest full day in the sampled week carried 57 views — about 2.4 an
|
|
1895
|
+
* hour on average, which would make six hours of zero look damning. But the
|
|
1896
|
+
* average is the wrong statistic here: measured 2026-09-10, the day's 62 views
|
|
1897
|
+
* came from ONE host of thirteen. Traffic that concentrated arrives in bursts,
|
|
1898
|
+
* and a burst distribution produces long empty stretches with nothing wrong.
|
|
1899
|
+
* A window sized to the average would page on one of them, which is how the
|
|
1900
|
+
* monitor that must never be ignored gets ignored.
|
|
1901
|
+
*
|
|
1902
|
+
* Twelve hours of exactly zero, on a platform serving 57 to 563 views a day,
|
|
1903
|
+
* is not a quiet stretch. It is slower to fire than anyone would like, and it
|
|
1904
|
+
* is the only thing that catches a fully blocked edge at all — nothing else
|
|
1905
|
+
* would ever fire, because every other check rides the bypass.
|
|
1906
|
+
*
|
|
1907
|
+
* ⚑ Tighten it once the real quiet-period distribution is known: the sampler
|
|
1908
|
+
* stamps `advancedAtMs` on every run, so a week of markers says what the true
|
|
1909
|
+
* maximum gap is. Do not tighten it from the daily average.
|
|
1910
|
+
*/ export const EDGE_ADMISSION_QUIET_AFTER_MS = 12 * 60 * 60 * 1000;
|
|
1911
|
+
/**
|
|
1912
|
+
* How old the sampler's own reading may be before it stops being evidence.
|
|
1913
|
+
*
|
|
1914
|
+
* Six hours, sized like `SIGNUP_CANARY_STALE_AFTER_MS` to a scheduler that
|
|
1915
|
+
* drops most of what it is asked for (AGL-2723) rather than to the cron
|
|
1916
|
+
* expression. Shorter than the quiet window on purpose: a stalled sampler
|
|
1917
|
+
* should be named as one BEFORE its frozen reading has had time to look like
|
|
1918
|
+
* an edge outage.
|
|
1919
|
+
*/ export const EDGE_ADMISSION_SAMPLE_STALE_AFTER_MS = 6 * 60 * 60 * 1000;
|
|
1920
|
+
/**
|
|
1921
|
+
* Grade the last recorded metered-traffic sample.
|
|
1922
|
+
*
|
|
1923
|
+
* Pure, like every sibling. The sampler decides when the total last MOVED and
|
|
1924
|
+
* stamps it; this only asks how long ago that was. Keeping the movement
|
|
1925
|
+
* detection in the writer is what lets the door stay a read: a day rollover
|
|
1926
|
+
* resets the counter to zero, and a grader looking at one sample could not
|
|
1927
|
+
* tell that from traffic stopping.
|
|
1928
|
+
*/ export function edgeAdmissionHealth(marker, ms, nowMs = Date.now(), quietAfterMs = EDGE_ADMISSION_QUIET_AFTER_MS, sampleStaleAfterMs = EDGE_ADMISSION_SAMPLE_STALE_AFTER_MS) {
|
|
1929
|
+
const base = {
|
|
1930
|
+
ms,
|
|
1931
|
+
quietMs: null,
|
|
1932
|
+
day: null,
|
|
1933
|
+
total: null,
|
|
1934
|
+
quietAfterMs,
|
|
1935
|
+
sampleAgeMs: null
|
|
1936
|
+
};
|
|
1937
|
+
// No marker, or one that cannot be read. Absence of proof reds here for the
|
|
1938
|
+
// same reason it does on every sibling: "nobody measured" and "traffic is
|
|
1939
|
+
// arriving" are the two readings this exists to keep apart.
|
|
1940
|
+
if (!marker || typeof marker.advancedAtMs !== 'number') {
|
|
1941
|
+
return _extends({}, base, {
|
|
1942
|
+
ok: false,
|
|
1943
|
+
code: 'edge-admission-unavailable'
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
const sampleAgeMs = typeof marker.sampledAtMs === 'number' ? Math.max(0, nowMs - marker.sampledAtMs) : null;
|
|
1947
|
+
const carried = _extends({}, base, {
|
|
1948
|
+
quietMs: Math.max(0, nowMs - marker.advancedAtMs),
|
|
1949
|
+
day: typeof marker.day === 'string' ? marker.day : null,
|
|
1950
|
+
total: typeof marker.total === 'number' ? marker.total : null,
|
|
1951
|
+
sampleAgeMs
|
|
1952
|
+
});
|
|
1953
|
+
// ⛔ CHECKED FIRST, AND THE ORDER IS THE POINT.
|
|
1954
|
+
//
|
|
1955
|
+
// `advancedAtMs` only moves when the sampler runs, so a sampler that has
|
|
1956
|
+
// stopped looks exactly like traffic that has stopped — and the responder
|
|
1957
|
+
// would go looking at the firewall while the real fault was a job that
|
|
1958
|
+
// never fired. That is the confusion `canary-stale` was built to avoid on
|
|
1959
|
+
// the check next door, and it costs one comparison to avoid here too.
|
|
1960
|
+
if (sampleAgeMs === null || sampleAgeMs > sampleStaleAfterMs) {
|
|
1961
|
+
return _extends({}, carried, {
|
|
1962
|
+
ok: false,
|
|
1963
|
+
code: 'edge-sample-stale'
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
if (carried.quietMs !== null && carried.quietMs > quietAfterMs) {
|
|
1967
|
+
return _extends({}, carried, {
|
|
1968
|
+
ok: false,
|
|
1969
|
+
code: 'edge-admits-nobody'
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
return _extends({}, carried, {
|
|
1973
|
+
ok: true
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
/** The service signup depends on. See the docblock for why it is the only one. */ export const APP_CHECK_GRADED_SERVICE = 'identitytoolkit.googleapis.com';
|
|
1977
|
+
/**
|
|
1978
|
+
* Window the attestation rate is taken over.
|
|
1979
|
+
*
|
|
1980
|
+
* Six hours, not one. This metric is aggregated by Google on its own schedule
|
|
1981
|
+
* and arrives late, so a one-hour window reads empty for reasons that have
|
|
1982
|
+
* nothing to do with attestation. Six hours is long enough to always carry
|
|
1983
|
+
* traffic on this platform and short enough that a real collapse is caught the
|
|
1984
|
+
* same working day.
|
|
1985
|
+
*/ export const APP_CHECK_WINDOW_MINUTES = 6 * 60;
|
|
1986
|
+
/**
|
|
1987
|
+
* Requests below which the rate is not graded.
|
|
1988
|
+
*
|
|
1989
|
+
* A ratio over three requests is noise wearing a percentage. Twenty is a
|
|
1990
|
+
* fraction of the ~940 a day Identity Platform sees, so a healthy platform
|
|
1991
|
+
* always clears it, and a platform quiet enough not to is a platform where
|
|
1992
|
+
* this check has nothing to say.
|
|
1993
|
+
*/ export const APP_CHECK_MIN_REQUESTS = 20;
|
|
1994
|
+
/**
|
|
1995
|
+
* The floor the allow rate must clear.
|
|
1996
|
+
*
|
|
1997
|
+
* 0.90, against a measured 0.997. The gap is deliberate: a handful of genuine
|
|
1998
|
+
* denials is ordinary — a stale tab, a bot, a browser that failed its
|
|
1999
|
+
* reCAPTCHA — and a floor set near the observed rate would page on those.
|
|
2000
|
+
*
|
|
2001
|
+
* What this is built to catch is not a drift of a few percent; it is the
|
|
2002
|
+
* documented failure mode of `recaptcha-allowlist.ts`, where an origin that is
|
|
2003
|
+
* attached and routed but not allowlisted renders a console that can never
|
|
2004
|
+
* sign anyone in. That reads as ~0%, not 85%.
|
|
2005
|
+
*/ export const APP_CHECK_MIN_ALLOW_RATE = 0.9;
|
|
2006
|
+
/**
|
|
2007
|
+
* Grade attestation for real visitors.
|
|
2008
|
+
*
|
|
2009
|
+
* Pure, like every sibling: something else reads Cloud Monitoring, this
|
|
2010
|
+
* decides. Null samples are degraded rather than calm — an unreadable metric
|
|
2011
|
+
* is not evidence that attestation works, and this check exists precisely
|
|
2012
|
+
* because "we did not look" was being reported as "it is fine".
|
|
2013
|
+
*/ export function appCheckAttestationHealth(samples, ms, service = APP_CHECK_GRADED_SERVICE, minimumRequests = APP_CHECK_MIN_REQUESTS, minimumAllowRate = APP_CHECK_MIN_ALLOW_RATE, windowMinutes = APP_CHECK_WINDOW_MINUTES) {
|
|
2014
|
+
const base = {
|
|
2015
|
+
ms,
|
|
2016
|
+
allowRate: null,
|
|
2017
|
+
allowed: 0,
|
|
2018
|
+
denied: 0,
|
|
2019
|
+
minimumRequests,
|
|
2020
|
+
minimumAllowRate,
|
|
2021
|
+
service,
|
|
2022
|
+
deniedBy: {},
|
|
2023
|
+
windowMinutes
|
|
2024
|
+
};
|
|
2025
|
+
if (samples === null) {
|
|
2026
|
+
return _extends({}, base, {
|
|
2027
|
+
ok: false,
|
|
2028
|
+
code: 'attestation-unavailable'
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
let allowed = 0;
|
|
2032
|
+
let denied = 0;
|
|
2033
|
+
const deniedBy = {};
|
|
2034
|
+
for (const sample of samples){
|
|
2035
|
+
if (sample.service !== service) continue;
|
|
2036
|
+
// `Number(...) || 0` for the reason every sibling gives: a corrupt count
|
|
2037
|
+
// must not turn the total into NaN, which compares false against every
|
|
2038
|
+
// threshold and would report calm forever.
|
|
2039
|
+
const count = Number(sample.count) || 0;
|
|
2040
|
+
if (sample.result === 'ALLOW') allowed += count;
|
|
2041
|
+
else if (sample.result === 'DENY') {
|
|
2042
|
+
var _deniedBy_why;
|
|
2043
|
+
denied += count;
|
|
2044
|
+
const why = sample.security || 'UNKNOWN';
|
|
2045
|
+
deniedBy[why] = ((_deniedBy_why = deniedBy[why]) != null ? _deniedBy_why : 0) + count;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
const total = allowed + denied;
|
|
2049
|
+
const carried = _extends({}, base, {
|
|
2050
|
+
allowed,
|
|
2051
|
+
denied,
|
|
2052
|
+
deniedBy
|
|
2053
|
+
});
|
|
2054
|
+
// Too little traffic to say anything. Reported, not hidden: an on-call
|
|
2055
|
+
// person reading a green body deserves to know this one abstained.
|
|
2056
|
+
if (total < minimumRequests) {
|
|
2057
|
+
return _extends({}, carried, {
|
|
2058
|
+
ok: true,
|
|
2059
|
+
code: 'not-enough-traffic',
|
|
2060
|
+
allowRate: null
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
const allowRate = allowed / total;
|
|
2064
|
+
return _extends({}, carried, {
|
|
2065
|
+
allowRate,
|
|
2066
|
+
ok: allowRate >= minimumAllowRate
|
|
2067
|
+
}, allowRate >= minimumAllowRate ? {} : {
|
|
2068
|
+
code: 'attestation-failing'
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
//# sourceMappingURL=health-report.js.map
|