@aglyn/aglyn 1.0.0-beta.221 → 1.0.0-beta.222
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/package.json +15 -11
- package/src/lib/app-utils/agent-openapi.js +1 -1
- package/src/lib/app-utils/agent-openapi.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +3 -2
- package/src/lib/app-utils/analytics-events.js +3 -2
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/api-plugins.d.ts +30 -0
- package/src/lib/app-utils/api-plugins.js +24 -1
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/author-html.d.ts +1 -1
- package/src/lib/app-utils/author-html.js.map +1 -1
- package/src/lib/app-utils/bandwidth-cap.d.ts +12 -1
- package/src/lib/app-utils/bandwidth-cap.js +17 -1
- package/src/lib/app-utils/bandwidth-cap.js.map +1 -1
- package/src/lib/app-utils/compose-reusable-components.d.ts +19 -0
- package/src/lib/app-utils/compose-reusable-components.js +116 -26
- package/src/lib/app-utils/compose-reusable-components.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +4 -0
- package/src/lib/app-utils/console-routes.js +3 -0
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +71 -0
- package/src/lib/app-utils/contacts.js +55 -0
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/contexts.d.ts +1 -0
- package/src/lib/app-utils/contexts.js +1 -0
- package/src/lib/app-utils/contexts.js.map +1 -1
- package/src/lib/app-utils/crm-email-templates.js +21 -4
- package/src/lib/app-utils/crm-email-templates.js.map +1 -1
- package/src/lib/app-utils/crm-kinds.d.ts +2 -0
- package/src/lib/app-utils/crm-kinds.js +1 -0
- package/src/lib/app-utils/crm-kinds.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +1199 -67
- package/src/lib/app-utils/crm.js +1951 -310
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/csv-import.d.ts +10 -38
- package/src/lib/app-utils/csv-import.js +17 -42
- package/src/lib/app-utils/csv-import.js.map +1 -1
- package/src/lib/app-utils/csv.d.ts +32 -11
- package/src/lib/app-utils/csv.js +43 -5
- package/src/lib/app-utils/csv.js.map +1 -1
- package/src/lib/app-utils/default-site.d.ts +116 -0
- package/src/lib/app-utils/default-site.js +1024 -0
- package/src/lib/app-utils/default-site.js.map +1 -0
- package/src/lib/app-utils/docs-help.generated.d.ts +22 -22
- package/src/lib/app-utils/docs-help.generated.js +44 -10
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +738 -150
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/draft-placeholders.d.ts +76 -0
- package/src/lib/app-utils/draft-placeholders.js +115 -0
- package/src/lib/app-utils/draft-placeholders.js.map +1 -0
- package/src/lib/app-utils/entity-picker-context.d.ts +19 -15
- package/src/lib/app-utils/entity-picker-context.js +4 -4
- package/src/lib/app-utils/entity-picker-context.js.map +1 -1
- package/src/lib/app-utils/expand-repeatables.d.ts +66 -10
- package/src/lib/app-utils/expand-repeatables.js +88 -14
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/forms.d.ts +33 -22
- package/src/lib/app-utils/forms.js +47 -28
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/full-use-cost.d.ts +277 -0
- package/src/lib/app-utils/full-use-cost.js +421 -0
- package/src/lib/app-utils/full-use-cost.js.map +1 -0
- package/src/lib/app-utils/health-report.js +11 -0
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/llms-txt.d.ts +20 -0
- package/src/lib/app-utils/llms-txt.js +7 -1
- package/src/lib/app-utils/llms-txt.js.map +1 -1
- package/src/lib/app-utils/media-asset-facts.d.ts +16 -1
- package/src/lib/app-utils/media-asset-facts.js +40 -13
- package/src/lib/app-utils/media-asset-facts.js.map +1 -1
- package/src/lib/app-utils/media-bandwidth.d.ts +52 -0
- package/src/lib/app-utils/media-bandwidth.js +94 -0
- package/src/lib/app-utils/media-bandwidth.js.map +1 -0
- package/src/lib/app-utils/media-ref.d.ts +241 -33
- package/src/lib/app-utils/media-ref.js +324 -43
- package/src/lib/app-utils/media-ref.js.map +1 -1
- package/src/lib/app-utils/media-src.d.ts +8 -8
- package/src/lib/app-utils/media-src.js +27 -11
- package/src/lib/app-utils/media-src.js.map +1 -1
- package/src/lib/app-utils/media-takedown-reach.d.ts +4 -1
- package/src/lib/app-utils/media-takedown-reach.js +6 -3
- package/src/lib/app-utils/media-takedown-reach.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +1 -1
- package/src/lib/app-utils/notifications.js +2 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/organizations.d.ts +51 -2
- package/src/lib/app-utils/organizations.js +56 -3
- package/src/lib/app-utils/organizations.js.map +1 -1
- package/src/lib/app-utils/page-record-sources.d.ts +87 -0
- package/src/lib/app-utils/page-record-sources.js +53 -0
- package/src/lib/app-utils/page-record-sources.js.map +1 -0
- package/src/lib/app-utils/paid-media-source.d.ts +3 -7
- package/src/lib/app-utils/paid-media-source.js +5 -19
- package/src/lib/app-utils/paid-media-source.js.map +1 -1
- package/src/lib/app-utils/person-erasure.d.ts +5 -2
- package/src/lib/app-utils/person-erasure.js.map +1 -1
- package/src/lib/app-utils/picklists.d.ts +187 -0
- package/src/lib/app-utils/picklists.js +320 -0
- package/src/lib/app-utils/picklists.js.map +1 -0
- package/src/lib/app-utils/plan-entitlements.d.ts +220 -50
- package/src/lib/app-utils/plan-entitlements.js +309 -115
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/realm-host-surface.generated.js +2 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/release-flags.d.ts +1 -1
- package/src/lib/app-utils/release-flags.js +11 -0
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/rendered-widths.d.ts +121 -0
- package/src/lib/app-utils/rendered-widths.js +211 -0
- package/src/lib/app-utils/rendered-widths.js.map +1 -0
- package/src/lib/app-utils/repeat-sources.d.ts +2 -1
- package/src/lib/app-utils/repeat-sources.js.map +1 -1
- package/src/lib/app-utils/screen-route.d.ts +154 -36
- package/src/lib/app-utils/screen-route.js +185 -40
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/seo-audit.d.ts +11 -1
- package/src/lib/app-utils/seo-audit.js +33 -13
- package/src/lib/app-utils/seo-audit.js.map +1 -1
- package/src/lib/app-utils/seo-keywords.d.ts +11 -0
- package/src/lib/app-utils/seo-keywords.js +19 -5
- package/src/lib/app-utils/seo-keywords.js.map +1 -1
- package/src/lib/app-utils/seo-page-facts.d.ts +47 -4
- package/src/lib/app-utils/seo-page-facts.js +136 -31
- package/src/lib/app-utils/seo-page-facts.js.map +1 -1
- package/src/lib/app-utils/seo-site-scan.d.ts +42 -4
- package/src/lib/app-utils/seo-site-scan.js +113 -12
- package/src/lib/app-utils/seo-site-scan.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +5 -4
- package/src/lib/app-utils/server.js +14 -5
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-icon-set.d.ts +195 -0
- package/src/lib/app-utils/site-icon-set.js +351 -0
- package/src/lib/app-utils/site-icon-set.js.map +1 -0
- package/src/lib/app-utils/site-interactions.d.ts +101 -4
- package/src/lib/app-utils/site-interactions.js +120 -4
- package/src/lib/app-utils/site-interactions.js.map +1 -1
- package/src/lib/app-utils/starter-template-nodes.d.ts +7 -0
- package/src/lib/app-utils/starter-template-nodes.js +3 -0
- package/src/lib/app-utils/starter-template-nodes.js.map +1 -1
- package/src/lib/app-utils/starter-templates.d.ts +5 -19
- package/src/lib/app-utils/starter-templates.js +5 -49
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/transfer-launcher-context.d.ts +136 -0
- package/src/lib/app-utils/transfer-launcher-context.js +23 -0
- package/src/lib/app-utils/transfer-launcher-context.js.map +1 -0
- package/src/lib/app-utils/transfer-resume-import.d.ts +39 -0
- package/src/lib/app-utils/transfer-resume-import.js +128 -0
- package/src/lib/app-utils/transfer-resume-import.js.map +1 -0
- package/src/lib/app-utils/upgrade-proposal.d.ts +66 -0
- package/src/lib/app-utils/upgrade-proposal.js +95 -0
- package/src/lib/app-utils/upgrade-proposal.js.map +1 -0
- package/src/lib/app-utils/utc-month.d.ts +33 -0
- package/src/lib/app-utils/utc-month.js +35 -0
- package/src/lib/app-utils/utc-month.js.map +1 -0
- package/src/lib/app-utils/visitor-record-ceiling.d.ts +4 -4
- package/src/lib/app-utils/visitor-record-ceiling.js +4 -4
- package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
- package/src/lib/app-utils/wire-nodes.d.ts +39 -19
- package/src/lib/app-utils/wire-nodes.js +120 -34
- package/src/lib/app-utils/wire-nodes.js.map +1 -1
- package/src/lib/data-transfer/access.d.ts +74 -0
- package/src/lib/data-transfer/access.js +93 -0
- package/src/lib/data-transfer/access.js.map +1 -0
- package/src/lib/data-transfer/derive.d.ts +246 -0
- package/src/lib/data-transfer/derive.js +1007 -0
- package/src/lib/data-transfer/derive.js.map +1 -0
- package/src/lib/data-transfer/export-file.d.ts +46 -0
- package/src/lib/data-transfer/export-file.js +146 -0
- package/src/lib/data-transfer/export-file.js.map +1 -0
- package/src/lib/data-transfer/field-catalog.d.ts +134 -0
- package/src/lib/data-transfer/field-catalog.js +278 -0
- package/src/lib/data-transfer/field-catalog.js.map +1 -0
- package/src/lib/data-transfer/header-match.d.ts +129 -0
- package/src/lib/data-transfer/header-match.js +367 -0
- package/src/lib/data-transfer/header-match.js.map +1 -0
- package/src/lib/data-transfer/index.d.ts +34 -0
- package/src/lib/data-transfer/index.js +40 -0
- package/src/lib/data-transfer/index.js.map +1 -0
- package/src/lib/data-transfer/job.d.ts +190 -0
- package/src/lib/data-transfer/job.js +229 -0
- package/src/lib/data-transfer/job.js.map +1 -0
- package/src/lib/data-transfer/lookup.d.ts +45 -0
- package/src/lib/data-transfer/lookup.js +71 -0
- package/src/lib/data-transfer/lookup.js.map +1 -0
- package/src/lib/data-transfer/match.d.ts +99 -0
- package/src/lib/data-transfer/match.js +244 -0
- package/src/lib/data-transfer/match.js.map +1 -0
- package/src/lib/data-transfer/package-plan.d.ts +205 -0
- package/src/lib/data-transfer/package-plan.js +475 -0
- package/src/lib/data-transfer/package-plan.js.map +1 -0
- package/src/lib/data-transfer/package.d.ts +181 -0
- package/src/lib/data-transfer/package.js +426 -0
- package/src/lib/data-transfer/package.js.map +1 -0
- package/src/lib/data-transfer/picklist-map.d.ts +139 -0
- package/src/lib/data-transfer/picklist-map.js +271 -0
- package/src/lib/data-transfer/picklist-map.js.map +1 -0
- package/src/lib/data-transfer/plan.d.ts +176 -0
- package/src/lib/data-transfer/plan.js +451 -0
- package/src/lib/data-transfer/plan.js.map +1 -0
- package/src/lib/data-transfer/policy.d.ts +170 -0
- package/src/lib/data-transfer/policy.js +378 -0
- package/src/lib/data-transfer/policy.js.map +1 -0
- package/src/lib/data-transfer/resource.d.ts +211 -0
- package/src/lib/data-transfer/resource.js +241 -0
- package/src/lib/data-transfer/resource.js.map +1 -0
- package/src/lib/data-transfer/review.d.ts +70 -0
- package/src/lib/data-transfer/review.js +233 -0
- package/src/lib/data-transfer/review.js.map +1 -0
- package/src/lib/data-transfer/similarity.d.ts +40 -0
- package/src/lib/data-transfer/similarity.js +106 -0
- package/src/lib/data-transfer/similarity.js.map +1 -0
- package/src/lib/data-transfer/site-package.d.ts +365 -0
- package/src/lib/data-transfer/site-package.js +1077 -0
- package/src/lib/data-transfer/site-package.js.map +1 -0
- package/src/lib/data-transfer/source.d.ts +74 -0
- package/src/lib/data-transfer/source.js +293 -0
- package/src/lib/data-transfer/source.js.map +1 -0
- package/src/lib/data-transfer/transfer-api.d.ts +898 -0
- package/src/lib/data-transfer/transfer-api.js +107 -0
- package/src/lib/data-transfer/transfer-api.js.map +1 -0
- package/src/lib/foundation/definitions/org-billing.types.d.ts +31 -0
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +42 -1
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +6 -4
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/foundation/index.d.ts +1 -1
- package/src/lib/foundation/index.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +91 -2
- package/src/lib/plugin-manager/feature-plugins.js +48 -3
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +50 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +1129 -28
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/index.d.ts +0 -1
- package/src/lib/plugin-manager/index.js +0 -1
- package/src/lib/plugin-manager/index.js.map +1 -1
- package/src/lib/plugin-manager/interaction-recipes.d.ts +142 -0
- package/src/lib/plugin-manager/interaction-recipes.js +104 -0
- package/src/lib/plugin-manager/interaction-recipes.js.map +1 -0
- package/src/lib/plugin-manager/interaction-step-checks.d.ts +66 -0
- package/src/lib/plugin-manager/interaction-step-checks.js +86 -0
- package/src/lib/plugin-manager/interaction-step-checks.js.map +1 -0
- package/src/lib/plugin-manager/plugin-artifact-types.d.ts +235 -0
- package/src/lib/plugin-manager/plugin-artifact-types.js +99 -0
- package/src/lib/plugin-manager/plugin-artifact-types.js.map +1 -0
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +8 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-conversion-credit.d.ts +216 -0
- package/src/lib/plugin-manager/plugin-conversion-credit.js +91 -0
- package/src/lib/plugin-manager/plugin-conversion-credit.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entity-pickers.d.ts +83 -0
- package/src/lib/plugin-manager/plugin-entity-pickers.js +87 -0
- package/src/lib/plugin-manager/plugin-entity-pickers.js.map +1 -0
- package/src/lib/plugin-manager/plugin-host-resources.d.ts +6 -0
- package/src/lib/plugin-manager/plugin-host-resources.js +6 -1
- package/src/lib/plugin-manager/plugin-host-resources.js.map +1 -1
- package/src/lib/plugin-manager/plugin-intake-gates.d.ts +75 -0
- package/src/lib/plugin-manager/plugin-intake-gates.js +49 -0
- package/src/lib/plugin-manager/plugin-intake-gates.js.map +1 -0
- package/src/lib/plugin-manager/plugin-jobs.d.ts +5 -5
- package/src/lib/plugin-manager/plugin-jobs.js +7 -7
- package/src/lib/plugin-manager/plugin-jobs.js.map +1 -1
- package/src/lib/plugin-manager/plugin-live-paths.d.ts +70 -0
- package/src/lib/plugin-manager/plugin-live-paths.js +66 -0
- package/src/lib/plugin-manager/plugin-live-paths.js.map +1 -0
- package/src/lib/plugin-manager/plugin-org-erasure.d.ts +3 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js +30 -5
- package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -1
- package/src/lib/plugin-manager/plugin-person-erasure.d.ts +68 -9
- package/src/lib/plugin-manager/plugin-person-erasure.js +196 -51
- package/src/lib/plugin-manager/plugin-person-erasure.js.map +1 -1
- package/src/lib/plugin-manager/plugin-person-records.d.ts +275 -0
- package/src/lib/plugin-manager/plugin-person-records.js +107 -0
- package/src/lib/plugin-manager/plugin-person-records.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-counts.d.ts +76 -0
- package/src/lib/plugin-manager/plugin-record-counts.js +49 -0
- package/src/lib/plugin-manager/plugin-record-counts.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-email-state.d.ts +26 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js +16 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-index.d.ts +10 -0
- package/src/lib/plugin-manager/plugin-record-index.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-lists.d.ts +86 -6
- package/src/lib/plugin-manager/plugin-record-lists.js +42 -5
- package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-origin.d.ts +79 -0
- package/src/lib/plugin-manager/plugin-record-origin.js +46 -0
- package/src/lib/plugin-manager/plugin-record-origin.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-pages.d.ts +37 -0
- package/src/lib/plugin-manager/plugin-record-pages.js +52 -0
- package/src/lib/plugin-manager/plugin-record-pages.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-timeline.d.ts +60 -0
- package/src/lib/plugin-manager/plugin-record-timeline.js +16 -0
- package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
- package/src/lib/plugin-manager/plugin-resource-drafts.d.ts +5 -5
- package/src/lib/plugin-manager/plugin-resource-drafts.js.map +1 -1
- package/src/lib/plugin-manager/plugin-revocations.d.ts +64 -0
- package/src/lib/plugin-manager/plugin-revocations.js +47 -0
- package/src/lib/plugin-manager/plugin-revocations.js.map +1 -0
- package/src/lib/plugin-manager/plugin-send-tallies.d.ts +69 -0
- package/src/lib/plugin-manager/plugin-send-tallies.js +45 -0
- package/src/lib/plugin-manager/plugin-send-tallies.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-bundle.d.ts +40 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js +4 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-export.d.ts +57 -1
- package/src/lib/plugin-manager/plugin-site-export.js +23 -10
- package/src/lib/plugin-manager/plugin-site-export.js.map +1 -1
- package/src/lib/plugin-manager/plugin-sitemap-readers.d.ts +146 -0
- package/src/lib/plugin-manager/plugin-sitemap-readers.js +110 -0
- package/src/lib/plugin-manager/plugin-sitemap-readers.js.map +1 -0
- package/src/lib/plugin-manager/plugin-transfer-resources.d.ts +714 -0
- package/src/lib/plugin-manager/plugin-transfer-resources.js +443 -0
- package/src/lib/plugin-manager/plugin-transfer-resources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +54 -5
- package/src/lib/plugin-manager/plugin-usage-axes.js +11 -3
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -1
- package/src/lib/plugin-manager/plugin-user-erasure.js +9 -1
- package/src/lib/plugin-manager/plugin-user-erasure.js.map +1 -1
- package/src/lib/plugin-manager/plugin-visitor-doors.d.ts +104 -0
- package/src/lib/plugin-manager/plugin-visitor-doors.js +108 -0
- package/src/lib/plugin-manager/plugin-visitor-doors.js.map +1 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +2 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
- package/src/lib/plugin-manager/repeat-rows.d.ts +5 -6
- package/src/lib/plugin-manager/repeat-rows.js.map +1 -1
- package/src/lib/app-utils/actions.d.ts +0 -310
- package/src/lib/app-utils/actions.js +0 -506
- package/src/lib/app-utils/actions.js.map +0 -1
- package/src/lib/app-utils/automation-placeholders.d.ts +0 -72
- package/src/lib/app-utils/automation-placeholders.js +0 -207
- package/src/lib/app-utils/automation-placeholders.js.map +0 -1
- package/src/lib/app-utils/dataset-models.d.ts +0 -339
- package/src/lib/app-utils/dataset-models.js +0 -722
- package/src/lib/app-utils/dataset-models.js.map +0 -1
- package/src/lib/app-utils/datasets.d.ts +0 -171
- package/src/lib/app-utils/datasets.js +0 -227
- package/src/lib/app-utils/datasets.js.map +0 -1
- package/src/lib/app-utils/form-abuse-ceiling.d.ts +0 -146
- package/src/lib/app-utils/form-abuse-ceiling.js +0 -158
- package/src/lib/app-utils/form-abuse-ceiling.js.map +0 -1
- package/src/lib/plugin-manager/order-fulfilment.d.ts +0 -142
- package/src/lib/plugin-manager/order-fulfilment.js +0 -104
- package/src/lib/plugin-manager/order-fulfilment.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/author-html.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * An ISOMORPHIC sanitizer for AUTHOR-WRITTEN HTML (AGL-1901).\n *\n * ## Why this exists\n *\n * Rich text (`typography.tsx`'s `html` prop) and Custom HTML\n * (`custom-html.tsx`) are the two places a site author's raw markup reaches\n * `dangerouslySetInnerHTML`. Both sanitized with DOMPurify, which needs a\n * real DOM, so both deferred the sanitize into a `useEffect` — server render\n * and first client render agreed only because BOTH were empty (AGL-1268).\n * That fixed hydration and left every rich-text body and every Custom HTML\n * block absent from the server response: a crawler that does not run JS saw\n * nothing, on a product sold on \"build a site and be found\".\n *\n * AGL-1901 named two ways out. Isomorphic DOMPurify over jsdom keeps one\n * engine but puts jsdom in the tenant SERVER bundle, landing on the open\n * cold-start 502s (AGL-1152). A second sanitizer on the server is light but\n * re-encodes the policy in a second engine, and any drift between the two\n * becomes a stored-XSS hole rather than a rendering bug.\n *\n * This module takes the second shape and DELETES the drift risk instead of\n * managing it: it is the ONLY sanitizer on the render path, on both sides.\n * The server renders `sanitizeAuthorHtml(html)`; the first client render\n * calls the same pure function on the same string and gets the same bytes,\n * so hydration is clean by construction and there is no second engine to\n * drift from. DOMPurify remains the reference the parity spec measures this\n * against (`author-html-dompurify-parity.spec.ts`), not a second gate at\n * runtime — a runtime second pass would be worthless anyway, because the\n * server string is parsed and executed by the browser BEFORE any client code\n * runs.\n *\n * The safety argument is therefore a SUBSET argument, and the parity spec\n * asserts it: every tag and every attribute this emits is one DOMPurify\n * would also have kept under the same config. Where the two differ, this one\n * keeps less. Keeping less can only cost an author some markup; keeping more\n * would be the hole.\n *\n * ## How it is safe without a DOM\n *\n * A source-level sanitizer is only sound if the string it emits re-parses to\n * the tree it thinks it built — the mXSS class. Four rules make that true\n * here, and each is load-bearing:\n *\n * 1. **No foreign content, ever.** `<svg>` and `<math>` switch the parser\n * into a namespace with different tokenization rules; they are dropped\n * with their contents, as are `<template>`, `<noscript>`, `<noembed>`,\n * `<xmp>` and `<plaintext>`, whose contents parse under rules of their\n * own. Nothing this emits can re-enter foreign or raw-text mode.\n * 2. **Everything is re-serialized, never passed through.** Text is emitted\n * with `&`, `<` and `>` escaped; attribute values are always double\n * quoted with `&`, `<`, `>` and `\"` escaped. There are no unquoted\n * attribute values and no raw `<` in text for a re-parse to misread.\n * 3. **Character references are decoded BEFORE any value is judged.** A\n * tokenizer that reads source rather than a DOM is otherwise trivially\n * defeated by `href=\"javascript:alert(1)\"`, which the browser\n * decodes and this would not. {@link decodeCharacterReferences} handles\n * every numeric reference generically, plus the named references that can\n * contribute a colon or whitespace to a scheme. ASCII letters have NO\n * named references in HTML, so no named reference this does not know can\n * help spell a scheme; only numeric ones could, and those are all\n * decoded. The decoded value is what is validated AND what is re-escaped\n * on the way out, so the browser sees exactly the string that was judged.\n * 4. **Allowlists, not denylists**, for elements and for attributes, with\n * every URL-bearing attribute scheme-checked.\n *\n * ## Relationship to the CSS rule\n *\n * `style` attributes go through {@link sanitizeAuthorCss} exactly as the\n * per-instance DOMPurify hook AGL-1725 added did, because DOMPurify performs\n * no CSS filtering of its own. A `<style>` ELEMENT is dropped rather than\n * filtered, which is what both DOMPurify configs already did — the `css`\n * component attribute remains the only route to a style block on a page.\n */\n\nimport { sanitizeAuthorCss } from './author-css'\n\n/**\n * Elements that survive: HTML flow and phrasing content, a strict subset of\n * DOMPurify's `USE_PROFILES: { html: true }` set minus the tags both callers\n * already forbade (`script`, `iframe`, `object`, `embed`, `form`, `base`)\n * and minus every interactive form control, which no author markup on a\n * rendered page has a use for and which carries `formaction` shapes.\n */\nexport const ALLOWED_AUTHOR_HTML_ELEMENTS: ReadonlySet<string> = new Set([\n 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',\n 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'data',\n 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption',\n 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup',\n // `main` is a deliberate subtraction from the DOMPurify profile (AGL-2486):\n // a published page carries exactly one `main` landmark, placed by\n // composition on the page's content region, so author markup carrying\n // another can only make the landmark ambiguous. It is\n // UNWRAPPED like any other unlisted tag, not dropped, so the author's content\n // survives verbatim and only the competing landmark goes.\n 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'mark', 'nav', 'ol', 'p',\n 'picture', 'pre', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'small',\n 'source', 'span', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody',\n 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'u', 'ul', 'var',\n 'video', 'wbr',\n])\n\n/**\n * The subset of {@link ALLOWED_AUTHOR_HTML_ELEMENTS} that may appear inside a\n * control — a `<button>` or an `<a>` (AGL-2557).\n *\n * Both have a content model of phrasing content with NO interactive\n * descendant. Serving a `<ul>`, a `<div>` or a nested `<a>` inside one is not\n * a styling problem: the parser closes the control early and promotes the\n * rest to a sibling, so React hydrates against a tree it did not describe and\n * reports the mismatch — the same shape `authorHtmlBreaksContainer` exists to\n * head off for a `<p>`. A nested control is also unreachable by keyboard and\n * announces as neither of the two things it is.\n *\n * Narrowing keeps LESS than the full allowlist, so the subset argument in\n * this module's header still holds: everything emitted under it is something\n * the full policy would also have kept. `img` is out with the rest of the\n * embedded content — an author who wants an icon on a button has the icon\n * fields for it, and nothing in the inline editor can produce one.\n */\nexport const INLINE_AUTHOR_HTML_ELEMENTS: ReadonlySet<string> = new Set([\n 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'del', 'dfn',\n 'em', 'i', 'ins', 'kbd', 'mark', 'q', 'rp', 'rt', 'ruby', 's', 'samp',\n 'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr',\n])\n\n/**\n * Void elements — emitted WITHOUT a closing tag and never pushed on the open\n * stack. A void element that carries children is not merely wrong markup\n * here: `<img>` with children is an SSR 500 in this codebase, so the\n * serializer must be structurally incapable of producing one.\n */\nconst VOID_ELEMENTS = new Set([\n 'br', 'col', 'hr', 'img', 'source', 'track', 'wbr',\n])\n\n/**\n * Elements dropped WITH their contents rather than unwrapped.\n *\n * Two reasons, both about the parser rather than about the tag: raw-text and\n * escapable-raw-text elements (`script`, `style`, `textarea`, `title`,\n * `xmp`, `plaintext`, `noscript`, `noembed`, `noframes`, `iframe`) tokenize\n * their contents under different rules, and foreign-content roots (`svg`,\n * `math`) switch namespace. Unwrapping either would hand the re-parse a\n * string this never modelled.\n *\n * `<style>` is in this set for BOTH callers, because both DOMPurify configs\n * dropped it: `typography.tsx` listed it in `FORBID_TAGS` and the `html`\n * profile does not carry it, which `custom-html-author-css.spec.tsx` pins.\n * The `css` component attribute stays the only route to a style block, and\n * it goes through {@link sanitizeAuthorCss} where it always did.\n */\nconst CONTENT_DROPPING = new Set([\n 'script', 'style', 'textarea', 'title', 'xmp', 'plaintext', 'noscript',\n 'noembed', 'noframes', 'iframe', 'svg', 'math', 'template', 'object',\n 'applet', 'frame', 'frameset', 'canvas',\n])\n\n/** Attributes any allowed element may carry. */\nexport const GLOBAL_AUTHOR_HTML_ATTRIBUTES: ReadonlySet<string> = new Set([\n 'class', 'id', 'title', 'lang', 'dir', 'style', 'role',\n])\n\n/** Attributes allowed only on the elements that define them. */\nexport const AUTHOR_HTML_ELEMENT_ATTRIBUTES: Readonly<Record<string, ReadonlySet<string>>> = {\n // No `target`. DOMPurify's html profile drops it, so both callers already\n // dropped it, and the parity spec measures this table against that profile\n // — putting it back here would be this sanitizer keeping MORE than the\n // policy it replaced, which is the one direction that is not allowed to\n // happen quietly. Giving authors `target=\"_blank\"` is a product decision\n // about the allowlist, not a side effect of moving the sanitizer.\n a: new Set(['href', 'rel', 'name', 'download', 'hreflang', 'type']),\n audio: new Set(['src', 'controls', 'loop', 'muted', 'preload']),\n blockquote: new Set(['cite']),\n col: new Set(['span']),\n colgroup: new Set(['span']),\n data: new Set(['value']),\n del: new Set(['cite', 'datetime']),\n details: new Set(['open']),\n img: new Set([\n 'src', 'srcset', 'alt', 'width', 'height', 'loading', 'decoding', 'sizes',\n ]),\n ins: new Set(['cite', 'datetime']),\n li: new Set(['value']),\n ol: new Set(['start', 'reversed', 'type']),\n q: new Set(['cite']),\n source: new Set(['src', 'srcset', 'type', 'media', 'sizes', 'width', 'height']),\n td: new Set(['colspan', 'rowspan', 'headers']),\n // No `abbr`: DOMPurify's html profile drops it on `th`, same reason as\n // `target` on `a` — this table may not be wider than the policy it replaced.\n th: new Set(['colspan', 'rowspan', 'headers', 'scope']),\n time: new Set(['datetime']),\n track: new Set(['src', 'kind', 'srclang', 'label', 'default']),\n video: new Set([\n 'src', 'poster', 'controls', 'loop', 'muted', 'preload', 'playsinline',\n 'width', 'height',\n ]),\n}\n\n/**\n * Start tags that force an open `<p>` closed (HTML \"in body\", the\n * `close a p element` clause).\n *\n * These exist here because the emitted string has to be a FIXED POINT of the\n * parser. A crawler and a browser both read the server response through the\n * document parser, which reparents `<p>a<div>b</div></p>` into\n * `<p>a</p><div>b</div><p></p>` — and React, hydrating, compares the\n * container's `innerHTML` to the `__html` it was handed and reports the\n * difference as a mismatch. Emitting the implied end tag ourselves is what\n * makes \"what we wrote\" and \"what the browser will hold\" the same string.\n * That is not a theoretical tidiness: it is the hydration regression this\n * change would otherwise have traded the SSR gap for.\n */\nconst CLOSES_OPEN_P = new Set([\n 'address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl', 'dd',\n 'dt', 'figcaption', 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n 'header', 'hgroup', 'hr', 'li', 'main', 'nav', 'ol', 'p', 'pre', 'section',\n 'summary', 'table', 'ul',\n])\n\n/**\n * Elements whose children the parser accepts ONLY as table parts. Text and\n * ordinary elements written here are foster-parented OUT of the table, which\n * is a reparenting this cannot express — so they are dropped instead, and\n * the emitted string round-trips.\n */\nconst TABLE_CONTEXTS = new Set(['table', 'thead', 'tbody', 'tfoot', 'tr'])\n\n/** Elements the parser only accepts inside a table. */\nconst TABLE_PARTS = new Set([\n 'caption', 'colgroup', 'col', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',\n])\n\n/** Attributes whose value is a URL and must clear the scheme rule. */\nconst URL_ATTRIBUTES = new Set(['href', 'src', 'poster', 'cite'])\n/** Attributes whose value is a comma-separated candidate list of URLs. */\nconst URL_LIST_ATTRIBUTES = new Set(['srcset'])\n\n/**\n * Schemes a URL attribute may name. Mirrors DOMPurify's default\n * `ALLOWED_URI_REGEXP` minus `javascript:`-adjacent shapes it also refuses;\n * `data:` is handled separately because it is only safe on the media\n * attributes and only for raster types.\n */\nconst ALLOWED_SCHEMES = new Set([\n 'http', 'https', 'ftp', 'ftps', 'mailto', 'tel', 'callto', 'sms', 'cid',\n 'xmpp',\n])\n\n/** Tag/attribute pairs on which a `data:` URL is considered at all. */\nconst DATA_URI_ATTRIBUTES = new Set(['img:src', 'img:srcset', 'source:src',\n 'source:srcset', 'video:src', 'video:poster', 'audio:src'])\n\n/**\n * `data:` payloads accepted on those pairs: raster images only.\n *\n * `image/svg+xml` is deliberately absent. An SVG document can carry\n * `<script>`, and a `data:` SVG in an `img` is same-origin-ish enough in\n * enough engines that treating it as an image is a bet this does not need to\n * take — the author has real image hosting.\n */\nconst DATA_URI_PAYLOAD =\n /^data:image\\/(?:png|jpe?g|gif|webp|avif|bmp|x-icon)\\s*;\\s*base64\\s*,/i\n\n/**\n * Named character references this decodes, beyond the numeric forms.\n *\n * The list is short on purpose and the reasoning is in the module header:\n * HTML has no named reference for an ASCII letter, so no reference outside\n * this table can contribute to spelling `javascript`. What a reference CAN\n * contribute is the colon and the leading whitespace a browser tolerates\n * before a scheme, so those are the ones that must be understood rather than\n * passed through.\n */\nconst NAMED_REFERENCES: Record<string, string> = {\n amp: '&', AMP: '&', lt: '<', LT: '<', gt: '>', GT: '>', quot: '\"',\n QUOT: '\"', apos: \"'\", nbsp: ' ', colon: ':', semi: ';', sol: '/',\n bsol: '\\\\', num: '#', period: '.', comma: ',', excl: '!', quest: '?',\n lpar: '(', rpar: ')', Tab: '\\t', NewLine: '\\n', equals: '=', ast: '*',\n commat: '@', dollar: '$', percnt: '%', plus: '+', minus: '-', lowbar: '_',\n}\n\n/**\n * Decodes character references in an attribute value the way a browser will,\n * so the value can be JUDGED as the browser will see it.\n *\n * Numeric references are decoded generically — that is the form that can\n * produce any character at all, and the only form that can spell a scheme.\n * Named references are decoded from {@link NAMED_REFERENCES}; anything else\n * is left verbatim, which is safe precisely because it cannot become a\n * letter or a colon. Everything decoded here is re-escaped on output, so the\n * emitted attribute value and the validated value are the same string.\n */\nexport function decodeCharacterReferences(value: string): string {\n if (!value.includes('&')) return value\n return value.replace(\n /&(?:#([0-9]{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z][a-zA-Z0-9]{1,31}))(;?)/g,\n (match, dec: string, hex: string, name: string) => {\n if (dec !== undefined || hex !== undefined) {\n const code = dec !== undefined ? parseInt(dec, 10) : parseInt(hex, 16)\n // Surrogates and out-of-range values have no scalar; dropping them\n // is safer than inventing a replacement a re-parse would disagree\n // with.\n if (!Number.isFinite(code) || code <= 0 || code > 0x10ffff) return ''\n if (code >= 0xd800 && code <= 0xdfff) return ''\n return String.fromCodePoint(code)\n }\n const named = NAMED_REFERENCES[name]\n return named === undefined ? match : named\n },\n )\n}\n\n/**\n * Escapes text content EXACTLY as the HTML serialization algorithm does.\n *\n * The byte-for-byte match is the requirement, not a nicety: React compares\n * the container's `innerHTML` against the `__html` it was given when it\n * hydrates, so a string that merely PARSES the same but SERIALIZES\n * differently is reported as a hydration mismatch. `escapeAuthorHtmlText`\n * and {@link escapeAttributeValue} together are the \"serialize\" half of the\n * fixed point `author-html-round-trip.spec.ts` asserts.\n */\nfunction escapeText(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/\\u00a0/g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n}\n\n/**\n * Escapes a decoded attribute value for a double-quoted slot.\n *\n * Deliberately does NOT escape `<` or `>`: the serialization algorithm\n * escapes only `&`, U+00A0 and `\"` in an attribute value, and inside a\n * quoted value a raw `<` is literal to the parser, so escaping it would be\n * safe but would not round-trip.\n *\n * That is also why this is NOT `@aglyn/shared-util-tools/escape-html`, which\n * every other string-built HTML surface in the repo now shares. That one\n * escapes the five characters a SAFE escaper needs; this one has to emit\n * exactly the characters a browser's serializer emits and no others. Pointing\n * it at the shared escaper was measured (AGL-2706): the `<` inside a quoted\n * href comes back `<`, and `author-html-round-trip.spec.tsx` fails on the\n * fixed point — the string no longer re-serializes to itself, which is the\n * hydration mismatch this pair exists to prevent. U+00A0 is the same argument\n * in the other direction: the shared escaper leaves it alone, the serializer\n * writes ` `.\n */\nfunction escapeAttribute(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/\\u00a0/g, ' ')\n .replace(/\"/g, '"')\n}\n\n/**\n * Whether a decoded URL clears the scheme rule for this tag/attribute.\n *\n * The comparison is made on a copy with every C0 control and space removed\n * and folded to lower case, because browsers strip exactly those while\n * resolving a scheme — `\"java\\tscript:alert(1)\"` navigates.\n *\n * Exported because the sanitizer is not the only reader of author markup\n * (AGL-2740). The Markdown representation of a page converts the same stored\n * fragment without sanitizing it, and a scheme rule it re-spelled for itself\n * is a scheme rule that can disagree — which is how `screen:` survived into a\n * link target the page itself refuses to render.\n */\nexport function isAllowedAuthorHtmlUrl(\n tag: string,\n attribute: string,\n decoded: string,\n): boolean {\n // eslint-disable-next-line no-control-regex\n const stripped = decoded.replace(/[\\u0000-\\u0020\\u007f]/g, '').toLowerCase()\n if (!stripped) return true\n const scheme = /^([a-z][a-z0-9+.-]*):/.exec(stripped)\n // No scheme at all: a relative path, a query, or a fragment. Nothing to\n // refuse — it resolves against the site's own origin.\n if (!scheme) return true\n if (scheme[1] === 'data') {\n if (!DATA_URI_ATTRIBUTES.has(`${tag}:${attribute}`)) return false\n return DATA_URI_PAYLOAD.test(stripped)\n }\n return ALLOWED_SCHEMES.has(scheme[1])\n}\n\n/**\n * Filters a `srcset`, keeping only candidates whose URL clears the rule.\n *\n * Returns `null` when nothing survives, so the caller drops the attribute\n * rather than emitting an empty one.\n */\nfunction filterSrcset(tag: string, attribute: string, decoded: string): string | null {\n const kept = decoded\n .split(',')\n .map((candidate) => candidate.trim())\n .filter((candidate) => {\n if (!candidate) return false\n const url = candidate.split(/\\s+/)[0] ?? ''\n return isAllowedAuthorHtmlUrl(tag, attribute, url)\n })\n return kept.length ? kept.join(', ') : null\n}\n\n/**\n * CSS shapes dropped outright rather than filtered.\n *\n * {@link sanitizeAuthorCss} is a `url()` scheme rule and says so; it does not\n * claim to understand these. They are legacy script-in-CSS vectors, dead in\n * current engines, and no author has a use for them — refusing the whole\n * declaration block is both cheaper and more honest than pretending to parse\n * it. This is one of the places this sanitizer keeps LESS than DOMPurify,\n * which filters no CSS at all.\n */\nconst REFUSED_CSS = /expression\\s*\\(|behaviou?r\\s*:|-moz-binding|@import|javascript\\s*:|vbscript\\s*:/i\n\n/** Applies the CSS rule to a decoded `style` attribute or `<style>` body. */\nfunction sanitizeStyleValue(css: string): string | null {\n if (REFUSED_CSS.test(css)) return null\n const safe = sanitizeAuthorCss(css)\n return safe.trim() ? safe : null\n}\n\ninterface Attribute {\n name: string\n value: string\n}\n\n/**\n * Reads the attributes of a start tag beginning at `index` (just past the\n * tag name). Returns where the tag ends and whether it self-closed.\n */\nfunction readAttributes(\n source: string,\n start: number,\n): { attributes: Attribute[]; end: number; selfClosing: boolean } {\n const attributes: Attribute[] = []\n let index = start\n let selfClosing = false\n while (index < source.length) {\n while (index < source.length && /[\\s/]/.test(source[index])) {\n if (source[index] === '/') selfClosing = true\n index += 1\n }\n if (index >= source.length) break\n if (source[index] === '>') {\n index += 1\n break\n }\n selfClosing = false\n const nameMatch = /^[^\\s\"'>/=]+/.exec(source.slice(index))\n if (!nameMatch) {\n index += 1\n continue\n }\n const name = nameMatch[0]\n index += name.length\n while (index < source.length && /\\s/.test(source[index])) index += 1\n let value = ''\n if (source[index] === '=') {\n index += 1\n while (index < source.length && /\\s/.test(source[index])) index += 1\n const quote = source[index]\n if (quote === '\"' || quote === \"'\") {\n index += 1\n const close = source.indexOf(quote, index)\n const stop = close === -1 ? source.length : close\n value = source.slice(index, stop)\n index = close === -1 ? source.length : close + 1\n } else {\n const unquoted = /^[^\\s>]*/.exec(source.slice(index))\n value = unquoted ? unquoted[0] : ''\n index += value.length\n }\n }\n attributes.push({ name, value })\n }\n return { attributes, end: index, selfClosing }\n}\n\n/**\n * Skips a raw-text element's contents, returning the index just past its end\n * tag and the raw body.\n *\n * Raw text is the one place this cannot escape its way to safety: the\n * browser will not decode references or recognize tags inside it, so there\n * is no serialization that makes such a body safe to re-emit. Every element\n * in {@link CONTENT_DROPPING} therefore loses its contents entirely.\n */\nfunction readRawTextElement(\n source: string,\n tag: string,\n contentStart: number,\n): { body: string; end: number } {\n const closing = new RegExp(`</${tag}[\\\\s/>]`, 'i')\n const rest = source.slice(contentStart)\n const match = closing.exec(rest)\n if (!match) return { body: rest, end: source.length }\n const body = rest.slice(0, match.index)\n const afterTag = source.indexOf('>', contentStart + match.index)\n return { body, end: afterTag === -1 ? source.length : afterTag + 1 }\n}\n\n/**\n * Whether `container` would be TORN OPEN by the sanitized body put inside it.\n *\n * The caller renders the body with `dangerouslySetInnerHTML` inside an\n * element of its own choosing, and that element is part of the served\n * document — so the parser's implied end tags apply to it exactly as they do\n * inside the body. A `<p>` holding an `<h2>`, or an `<h2>` holding an\n * `<h2>`, is closed early by the parser, the rest of the body becomes a\n * SIBLING, and React hydrates against a tree it did not describe.\n *\n * Lives here rather than in the caller so the rule and the\n * {@link CLOSES_OPEN_P} table cannot drift apart; `typography.tsx` uses it to\n * fall back to a `<div>`, which no start tag implies an end tag for.\n *\n * Pure, so the server and the client reach the same answer.\n */\nexport function authorHtmlBreaksContainer(\n sanitized: string,\n container: string,\n): boolean {\n if (!sanitized) return false\n const tags = sanitized.match(/<([a-z][a-z0-9]*)[\\s/>]/gi) ?? []\n const names = tags.map((tag) => tag.slice(1, -1).toLowerCase())\n if (container === 'p') return names.some((name) => CLOSES_OPEN_P.has(name))\n if (/^h[1-6]$/.test(container)) {\n return names.some((name) => /^h[1-6]$/.test(name))\n }\n return false\n}\n\n/**\n * Something this sanitizer took OUT, phrased for the author who typed it\n * (AGL-2486).\n *\n * Collected BY the sanitizer, at the point each decision is made, rather\n * than re-derived by a second scan of the input. That is the whole design\n * constraint: this module's header refuses to let the policy exist in two\n * engines, and a warning that re-implements the rules is a second engine —\n * one that goes quietly wrong the moment the real rules move, and tells the\n * author their markup is fine while the page drops it.\n *\n * The render path never asks for these. They exist for the AUTHORING side,\n * which is the only place a person who can fix the markup is standing; the\n * runtime is executing for a VISITOR and can do nothing but drop the value\n * silently. Same reasoning as the reserved-name refusal in `actions.ts`.\n */\nexport interface AuthorHtmlRemoval {\n kind: 'element' | 'attribute' | 'style' | 'url'\n /** One sentence, addressed to the author, naming what is gone and why. */\n message: string\n}\n\n/**\n * Sanitizes author-written HTML to a string safe to hand\n * `dangerouslySetInnerHTML`, on the server and in the browser alike.\n *\n * Pure and deterministic: the same input yields the same bytes in Node and\n * in a browser, which is what lets the server render and the first client\n * render agree without an effect (AGL-1901).\n *\n * `removals`, when passed, is APPENDED to with an {@link AuthorHtmlRemoval}\n * for each thing dropped (AGL-2486). It changes nothing about the output —\n * every existing caller is byte-identical whether it passes one or not — so\n * the render path stays exactly as pure as it was and only the editor pays\n * for the bookkeeping.\n *\n * `options.allowedElements` narrows the element allowlist for one call\n * (AGL-2557). The only value it is given in this repo is\n * {@link INLINE_AUTHOR_HTML_ELEMENTS}, by the components that render their\n * label inside a control. A caller may narrow but never widen: a set with a\n * tag the module's own allowlist refuses would reopen the policy, so the two\n * are intersected rather than substituted.\n */\nexport function sanitizeAuthorHtml(\n html: string,\n removals?: AuthorHtmlRemoval[],\n options?: { allowedElements?: ReadonlySet<string> },\n): string {\n const narrowed = options?.allowedElements\n const allowedElements: ReadonlySet<string> = narrowed\n ? new Set(\n Array.from(narrowed).filter((tag) =>\n ALLOWED_AUTHOR_HTML_ELEMENTS.has(tag),\n ),\n )\n : ALLOWED_AUTHOR_HTML_ELEMENTS\n /** Records one removal, deduped so a repeated tag reads once. */\n const note = (kind: AuthorHtmlRemoval['kind'], message: string): void => {\n if (!removals) return\n if (removals.some((entry) => entry.message === message)) return\n removals.push({ kind, message })\n }\n if (!html) return ''\n const out: string[] = []\n const open: string[] = []\n let index = 0\n /** Set right after a `<pre>` start tag — see {@link emitText}. */\n let justOpenedPre = false\n\n /** Emits end tags down to and including `depth`. */\n const closeTo = (depth: number): void => {\n for (let cursor = open.length - 1; cursor >= depth; cursor -= 1) {\n out.push(`</${open[cursor]}>`)\n }\n open.length = depth\n }\n\n /** Closes the nearest open `tag`, if one is open. */\n const closeNearest = (tag: string): void => {\n const at = open.lastIndexOf(tag)\n if (at !== -1) closeTo(at)\n }\n\n /**\n * The end tags the PARSER would imply before this start tag, emitted so the\n * string we write is the string a document parse will hold — see\n * {@link CLOSES_OPEN_P}.\n */\n const applyImpliedEndTags = (tag: string): void => {\n if (CLOSES_OPEN_P.has(tag)) closeNearest('p')\n // A heading start tag closes an open heading — h2 inside h2 is not\n // nesting to the parser, it is a sibling.\n if (/^h[1-6]$/.test(tag)) {\n const at = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']\n .map((heading) => open.lastIndexOf(heading))\n .reduce((best, found) => Math.max(best, found), -1)\n if (at !== -1) closeTo(at)\n }\n if (tag === 'li') closeNearest('li')\n if (tag === 'dd' || tag === 'dt') {\n const at = Math.max(open.lastIndexOf('dd'), open.lastIndexOf('dt'))\n if (at !== -1) closeTo(at)\n }\n // A nested anchor is not nesting — the parser closes the outer one.\n if (tag === 'a') closeNearest('a')\n if (tag === 'td' || tag === 'th') {\n const at = Math.max(open.lastIndexOf('td'), open.lastIndexOf('th'))\n if (at !== -1) closeTo(at)\n }\n if (tag === 'tr') closeNearest('tr')\n if (tag === 'thead' || tag === 'tbody' || tag === 'tfoot') {\n closeNearest('tr')\n for (const section of ['thead', 'tbody', 'tfoot']) closeNearest(section)\n }\n if (tag === 'table') closeNearest('table')\n }\n\n /** Whether the insertion point is directly inside a table's structure. */\n const inTableContext = (): boolean =>\n open.length > 0 && TABLE_CONTEXTS.has(open[open.length - 1])\n\n /**\n * Emits the `<tbody>` and `<tr>` the parser would have INSERTED.\n *\n * `<table><tr>` is held by every browser as `<table><tbody><tr>`, and a\n * `<td>` with no row around it gets both. Writing them out is the same\n * fixed-point rule as the implied end tags: whatever the parser will add,\n * this has to have said already, or the served string and the hydrated DOM\n * are different documents.\n */\n const openImpliedTableParents = (tag: string): void => {\n const parent = (): string => open[open.length - 1]\n if (tag === 'tr' && parent() === 'table') {\n out.push('<tbody>')\n open.push('tbody')\n return\n }\n if (tag === 'td' || tag === 'th') {\n if (parent() === 'table') {\n out.push('<tbody>')\n open.push('tbody')\n }\n if (parent() === 'thead' || parent() === 'tbody' || parent() === 'tfoot') {\n out.push('<tr>')\n open.push('tr')\n }\n }\n }\n\n const emitStart = (tag: string, attributes: Attribute[]): void => {\n const perElement = AUTHOR_HTML_ELEMENT_ATTRIBUTES[tag]\n const parts: string[] = [tag]\n for (const attribute of attributes) {\n const name = attribute.name.toLowerCase()\n // Event handlers, `data-*` (ALLOW_DATA_ATTR: false on both callers),\n // namespaced names, and the two shapes both configs forbade by name.\n if (!/^[a-z][a-z0-9-]*$/.test(name)) continue\n if (name.startsWith('on')) {\n note(\n 'attribute',\n `${name}=\"…\" is removed — a published page cannot run author JavaScript from an event handler.`,\n )\n continue\n }\n if (name.startsWith('data-')) continue\n if (name === 'srcdoc' || name === 'formaction' || name === 'is') continue\n if (!GLOBAL_AUTHOR_HTML_ATTRIBUTES.has(name) && !perElement?.has(name)) continue\n let value = decodeCharacterReferences(attribute.value)\n if (name === 'style') {\n const safe = sanitizeStyleValue(value)\n if (safe === null) {\n note(\n 'style',\n `the style attribute on <${tag}> is removed — inline CSS cannot use @import, expression(), behavior:, -moz-binding or javascript:.`,\n )\n continue\n }\n // Identity return means nothing was refused; anything else means a\n // `url()` was neutered, which is invisible on the page (the image\n // simply never appears) and therefore the case most worth naming.\n if (safe !== value) {\n note(\n 'url',\n `a url() in the style attribute on <${tag}> will not load — inline CSS may only fetch over https:, data: or blob:.`,\n )\n }\n value = safe\n } else if (URL_LIST_ATTRIBUTES.has(name)) {\n const safe = filterSrcset(tag, name, value)\n if (safe === null) {\n note(\n 'url',\n `${name} on <${tag}> is removed — only https:, data: and blob: URLs are allowed.`,\n )\n continue\n }\n value = safe\n } else if (URL_ATTRIBUTES.has(name)) {\n if (!isAllowedAuthorHtmlUrl(tag, name, value)) {\n note(\n 'url',\n `${name} on <${tag}> is removed — only https:, data: and blob: URLs are allowed.`,\n )\n continue\n }\n }\n parts.push(`${name}=\"${escapeAttribute(value)}\"`)\n }\n out.push(`<${parts.join(' ')}>`)\n }\n\n /**\n * Text between a table's own elements is foster-parented OUT of the table\n * by the parser, which is a move this serializer cannot express — so it is\n * dropped and the emitted string round-trips.\n */\n const emitText = (text: string): void => {\n if (!text || inTableContext()) return\n let value = text\n // The parser drops ONE U+000A immediately after a `<pre>` start tag. The\n // serializer is supposed to put one back when the text still begins with\n // one, but that half is not implemented everywhere (jsdom does not), so\n // the ONLY string that survives both a browser and a test DOM unchanged\n // is one with no leading newline at all. Emitting that costs an author a\n // blank first line inside a `<pre>` and buys a fixed point.\n if (justOpenedPre && open[open.length - 1] === 'pre') {\n value = value.replace(/^\\n+/, '')\n }\n justOpenedPre = false\n if (!value) return\n out.push(escapeText(value))\n }\n\n while (index < html.length) {\n const next = html.indexOf('<', index)\n if (next === -1) {\n emitText(html.slice(index))\n break\n }\n if (next > index) emitText(html.slice(index, next))\n\n const rest = html.slice(next)\n // Comments, doctypes and processing instructions: no content of ours.\n if (rest.startsWith('<!--')) {\n const close = html.indexOf('-->', next + 4)\n index = close === -1 ? html.length : close + 3\n continue\n }\n if (rest.startsWith('<!') || rest.startsWith('<?')) {\n const close = html.indexOf('>', next)\n index = close === -1 ? html.length : close + 1\n continue\n }\n\n const endTag = /^<\\/([a-zA-Z][a-zA-Z0-9]*)/.exec(rest)\n if (endTag) {\n const tag = endTag[1].toLowerCase()\n const close = html.indexOf('>', next)\n index = close === -1 ? html.length : close + 1\n const at = open.lastIndexOf(tag)\n // A stray end tag closes nothing — the browser ignores it too.\n if (at === -1) continue\n closeTo(at)\n continue\n }\n\n const startTag = /^<([a-zA-Z][a-zA-Z0-9]*)/.exec(rest)\n if (!startTag) {\n // A `<` that begins no tag is literal text, exactly as the parser\n // treats it.\n emitText('<')\n index = next + 1\n continue\n }\n\n const tag = startTag[1].toLowerCase()\n const { attributes, end, selfClosing } = readAttributes(\n html,\n next + startTag[0].length,\n )\n\n if (CONTENT_DROPPING.has(tag)) {\n note(\n 'element',\n `<${tag}> and everything inside it is removed — this element is not allowed in author HTML.`,\n )\n const { end: bodyEnd } = readRawTextElement(html, tag, end)\n index = bodyEnd\n continue\n }\n\n index = end\n\n if (!allowedElements.has(tag)) {\n // Unwrapped, not dropped — matches DOMPurify's `KEEP_CONTENT` default,\n // so a `<form>` or an unknown tag loses the element and keeps the words\n // inside it.\n note(\n 'element',\n `<${tag}> is removed — the text inside it is kept, but the element itself is not allowed in author HTML.`,\n )\n continue\n }\n\n // Context rules, both of them about reparenting rather than about safety.\n // A non-table element written directly inside a table is foster-parented\n // out of it; a table part written outside a table has its tag dropped and\n // its contents kept. Following the parser here is what keeps the emitted\n // string a fixed point of it.\n if (inTableContext() && !TABLE_PARTS.has(tag)) {\n const { end: bodyEnd } = readRawTextElement(html, tag, end)\n index = bodyEnd\n continue\n }\n if (TABLE_PARTS.has(tag) && !open.includes('table')) continue\n\n applyImpliedEndTags(tag)\n\n // `caption`, `colgroup` and `col` are only accepted in one place each;\n // written anywhere else the parser moves or drops them, so this does the\n // dropping and keeps the string stable.\n const parentTag = open[open.length - 1]\n if (\n (tag === 'caption' || tag === 'colgroup') && parentTag !== 'table'\n ) {\n continue\n }\n if (tag === 'col' && parentTag !== 'colgroup') continue\n\n openImpliedTableParents(tag)\n\n if (VOID_ELEMENTS.has(tag)) {\n emitStart(tag, attributes)\n continue\n }\n emitStart(tag, attributes)\n justOpenedPre = tag === 'pre'\n if (!selfClosing) open.push(tag)\n else out.push(`</${tag}>`)\n }\n\n closeTo(0)\n return out.join('')\n}\n"],"names":["sanitizeAuthorCss","ALLOWED_AUTHOR_HTML_ELEMENTS","Set","INLINE_AUTHOR_HTML_ELEMENTS","VOID_ELEMENTS","CONTENT_DROPPING","GLOBAL_AUTHOR_HTML_ATTRIBUTES","AUTHOR_HTML_ELEMENT_ATTRIBUTES","a","audio","blockquote","col","colgroup","data","del","details","img","ins","li","ol","q","source","td","th","time","track","video","CLOSES_OPEN_P","TABLE_CONTEXTS","TABLE_PARTS","URL_ATTRIBUTES","URL_LIST_ATTRIBUTES","ALLOWED_SCHEMES","DATA_URI_ATTRIBUTES","DATA_URI_PAYLOAD","NAMED_REFERENCES","amp","AMP","lt","LT","gt","GT","quot","QUOT","apos","nbsp","colon","semi","sol","bsol","num","period","comma","excl","quest","lpar","rpar","Tab","NewLine","equals","ast","commat","dollar","percnt","plus","minus","lowbar","decodeCharacterReferences","value","includes","replace","match","dec","hex","name","undefined","code","parseInt","Number","isFinite","String","fromCodePoint","named","escapeText","text","escapeAttribute","isAllowedAuthorHtmlUrl","tag","attribute","decoded","stripped","toLowerCase","scheme","exec","has","test","filterSrcset","kept","split","map","candidate","trim","filter","url","length","join","REFUSED_CSS","sanitizeStyleValue","css","safe","readAttributes","start","attributes","index","selfClosing","nameMatch","slice","quote","close","indexOf","stop","unquoted","push","end","readRawTextElement","contentStart","closing","RegExp","rest","body","afterTag","authorHtmlBreaksContainer","sanitized","container","tags","names","some","sanitizeAuthorHtml","html","removals","options","narrowed","allowedElements","Array","from","note","kind","message","entry","out","open","justOpenedPre","closeTo","depth","cursor","closeNearest","at","lastIndexOf","applyImpliedEndTags","heading","reduce","best","found","Math","max","section","inTableContext","openImpliedTableParents","parent","emitStart","perElement","parts","startsWith","emitText","next","endTag","startTag","bodyEnd","parentTag"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/author-html.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * An ISOMORPHIC sanitizer for AUTHOR-WRITTEN HTML (AGL-1901).\n *\n * ## Why this exists\n *\n * Rich text (`typography.tsx`'s `html` prop) and Custom HTML\n * (`custom-html.tsx`) are the two places a site author's raw markup reaches\n * `dangerouslySetInnerHTML`. Both sanitized with DOMPurify, which needs a\n * real DOM, so both deferred the sanitize into a `useEffect` — server render\n * and first client render agreed only because BOTH were empty (AGL-1268).\n * That fixed hydration and left every rich-text body and every Custom HTML\n * block absent from the server response: a crawler that does not run JS saw\n * nothing, on a product sold on \"build a site and be found\".\n *\n * AGL-1901 named two ways out. Isomorphic DOMPurify over jsdom keeps one\n * engine but puts jsdom in the tenant SERVER bundle, landing on the open\n * cold-start 502s (AGL-1152). A second sanitizer on the server is light but\n * re-encodes the policy in a second engine, and any drift between the two\n * becomes a stored-XSS hole rather than a rendering bug.\n *\n * This module takes the second shape and DELETES the drift risk instead of\n * managing it: it is the ONLY sanitizer on the render path, on both sides.\n * The server renders `sanitizeAuthorHtml(html)`; the first client render\n * calls the same pure function on the same string and gets the same bytes,\n * so hydration is clean by construction and there is no second engine to\n * drift from. DOMPurify remains the reference the parity spec measures this\n * against (`author-html-dompurify-parity.spec.ts`), not a second gate at\n * runtime — a runtime second pass would be worthless anyway, because the\n * server string is parsed and executed by the browser BEFORE any client code\n * runs.\n *\n * The safety argument is therefore a SUBSET argument, and the parity spec\n * asserts it: every tag and every attribute this emits is one DOMPurify\n * would also have kept under the same config. Where the two differ, this one\n * keeps less. Keeping less can only cost an author some markup; keeping more\n * would be the hole.\n *\n * ## How it is safe without a DOM\n *\n * A source-level sanitizer is only sound if the string it emits re-parses to\n * the tree it thinks it built — the mXSS class. Four rules make that true\n * here, and each is load-bearing:\n *\n * 1. **No foreign content, ever.** `<svg>` and `<math>` switch the parser\n * into a namespace with different tokenization rules; they are dropped\n * with their contents, as are `<template>`, `<noscript>`, `<noembed>`,\n * `<xmp>` and `<plaintext>`, whose contents parse under rules of their\n * own. Nothing this emits can re-enter foreign or raw-text mode.\n * 2. **Everything is re-serialized, never passed through.** Text is emitted\n * with `&`, `<` and `>` escaped; attribute values are always double\n * quoted with `&`, `<`, `>` and `\"` escaped. There are no unquoted\n * attribute values and no raw `<` in text for a re-parse to misread.\n * 3. **Character references are decoded BEFORE any value is judged.** A\n * tokenizer that reads source rather than a DOM is otherwise trivially\n * defeated by `href=\"javascript:alert(1)\"`, which the browser\n * decodes and this would not. {@link decodeCharacterReferences} handles\n * every numeric reference generically, plus the named references that can\n * contribute a colon or whitespace to a scheme. ASCII letters have NO\n * named references in HTML, so no named reference this does not know can\n * help spell a scheme; only numeric ones could, and those are all\n * decoded. The decoded value is what is validated AND what is re-escaped\n * on the way out, so the browser sees exactly the string that was judged.\n * 4. **Allowlists, not denylists**, for elements and for attributes, with\n * every URL-bearing attribute scheme-checked.\n *\n * ## Relationship to the CSS rule\n *\n * `style` attributes go through {@link sanitizeAuthorCss} exactly as the\n * per-instance DOMPurify hook AGL-1725 added did, because DOMPurify performs\n * no CSS filtering of its own. A `<style>` ELEMENT is dropped rather than\n * filtered, which is what both DOMPurify configs already did — the `css`\n * component attribute remains the only route to a style block on a page.\n */\n\nimport { sanitizeAuthorCss } from './author-css'\n\n/**\n * Elements that survive: HTML flow and phrasing content, a strict subset of\n * DOMPurify's `USE_PROFILES: { html: true }` set minus the tags both callers\n * already forbade (`script`, `iframe`, `object`, `embed`, `form`, `base`)\n * and minus every interactive form control, which no author markup on a\n * rendered page has a use for and which carries `formaction` shapes.\n */\nexport const ALLOWED_AUTHOR_HTML_ELEMENTS: ReadonlySet<string> = new Set([\n 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',\n 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'data',\n 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption',\n 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup',\n // `main` is a deliberate subtraction from the DOMPurify profile (AGL-2486):\n // a published page carries exactly one `main` landmark, placed by\n // composition on the page's content region, so author markup carrying\n // another can only make the landmark ambiguous. It is\n // UNWRAPPED like any other unlisted tag, not dropped, so the author's content\n // survives verbatim and only the competing landmark goes.\n 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'mark', 'nav', 'ol', 'p',\n 'picture', 'pre', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'small',\n 'source', 'span', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody',\n 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'u', 'ul', 'var',\n 'video', 'wbr',\n])\n\n/**\n * The subset of {@link ALLOWED_AUTHOR_HTML_ELEMENTS} that may appear inside a\n * control — a `<button>` or an `<a>` (AGL-2557).\n *\n * Both have a content model of phrasing content with NO interactive\n * descendant. Serving a `<ul>`, a `<div>` or a nested `<a>` inside one is not\n * a styling problem: the parser closes the control early and promotes the\n * rest to a sibling, so React hydrates against a tree it did not describe and\n * reports the mismatch — the same shape `authorHtmlBreaksContainer` exists to\n * head off for a `<p>`. A nested control is also unreachable by keyboard and\n * announces as neither of the two things it is.\n *\n * Narrowing keeps LESS than the full allowlist, so the subset argument in\n * this module's header still holds: everything emitted under it is something\n * the full policy would also have kept. `img` is out with the rest of the\n * embedded content — an author who wants an icon on a button has the icon\n * fields for it, and nothing in the inline editor can produce one.\n */\nexport const INLINE_AUTHOR_HTML_ELEMENTS: ReadonlySet<string> = new Set([\n 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'del', 'dfn',\n 'em', 'i', 'ins', 'kbd', 'mark', 'q', 'rp', 'rt', 'ruby', 's', 'samp',\n 'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr',\n])\n\n/**\n * Void elements — emitted WITHOUT a closing tag and never pushed on the open\n * stack. A void element that carries children is not merely wrong markup\n * here: `<img>` with children is an SSR 500 in this codebase, so the\n * serializer must be structurally incapable of producing one.\n */\nconst VOID_ELEMENTS = new Set([\n 'br', 'col', 'hr', 'img', 'source', 'track', 'wbr',\n])\n\n/**\n * Elements dropped WITH their contents rather than unwrapped.\n *\n * Two reasons, both about the parser rather than about the tag: raw-text and\n * escapable-raw-text elements (`script`, `style`, `textarea`, `title`,\n * `xmp`, `plaintext`, `noscript`, `noembed`, `noframes`, `iframe`) tokenize\n * their contents under different rules, and foreign-content roots (`svg`,\n * `math`) switch namespace. Unwrapping either would hand the re-parse a\n * string this never modelled.\n *\n * `<style>` is in this set for BOTH callers, because both DOMPurify configs\n * dropped it: `typography.tsx` listed it in `FORBID_TAGS` and the `html`\n * profile does not carry it, which `custom-html-author-css.spec.tsx` pins.\n * The `css` component attribute stays the only route to a style block, and\n * it goes through {@link sanitizeAuthorCss} where it always did.\n */\nconst CONTENT_DROPPING = new Set([\n 'script', 'style', 'textarea', 'title', 'xmp', 'plaintext', 'noscript',\n 'noembed', 'noframes', 'iframe', 'svg', 'math', 'template', 'object',\n 'applet', 'frame', 'frameset', 'canvas',\n])\n\n/** Attributes any allowed element may carry. */\nexport const GLOBAL_AUTHOR_HTML_ATTRIBUTES: ReadonlySet<string> = new Set([\n 'class', 'id', 'title', 'lang', 'dir', 'style', 'role',\n])\n\n/** Attributes allowed only on the elements that define them. */\nexport const AUTHOR_HTML_ELEMENT_ATTRIBUTES: Readonly<Record<string, ReadonlySet<string>>> = {\n // No `target`. DOMPurify's html profile drops it, so both callers already\n // dropped it, and the parity spec measures this table against that profile\n // — putting it back here would be this sanitizer keeping MORE than the\n // policy it replaced, which is the one direction that is not allowed to\n // happen quietly. Giving authors `target=\"_blank\"` is a product decision\n // about the allowlist, not a side effect of moving the sanitizer.\n a: new Set(['href', 'rel', 'name', 'download', 'hreflang', 'type']),\n audio: new Set(['src', 'controls', 'loop', 'muted', 'preload']),\n blockquote: new Set(['cite']),\n col: new Set(['span']),\n colgroup: new Set(['span']),\n data: new Set(['value']),\n del: new Set(['cite', 'datetime']),\n details: new Set(['open']),\n img: new Set([\n 'src', 'srcset', 'alt', 'width', 'height', 'loading', 'decoding', 'sizes',\n ]),\n ins: new Set(['cite', 'datetime']),\n li: new Set(['value']),\n ol: new Set(['start', 'reversed', 'type']),\n q: new Set(['cite']),\n source: new Set(['src', 'srcset', 'type', 'media', 'sizes', 'width', 'height']),\n td: new Set(['colspan', 'rowspan', 'headers']),\n // No `abbr`: DOMPurify's html profile drops it on `th`, same reason as\n // `target` on `a` — this table may not be wider than the policy it replaced.\n th: new Set(['colspan', 'rowspan', 'headers', 'scope']),\n time: new Set(['datetime']),\n track: new Set(['src', 'kind', 'srclang', 'label', 'default']),\n video: new Set([\n 'src', 'poster', 'controls', 'loop', 'muted', 'preload', 'playsinline',\n 'width', 'height',\n ]),\n}\n\n/**\n * Start tags that force an open `<p>` closed (HTML \"in body\", the\n * `close a p element` clause).\n *\n * These exist here because the emitted string has to be a FIXED POINT of the\n * parser. A crawler and a browser both read the server response through the\n * document parser, which reparents `<p>a<div>b</div></p>` into\n * `<p>a</p><div>b</div><p></p>` — and React, hydrating, compares the\n * container's `innerHTML` to the `__html` it was handed and reports the\n * difference as a mismatch. Emitting the implied end tag ourselves is what\n * makes \"what we wrote\" and \"what the browser will hold\" the same string.\n * That is not a theoretical tidiness: it is the hydration regression this\n * change would otherwise have traded the SSR gap for.\n */\nconst CLOSES_OPEN_P = new Set([\n 'address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl', 'dd',\n 'dt', 'figcaption', 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n 'header', 'hgroup', 'hr', 'li', 'main', 'nav', 'ol', 'p', 'pre', 'section',\n 'summary', 'table', 'ul',\n])\n\n/**\n * Elements whose children the parser accepts ONLY as table parts. Text and\n * ordinary elements written here are foster-parented OUT of the table, which\n * is a reparenting this cannot express — so they are dropped instead, and\n * the emitted string round-trips.\n */\nconst TABLE_CONTEXTS = new Set(['table', 'thead', 'tbody', 'tfoot', 'tr'])\n\n/** Elements the parser only accepts inside a table. */\nconst TABLE_PARTS = new Set([\n 'caption', 'colgroup', 'col', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',\n])\n\n/** Attributes whose value is a URL and must clear the scheme rule. */\nconst URL_ATTRIBUTES = new Set(['href', 'src', 'poster', 'cite'])\n/** Attributes whose value is a comma-separated candidate list of URLs. */\nconst URL_LIST_ATTRIBUTES = new Set(['srcset'])\n\n/**\n * Schemes a URL attribute may name. Mirrors DOMPurify's default\n * `ALLOWED_URI_REGEXP` minus `javascript:`-adjacent shapes it also refuses;\n * `data:` is handled separately because it is only safe on the media\n * attributes and only for raster types.\n */\nconst ALLOWED_SCHEMES = new Set([\n 'http', 'https', 'ftp', 'ftps', 'mailto', 'tel', 'callto', 'sms', 'cid',\n 'xmpp',\n])\n\n/** Tag/attribute pairs on which a `data:` URL is considered at all. */\nconst DATA_URI_ATTRIBUTES = new Set(['img:src', 'img:srcset', 'source:src',\n 'source:srcset', 'video:src', 'video:poster', 'audio:src'])\n\n/**\n * `data:` payloads accepted on those pairs: raster images only.\n *\n * `image/svg+xml` is deliberately absent. An SVG document can carry\n * `<script>`, and a `data:` SVG in an `img` is same-origin-ish enough in\n * enough engines that treating it as an image is a bet this does not need to\n * take — the author has real image hosting.\n */\nconst DATA_URI_PAYLOAD =\n /^data:image\\/(?:png|jpe?g|gif|webp|avif|bmp|x-icon)\\s*;\\s*base64\\s*,/i\n\n/**\n * Named character references this decodes, beyond the numeric forms.\n *\n * The list is short on purpose and the reasoning is in the module header:\n * HTML has no named reference for an ASCII letter, so no reference outside\n * this table can contribute to spelling `javascript`. What a reference CAN\n * contribute is the colon and the leading whitespace a browser tolerates\n * before a scheme, so those are the ones that must be understood rather than\n * passed through.\n */\nconst NAMED_REFERENCES: Record<string, string> = {\n amp: '&', AMP: '&', lt: '<', LT: '<', gt: '>', GT: '>', quot: '\"',\n QUOT: '\"', apos: \"'\", nbsp: ' ', colon: ':', semi: ';', sol: '/',\n bsol: '\\\\', num: '#', period: '.', comma: ',', excl: '!', quest: '?',\n lpar: '(', rpar: ')', Tab: '\\t', NewLine: '\\n', equals: '=', ast: '*',\n commat: '@', dollar: '$', percnt: '%', plus: '+', minus: '-', lowbar: '_',\n}\n\n/**\n * Decodes character references in an attribute value the way a browser will,\n * so the value can be JUDGED as the browser will see it.\n *\n * Numeric references are decoded generically — that is the form that can\n * produce any character at all, and the only form that can spell a scheme.\n * Named references are decoded from {@link NAMED_REFERENCES}; anything else\n * is left verbatim, which is safe precisely because it cannot become a\n * letter or a colon. Everything decoded here is re-escaped on output, so the\n * emitted attribute value and the validated value are the same string.\n */\nexport function decodeCharacterReferences(value: string): string {\n if (!value.includes('&')) return value\n return value.replace(\n /&(?:#([0-9]{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z][a-zA-Z0-9]{1,31}))(;?)/g,\n (match, dec: string, hex: string, name: string) => {\n if (dec !== undefined || hex !== undefined) {\n const code = dec !== undefined ? parseInt(dec, 10) : parseInt(hex, 16)\n // Surrogates and out-of-range values have no scalar; dropping them\n // is safer than inventing a replacement a re-parse would disagree\n // with.\n if (!Number.isFinite(code) || code <= 0 || code > 0x10ffff) return ''\n if (code >= 0xd800 && code <= 0xdfff) return ''\n return String.fromCodePoint(code)\n }\n const named = NAMED_REFERENCES[name]\n return named === undefined ? match : named\n },\n )\n}\n\n/**\n * Escapes text content EXACTLY as the HTML serialization algorithm does.\n *\n * The byte-for-byte match is the requirement, not a nicety: React compares\n * the container's `innerHTML` against the `__html` it was given when it\n * hydrates, so a string that merely PARSES the same but SERIALIZES\n * differently is reported as a hydration mismatch. `escapeAuthorHtmlText`\n * and {@link escapeAttributeValue} together are the \"serialize\" half of the\n * fixed point `author-html-round-trip.spec.ts` asserts.\n */\nfunction escapeText(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/\\u00a0/g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n}\n\n/**\n * Escapes a decoded attribute value for a double-quoted slot.\n *\n * Deliberately does NOT escape `<` or `>`: the serialization algorithm\n * escapes only `&`, U+00A0 and `\"` in an attribute value, and inside a\n * quoted value a raw `<` is literal to the parser, so escaping it would be\n * safe but would not round-trip.\n *\n * That is also why this is NOT `@aglyn/shared-util-tools/escape-html`, which\n * every other string-built HTML surface in the repo now shares. That one\n * escapes the five characters a SAFE escaper needs; this one has to emit\n * exactly the characters a browser's serializer emits and no others. Pointing\n * it at the shared escaper was measured (AGL-2706): the `<` inside a quoted\n * href comes back `<`, and `author-html-round-trip.spec.tsx` fails on the\n * fixed point — the string no longer re-serializes to itself, which is the\n * hydration mismatch this pair exists to prevent. U+00A0 is the same argument\n * in the other direction: the shared escaper leaves it alone, the serializer\n * writes ` `.\n */\nfunction escapeAttribute(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/\\u00a0/g, ' ')\n .replace(/\"/g, '"')\n}\n\n/**\n * Whether a decoded URL clears the scheme rule for this tag/attribute.\n *\n * The comparison is made on a copy with every C0 control and space removed\n * and folded to lower case, because browsers strip exactly those while\n * resolving a scheme — `\"java\\tscript:alert(1)\"` navigates.\n *\n * Exported because the sanitizer is not the only reader of author markup\n * (AGL-2740). The Markdown representation of a page converts the same stored\n * fragment without sanitizing it, and a scheme rule it re-spelled for itself\n * is a scheme rule that can disagree — which is how `screen:` survived into a\n * link target the page itself refuses to render.\n */\nexport function isAllowedAuthorHtmlUrl(\n tag: string,\n attribute: string,\n decoded: string,\n): boolean {\n // eslint-disable-next-line no-control-regex\n const stripped = decoded.replace(/[\\u0000-\\u0020\\u007f]/g, '').toLowerCase()\n if (!stripped) return true\n const scheme = /^([a-z][a-z0-9+.-]*):/.exec(stripped)\n // No scheme at all: a relative path, a query, or a fragment. Nothing to\n // refuse — it resolves against the site's own origin.\n if (!scheme) return true\n if (scheme[1] === 'data') {\n if (!DATA_URI_ATTRIBUTES.has(`${tag}:${attribute}`)) return false\n return DATA_URI_PAYLOAD.test(stripped)\n }\n return ALLOWED_SCHEMES.has(scheme[1])\n}\n\n/**\n * Filters a `srcset`, keeping only candidates whose URL clears the rule.\n *\n * Returns `null` when nothing survives, so the caller drops the attribute\n * rather than emitting an empty one.\n */\nfunction filterSrcset(tag: string, attribute: string, decoded: string): string | null {\n const kept = decoded\n .split(',')\n .map((candidate) => candidate.trim())\n .filter((candidate) => {\n if (!candidate) return false\n const url = candidate.split(/\\s+/)[0] ?? ''\n return isAllowedAuthorHtmlUrl(tag, attribute, url)\n })\n return kept.length ? kept.join(', ') : null\n}\n\n/**\n * CSS shapes dropped outright rather than filtered.\n *\n * {@link sanitizeAuthorCss} is a `url()` scheme rule and says so; it does not\n * claim to understand these. They are legacy script-in-CSS vectors, dead in\n * current engines, and no author has a use for them — refusing the whole\n * declaration block is both cheaper and more honest than pretending to parse\n * it. This is one of the places this sanitizer keeps LESS than DOMPurify,\n * which filters no CSS at all.\n */\nconst REFUSED_CSS = /expression\\s*\\(|behaviou?r\\s*:|-moz-binding|@import|javascript\\s*:|vbscript\\s*:/i\n\n/** Applies the CSS rule to a decoded `style` attribute or `<style>` body. */\nfunction sanitizeStyleValue(css: string): string | null {\n if (REFUSED_CSS.test(css)) return null\n const safe = sanitizeAuthorCss(css)\n return safe.trim() ? safe : null\n}\n\ninterface Attribute {\n name: string\n value: string\n}\n\n/**\n * Reads the attributes of a start tag beginning at `index` (just past the\n * tag name). Returns where the tag ends and whether it self-closed.\n */\nfunction readAttributes(\n source: string,\n start: number,\n): { attributes: Attribute[]; end: number; selfClosing: boolean } {\n const attributes: Attribute[] = []\n let index = start\n let selfClosing = false\n while (index < source.length) {\n while (index < source.length && /[\\s/]/.test(source[index])) {\n if (source[index] === '/') selfClosing = true\n index += 1\n }\n if (index >= source.length) break\n if (source[index] === '>') {\n index += 1\n break\n }\n selfClosing = false\n const nameMatch = /^[^\\s\"'>/=]+/.exec(source.slice(index))\n if (!nameMatch) {\n index += 1\n continue\n }\n const name = nameMatch[0]\n index += name.length\n while (index < source.length && /\\s/.test(source[index])) index += 1\n let value = ''\n if (source[index] === '=') {\n index += 1\n while (index < source.length && /\\s/.test(source[index])) index += 1\n const quote = source[index]\n if (quote === '\"' || quote === \"'\") {\n index += 1\n const close = source.indexOf(quote, index)\n const stop = close === -1 ? source.length : close\n value = source.slice(index, stop)\n index = close === -1 ? source.length : close + 1\n } else {\n const unquoted = /^[^\\s>]*/.exec(source.slice(index))\n value = unquoted ? unquoted[0] : ''\n index += value.length\n }\n }\n attributes.push({ name, value })\n }\n return { attributes, end: index, selfClosing }\n}\n\n/**\n * Skips a raw-text element's contents, returning the index just past its end\n * tag and the raw body.\n *\n * Raw text is the one place this cannot escape its way to safety: the\n * browser will not decode references or recognize tags inside it, so there\n * is no serialization that makes such a body safe to re-emit. Every element\n * in {@link CONTENT_DROPPING} therefore loses its contents entirely.\n */\nfunction readRawTextElement(\n source: string,\n tag: string,\n contentStart: number,\n): { body: string; end: number } {\n const closing = new RegExp(`</${tag}[\\\\s/>]`, 'i')\n const rest = source.slice(contentStart)\n const match = closing.exec(rest)\n if (!match) return { body: rest, end: source.length }\n const body = rest.slice(0, match.index)\n const afterTag = source.indexOf('>', contentStart + match.index)\n return { body, end: afterTag === -1 ? source.length : afterTag + 1 }\n}\n\n/**\n * Whether `container` would be TORN OPEN by the sanitized body put inside it.\n *\n * The caller renders the body with `dangerouslySetInnerHTML` inside an\n * element of its own choosing, and that element is part of the served\n * document — so the parser's implied end tags apply to it exactly as they do\n * inside the body. A `<p>` holding an `<h2>`, or an `<h2>` holding an\n * `<h2>`, is closed early by the parser, the rest of the body becomes a\n * SIBLING, and React hydrates against a tree it did not describe.\n *\n * Lives here rather than in the caller so the rule and the\n * {@link CLOSES_OPEN_P} table cannot drift apart; `typography.tsx` uses it to\n * fall back to a `<div>`, which no start tag implies an end tag for.\n *\n * Pure, so the server and the client reach the same answer.\n */\nexport function authorHtmlBreaksContainer(\n sanitized: string,\n container: string,\n): boolean {\n if (!sanitized) return false\n const tags = sanitized.match(/<([a-z][a-z0-9]*)[\\s/>]/gi) ?? []\n const names = tags.map((tag) => tag.slice(1, -1).toLowerCase())\n if (container === 'p') return names.some((name) => CLOSES_OPEN_P.has(name))\n if (/^h[1-6]$/.test(container)) {\n return names.some((name) => /^h[1-6]$/.test(name))\n }\n return false\n}\n\n/**\n * Something this sanitizer took OUT, phrased for the author who typed it\n * (AGL-2486).\n *\n * Collected BY the sanitizer, at the point each decision is made, rather\n * than re-derived by a second scan of the input. That is the whole design\n * constraint: this module's header refuses to let the policy exist in two\n * engines, and a warning that re-implements the rules is a second engine —\n * one that goes quietly wrong the moment the real rules move, and tells the\n * author their markup is fine while the page drops it.\n *\n * The render path never asks for these. They exist for the AUTHORING side,\n * which is the only place a person who can fix the markup is standing; the\n * runtime is executing for a VISITOR and can do nothing but drop the value\n * silently. Same reasoning as the reserved-name refusal in `site-interactions.ts`.\n */\nexport interface AuthorHtmlRemoval {\n kind: 'element' | 'attribute' | 'style' | 'url'\n /** One sentence, addressed to the author, naming what is gone and why. */\n message: string\n}\n\n/**\n * Sanitizes author-written HTML to a string safe to hand\n * `dangerouslySetInnerHTML`, on the server and in the browser alike.\n *\n * Pure and deterministic: the same input yields the same bytes in Node and\n * in a browser, which is what lets the server render and the first client\n * render agree without an effect (AGL-1901).\n *\n * `removals`, when passed, is APPENDED to with an {@link AuthorHtmlRemoval}\n * for each thing dropped (AGL-2486). It changes nothing about the output —\n * every existing caller is byte-identical whether it passes one or not — so\n * the render path stays exactly as pure as it was and only the editor pays\n * for the bookkeeping.\n *\n * `options.allowedElements` narrows the element allowlist for one call\n * (AGL-2557). The only value it is given in this repo is\n * {@link INLINE_AUTHOR_HTML_ELEMENTS}, by the components that render their\n * label inside a control. A caller may narrow but never widen: a set with a\n * tag the module's own allowlist refuses would reopen the policy, so the two\n * are intersected rather than substituted.\n */\nexport function sanitizeAuthorHtml(\n html: string,\n removals?: AuthorHtmlRemoval[],\n options?: { allowedElements?: ReadonlySet<string> },\n): string {\n const narrowed = options?.allowedElements\n const allowedElements: ReadonlySet<string> = narrowed\n ? new Set(\n Array.from(narrowed).filter((tag) =>\n ALLOWED_AUTHOR_HTML_ELEMENTS.has(tag),\n ),\n )\n : ALLOWED_AUTHOR_HTML_ELEMENTS\n /** Records one removal, deduped so a repeated tag reads once. */\n const note = (kind: AuthorHtmlRemoval['kind'], message: string): void => {\n if (!removals) return\n if (removals.some((entry) => entry.message === message)) return\n removals.push({ kind, message })\n }\n if (!html) return ''\n const out: string[] = []\n const open: string[] = []\n let index = 0\n /** Set right after a `<pre>` start tag — see {@link emitText}. */\n let justOpenedPre = false\n\n /** Emits end tags down to and including `depth`. */\n const closeTo = (depth: number): void => {\n for (let cursor = open.length - 1; cursor >= depth; cursor -= 1) {\n out.push(`</${open[cursor]}>`)\n }\n open.length = depth\n }\n\n /** Closes the nearest open `tag`, if one is open. */\n const closeNearest = (tag: string): void => {\n const at = open.lastIndexOf(tag)\n if (at !== -1) closeTo(at)\n }\n\n /**\n * The end tags the PARSER would imply before this start tag, emitted so the\n * string we write is the string a document parse will hold — see\n * {@link CLOSES_OPEN_P}.\n */\n const applyImpliedEndTags = (tag: string): void => {\n if (CLOSES_OPEN_P.has(tag)) closeNearest('p')\n // A heading start tag closes an open heading — h2 inside h2 is not\n // nesting to the parser, it is a sibling.\n if (/^h[1-6]$/.test(tag)) {\n const at = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']\n .map((heading) => open.lastIndexOf(heading))\n .reduce((best, found) => Math.max(best, found), -1)\n if (at !== -1) closeTo(at)\n }\n if (tag === 'li') closeNearest('li')\n if (tag === 'dd' || tag === 'dt') {\n const at = Math.max(open.lastIndexOf('dd'), open.lastIndexOf('dt'))\n if (at !== -1) closeTo(at)\n }\n // A nested anchor is not nesting — the parser closes the outer one.\n if (tag === 'a') closeNearest('a')\n if (tag === 'td' || tag === 'th') {\n const at = Math.max(open.lastIndexOf('td'), open.lastIndexOf('th'))\n if (at !== -1) closeTo(at)\n }\n if (tag === 'tr') closeNearest('tr')\n if (tag === 'thead' || tag === 'tbody' || tag === 'tfoot') {\n closeNearest('tr')\n for (const section of ['thead', 'tbody', 'tfoot']) closeNearest(section)\n }\n if (tag === 'table') closeNearest('table')\n }\n\n /** Whether the insertion point is directly inside a table's structure. */\n const inTableContext = (): boolean =>\n open.length > 0 && TABLE_CONTEXTS.has(open[open.length - 1])\n\n /**\n * Emits the `<tbody>` and `<tr>` the parser would have INSERTED.\n *\n * `<table><tr>` is held by every browser as `<table><tbody><tr>`, and a\n * `<td>` with no row around it gets both. Writing them out is the same\n * fixed-point rule as the implied end tags: whatever the parser will add,\n * this has to have said already, or the served string and the hydrated DOM\n * are different documents.\n */\n const openImpliedTableParents = (tag: string): void => {\n const parent = (): string => open[open.length - 1]\n if (tag === 'tr' && parent() === 'table') {\n out.push('<tbody>')\n open.push('tbody')\n return\n }\n if (tag === 'td' || tag === 'th') {\n if (parent() === 'table') {\n out.push('<tbody>')\n open.push('tbody')\n }\n if (parent() === 'thead' || parent() === 'tbody' || parent() === 'tfoot') {\n out.push('<tr>')\n open.push('tr')\n }\n }\n }\n\n const emitStart = (tag: string, attributes: Attribute[]): void => {\n const perElement = AUTHOR_HTML_ELEMENT_ATTRIBUTES[tag]\n const parts: string[] = [tag]\n for (const attribute of attributes) {\n const name = attribute.name.toLowerCase()\n // Event handlers, `data-*` (ALLOW_DATA_ATTR: false on both callers),\n // namespaced names, and the two shapes both configs forbade by name.\n if (!/^[a-z][a-z0-9-]*$/.test(name)) continue\n if (name.startsWith('on')) {\n note(\n 'attribute',\n `${name}=\"…\" is removed — a published page cannot run author JavaScript from an event handler.`,\n )\n continue\n }\n if (name.startsWith('data-')) continue\n if (name === 'srcdoc' || name === 'formaction' || name === 'is') continue\n if (!GLOBAL_AUTHOR_HTML_ATTRIBUTES.has(name) && !perElement?.has(name)) continue\n let value = decodeCharacterReferences(attribute.value)\n if (name === 'style') {\n const safe = sanitizeStyleValue(value)\n if (safe === null) {\n note(\n 'style',\n `the style attribute on <${tag}> is removed — inline CSS cannot use @import, expression(), behavior:, -moz-binding or javascript:.`,\n )\n continue\n }\n // Identity return means nothing was refused; anything else means a\n // `url()` was neutered, which is invisible on the page (the image\n // simply never appears) and therefore the case most worth naming.\n if (safe !== value) {\n note(\n 'url',\n `a url() in the style attribute on <${tag}> will not load — inline CSS may only fetch over https:, data: or blob:.`,\n )\n }\n value = safe\n } else if (URL_LIST_ATTRIBUTES.has(name)) {\n const safe = filterSrcset(tag, name, value)\n if (safe === null) {\n note(\n 'url',\n `${name} on <${tag}> is removed — only https:, data: and blob: URLs are allowed.`,\n )\n continue\n }\n value = safe\n } else if (URL_ATTRIBUTES.has(name)) {\n if (!isAllowedAuthorHtmlUrl(tag, name, value)) {\n note(\n 'url',\n `${name} on <${tag}> is removed — only https:, data: and blob: URLs are allowed.`,\n )\n continue\n }\n }\n parts.push(`${name}=\"${escapeAttribute(value)}\"`)\n }\n out.push(`<${parts.join(' ')}>`)\n }\n\n /**\n * Text between a table's own elements is foster-parented OUT of the table\n * by the parser, which is a move this serializer cannot express — so it is\n * dropped and the emitted string round-trips.\n */\n const emitText = (text: string): void => {\n if (!text || inTableContext()) return\n let value = text\n // The parser drops ONE U+000A immediately after a `<pre>` start tag. The\n // serializer is supposed to put one back when the text still begins with\n // one, but that half is not implemented everywhere (jsdom does not), so\n // the ONLY string that survives both a browser and a test DOM unchanged\n // is one with no leading newline at all. Emitting that costs an author a\n // blank first line inside a `<pre>` and buys a fixed point.\n if (justOpenedPre && open[open.length - 1] === 'pre') {\n value = value.replace(/^\\n+/, '')\n }\n justOpenedPre = false\n if (!value) return\n out.push(escapeText(value))\n }\n\n while (index < html.length) {\n const next = html.indexOf('<', index)\n if (next === -1) {\n emitText(html.slice(index))\n break\n }\n if (next > index) emitText(html.slice(index, next))\n\n const rest = html.slice(next)\n // Comments, doctypes and processing instructions: no content of ours.\n if (rest.startsWith('<!--')) {\n const close = html.indexOf('-->', next + 4)\n index = close === -1 ? html.length : close + 3\n continue\n }\n if (rest.startsWith('<!') || rest.startsWith('<?')) {\n const close = html.indexOf('>', next)\n index = close === -1 ? html.length : close + 1\n continue\n }\n\n const endTag = /^<\\/([a-zA-Z][a-zA-Z0-9]*)/.exec(rest)\n if (endTag) {\n const tag = endTag[1].toLowerCase()\n const close = html.indexOf('>', next)\n index = close === -1 ? html.length : close + 1\n const at = open.lastIndexOf(tag)\n // A stray end tag closes nothing — the browser ignores it too.\n if (at === -1) continue\n closeTo(at)\n continue\n }\n\n const startTag = /^<([a-zA-Z][a-zA-Z0-9]*)/.exec(rest)\n if (!startTag) {\n // A `<` that begins no tag is literal text, exactly as the parser\n // treats it.\n emitText('<')\n index = next + 1\n continue\n }\n\n const tag = startTag[1].toLowerCase()\n const { attributes, end, selfClosing } = readAttributes(\n html,\n next + startTag[0].length,\n )\n\n if (CONTENT_DROPPING.has(tag)) {\n note(\n 'element',\n `<${tag}> and everything inside it is removed — this element is not allowed in author HTML.`,\n )\n const { end: bodyEnd } = readRawTextElement(html, tag, end)\n index = bodyEnd\n continue\n }\n\n index = end\n\n if (!allowedElements.has(tag)) {\n // Unwrapped, not dropped — matches DOMPurify's `KEEP_CONTENT` default,\n // so a `<form>` or an unknown tag loses the element and keeps the words\n // inside it.\n note(\n 'element',\n `<${tag}> is removed — the text inside it is kept, but the element itself is not allowed in author HTML.`,\n )\n continue\n }\n\n // Context rules, both of them about reparenting rather than about safety.\n // A non-table element written directly inside a table is foster-parented\n // out of it; a table part written outside a table has its tag dropped and\n // its contents kept. Following the parser here is what keeps the emitted\n // string a fixed point of it.\n if (inTableContext() && !TABLE_PARTS.has(tag)) {\n const { end: bodyEnd } = readRawTextElement(html, tag, end)\n index = bodyEnd\n continue\n }\n if (TABLE_PARTS.has(tag) && !open.includes('table')) continue\n\n applyImpliedEndTags(tag)\n\n // `caption`, `colgroup` and `col` are only accepted in one place each;\n // written anywhere else the parser moves or drops them, so this does the\n // dropping and keeps the string stable.\n const parentTag = open[open.length - 1]\n if (\n (tag === 'caption' || tag === 'colgroup') && parentTag !== 'table'\n ) {\n continue\n }\n if (tag === 'col' && parentTag !== 'colgroup') continue\n\n openImpliedTableParents(tag)\n\n if (VOID_ELEMENTS.has(tag)) {\n emitStart(tag, attributes)\n continue\n }\n emitStart(tag, attributes)\n justOpenedPre = tag === 'pre'\n if (!selfClosing) open.push(tag)\n else out.push(`</${tag}>`)\n }\n\n closeTo(0)\n return out.join('')\n}\n"],"names":["sanitizeAuthorCss","ALLOWED_AUTHOR_HTML_ELEMENTS","Set","INLINE_AUTHOR_HTML_ELEMENTS","VOID_ELEMENTS","CONTENT_DROPPING","GLOBAL_AUTHOR_HTML_ATTRIBUTES","AUTHOR_HTML_ELEMENT_ATTRIBUTES","a","audio","blockquote","col","colgroup","data","del","details","img","ins","li","ol","q","source","td","th","time","track","video","CLOSES_OPEN_P","TABLE_CONTEXTS","TABLE_PARTS","URL_ATTRIBUTES","URL_LIST_ATTRIBUTES","ALLOWED_SCHEMES","DATA_URI_ATTRIBUTES","DATA_URI_PAYLOAD","NAMED_REFERENCES","amp","AMP","lt","LT","gt","GT","quot","QUOT","apos","nbsp","colon","semi","sol","bsol","num","period","comma","excl","quest","lpar","rpar","Tab","NewLine","equals","ast","commat","dollar","percnt","plus","minus","lowbar","decodeCharacterReferences","value","includes","replace","match","dec","hex","name","undefined","code","parseInt","Number","isFinite","String","fromCodePoint","named","escapeText","text","escapeAttribute","isAllowedAuthorHtmlUrl","tag","attribute","decoded","stripped","toLowerCase","scheme","exec","has","test","filterSrcset","kept","split","map","candidate","trim","filter","url","length","join","REFUSED_CSS","sanitizeStyleValue","css","safe","readAttributes","start","attributes","index","selfClosing","nameMatch","slice","quote","close","indexOf","stop","unquoted","push","end","readRawTextElement","contentStart","closing","RegExp","rest","body","afterTag","authorHtmlBreaksContainer","sanitized","container","tags","names","some","sanitizeAuthorHtml","html","removals","options","narrowed","allowedElements","Array","from","note","kind","message","entry","out","open","justOpenedPre","closeTo","depth","cursor","closeNearest","at","lastIndexOf","applyImpliedEndTags","heading","reduce","best","found","Math","max","section","inTableContext","openImpliedTableParents","parent","emitStart","perElement","parts","startsWith","emitText","next","endTag","startTag","bodyEnd","parentTag"],"mappings":"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"}
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
import type { AglynOrgBilling } from '../foundation';
|
|
90
90
|
/**
|
|
91
91
|
* The month key the marker is stamped with: UTC `YYYY-MM`, the same shape as
|
|
92
|
-
* `
|
|
92
|
+
* `utcMonthKey` and the `orgs/{id}/usage/{month}` document id, and the
|
|
93
93
|
* same key `usage-alerts` dedupes its own guards by. One month boundary
|
|
94
94
|
* across the platform — a cap that reset on a different day from the counter
|
|
95
95
|
* it is derived from would engage against last month's traffic.
|
|
@@ -148,6 +148,17 @@ export declare function bandwidthCapShouldEngage(options: {
|
|
|
148
148
|
usedBandwidthGb: number;
|
|
149
149
|
includedBandwidthGb: number;
|
|
150
150
|
}): boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Could this org's traffic EVER engage the cap — is it on a plan that stops
|
|
153
|
+
* at its band rather than billing past it (AGL-3474)?
|
|
154
|
+
*
|
|
155
|
+
* {@link bandwidthCapShouldEngage} with the usage left out, for a writer that
|
|
156
|
+
* has to decide whether measuring is worth its reads at all: the media CDN
|
|
157
|
+
* totals an org's month only when this is true, so a paying org's video costs
|
|
158
|
+
* no read to meter. Answered by the same predicate rather than restated, so
|
|
159
|
+
* the two cannot disagree about which plans the cap is for.
|
|
160
|
+
*/
|
|
161
|
+
export declare function bandwidthCapApplies(org: Partial<AglynOrgBilling> | null | undefined): boolean;
|
|
151
162
|
/** What a visitor sees on a capped site. */
|
|
152
163
|
export interface BandwidthCapNotice {
|
|
153
164
|
title: string;
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
*/ import { isCustomPricedPlan, planMetersInfraOverage, resolveEffectivePlan, resolveOrgEntitlements } from "./plan-entitlements.js";
|
|
88
88
|
/**
|
|
89
89
|
* The month key the marker is stamped with: UTC `YYYY-MM`, the same shape as
|
|
90
|
-
* `
|
|
90
|
+
* `utcMonthKey` and the `orgs/{id}/usage/{month}` document id, and the
|
|
91
91
|
* same key `usage-alerts` dedupes its own guards by. One month boundary
|
|
92
92
|
* across the platform — a cap that reset on a different day from the counter
|
|
93
93
|
* it is derived from would engage against last month's traffic.
|
|
@@ -158,6 +158,22 @@
|
|
|
158
158
|
}
|
|
159
159
|
return usedBandwidthGb > includedBandwidthGb;
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Could this org's traffic EVER engage the cap — is it on a plan that stops
|
|
163
|
+
* at its band rather than billing past it (AGL-3474)?
|
|
164
|
+
*
|
|
165
|
+
* {@link bandwidthCapShouldEngage} with the usage left out, for a writer that
|
|
166
|
+
* has to decide whether measuring is worth its reads at all: the media CDN
|
|
167
|
+
* totals an org's month only when this is true, so a paying org's video costs
|
|
168
|
+
* no read to meter. Answered by the same predicate rather than restated, so
|
|
169
|
+
* the two cannot disagree about which plans the cap is for.
|
|
170
|
+
*/ export function bandwidthCapApplies(org) {
|
|
171
|
+
return bandwidthCapShouldEngage({
|
|
172
|
+
org,
|
|
173
|
+
usedBandwidthGb: Number.POSITIVE_INFINITY,
|
|
174
|
+
includedBandwidthGb: resolveOrgEntitlements(org).bandwidthGb
|
|
175
|
+
});
|
|
176
|
+
}
|
|
161
177
|
/**
|
|
162
178
|
* The visitor's sentence.
|
|
163
179
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/bandwidth-cap.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The free plan's BANDWIDTH HARD CAP (AGL-1967 / AGL-2070 / AGL-2155).\n *\n * Enforced from before public signups, so the cap is proven under real\n * traffic while the cohort is small and a mistake is cheap. The requirement\n * this module exists to make true: the free/hobby tier hard-caps, so that it\n * always actually stays free.\n *\n * Until this shipped, bandwidth was the ONE billable dimension with no\n * runtime refusal anywhere. Media ingress, forms, contacts, datasets, API and\n * seats all hard-stop on free; `bandwidthGb: 5` appeared only in the plan\n * table, in reporting arithmetic and in UI. A free site that went viral\n * served every page and Aglyn paid the egress with no ceiling.\n *\n * ## Why a MARKER on the org doc, and not a check\n *\n * The obvious implementation — read the page-view counter before rendering —\n * is the one thing this cap must not do. Page views are counted AFTER the\n * fact, by a client beacon into `hosts/{id}/analytics/{day}`; consulting them\n * per render would put a Firestore read on the hot path of every public page\n * on the platform, paid by every PAYING customer too, to answer a question\n * only free orgs can ever fail. That is the exact cost the whole ISR/cache\n * design exists to avoid.\n *\n * So the decision is made where the arithmetic already happens, and its\n * verdict is denormalized onto the org document as {@link OrgBandwidthCap}.\n * The serving path then reads it from `getOrgBilling`, the org doc it ALREADY\n * loads and caches for 60 seconds as \"the suspension gate and every\n * entitlement input for the render\". **The cap costs zero additional reads.**\n *\n * ### TWO writers, and neither is on the render path (AGL-2413)\n *\n * 1. `apps/tenant/app/api/analytics/collect` — the beacon that WRITES the\n * page-view counter, on its existing 1-in-200 sample. It already sums this\n * month with this query and already loads this org doc for the abuse\n * ceiling, so the cap rides both for free.\n * 2. `api/billing/usage-alerts` — the daily sweep, which sums page views\n * ORG-WIDE across up to 100 hosts and is therefore the backstop for any\n * org running more than one site.\n *\n * The beacon is the one that matters, and it exists because the sweep alone\n * was not a cap at all: it skips every org with no `plan` field, and\n * `createOrganization` writes none — so for the whole never-subscribed free\n * tier the marker had never once been written. The general lesson: **a cap\n * that lives only in a scheduled sweep stops existing the moment the sweep\n * does not run.** Every other free dimension refuses at the\n * point of use; this one now does too.\n *\n * ### The latency, stated plainly\n *\n * The beacon closes the window to the sampling slop — at most\n * (200 x instances) page views past the band, against a free band of ~8,700 —\n * so on the common single-site free org the cap engages within a couple of\n * hundred views. It is still not a per-request throttle, and the honest way to\n * describe it is \"a free site cannot run away\", not \"a free site cannot serve\n * byte 5,368,709,121\". Do not close the remaining slop by adding a read to the\n * RENDER path; the place to close it is the CDN, which is Vercel configuration\n * and not a repo change.\n *\n * ### It RELEASES immediately, though\n *\n * Asymmetric on purpose, and this is why {@link bandwidthCapEngaged} re-reads\n * the plan rather than trusting the marker alone. An org that upgrades starts\n * serving again on the next org-doc TTL — about 60 seconds — without waiting\n * for any cron to run or any flag to be cleared. Making somebody who has just\n * paid wait until tomorrow morning for their site to come back is the failure\n * mode that would make this feature worse than the hole it closes.\n *\n * Nothing ever CLEARS the marker either: it names the month it was written\n * for, so a new month simply stops matching. A cap whose lift depends on a\n * write is a cap that stays engaged when the writer fails.\n */\n\nimport type { AglynOrgBilling, OrgBandwidthCap } from '../foundation'\nimport {\n isCustomPricedPlan,\n planMetersInfraOverage,\n resolveEffectivePlan,\n resolveOrgEntitlements,\n} from './plan-entitlements'\n\n/**\n * The month key the marker is stamped with: UTC `YYYY-MM`, the same shape as\n * `submissionMonthKey` and the `orgs/{id}/usage/{month}` document id, and the\n * same key `usage-alerts` dedupes its own guards by. One month boundary\n * across the platform — a cap that reset on a different day from the counter\n * it is derived from would engage against last month's traffic.\n */\nexport function bandwidthCapMonthKey(now: Date = new Date()): string {\n return now.toISOString().slice(0, 7)\n}\n\n/**\n * How long a crawler or uptime check is told to wait.\n *\n * NOT \"until the month rolls over\", which is the arithmetically correct\n * answer and can be three weeks. The site can come back at any moment — the\n * owner upgrades and the release path above restores it in about a minute —\n * so a multi-week `Retry-After` would tell Google to stop looking long after\n * the site was serving again. An hour is short enough to be wrong in the\n * recoverable direction.\n */\nexport const BANDWIDTH_CAP_RETRY_AFTER_SECONDS = 3600\n\n/**\n * Is this org's serving currently refused by the free-plan bandwidth cap?\n *\n * THREE conditions, and the second and third are the ones that matter:\n *\n * 1. the marker names the CURRENT month (a stale marker is not a cap);\n * 2. the org's plan does not meter infrastructure overage — i.e. it is the\n * free plan, or a paid plan held as a staff comp (AGL-3034), which has no\n * subscription to bill past the band either. Paid plans BILL bandwidth\n * past the band and are never cut off; that promise is older than this\n * cap and this function is where it is kept. Re-derived from the CURRENT\n * plan on every read, which is what releases an upgraded org without a\n * write;\n * 3. the plan is not custom-priced, and its band is finite. Enterprise also\n * answers false to `planMetersInfraOverage` — its price is negotiated —\n * and since 2026-09-07 its band is a FINITE fallback (twice Agency's), so\n * \"unmetered and finite\" would describe a contracted customer exactly as\n * it describes a free one. Its traffic is paid for by agreement, not by\n * invoice, and this cap is the free plan's rule alone: an enterprise org\n * carrying a marker some future writer left behind is never refused.\n * Belt and braces: today's writer cannot produce that marker, and this\n * makes it impossible rather than merely unreachable.\n *\n * Fail OPEN on anything unrecognised. A missing org, a malformed marker or a\n * plan this build cannot read all serve the site: an unreadable org doc is an\n * outage, and taking a customer's website down over one is not a cap, it is\n * the incident.\n */\nexport function bandwidthCapEngaged(\n org: Partial<AglynOrgBilling> | null | undefined,\n now: Date = new Date(),\n): boolean {\n if (!org) return false\n if (planMetersInfraOverage(org)) return false\n if (isCustomPricedPlan(resolveEffectivePlan(org))) return false\n if (!Number.isFinite(resolveOrgEntitlements(org).bandwidthGb)) return false\n const cap = org.bandwidthCap\n if (!cap || typeof cap !== 'object') return false\n return cap.month === bandwidthCapMonthKey(now)\n}\n\n/**\n * Should the cap be ENGAGED for this org-month? The writer's half of the\n * question, kept beside the reader's so the two cannot drift apart.\n *\n * Takes the measured figure and the band rather than reading anything, so the\n * cron passes in the very numbers it alerts on. `>` and not `>=`: an org\n * exactly at its band has used what it was given and is not over it.\n */\nexport function bandwidthCapShouldEngage(options: {\n org: Partial<AglynOrgBilling> | null | undefined\n usedBandwidthGb: number\n includedBandwidthGb: number\n}): boolean {\n const { org, usedBandwidthGb, includedBandwidthGb } = options\n if (planMetersInfraOverage(org)) return false\n // The reader's third condition, kept in step: a custom-priced plan is paid\n // for by agreement and is never paused, whatever its band resolves to.\n if (isCustomPricedPlan(resolveEffectivePlan(org))) return false\n if (!Number.isFinite(includedBandwidthGb) || !(includedBandwidthGb > 0)) {\n return false\n }\n return usedBandwidthGb > includedBandwidthGb\n}\n\n/** What a visitor sees on a capped site. */\nexport interface BandwidthCapNotice {\n title: string\n body: string\n}\n\n/**\n * The visitor's sentence.\n *\n * Constrained the same way the form abuse-ceiling copy is (AGL-1666), and for\n * the same reason: the person reading it is a stranger to the site, so it\n * must not disclose the owner's plan, their bill, or anything about their\n * account beyond the one fact that makes the page they asked for absent. It\n * says what happened, that it is not their fault and not permanent, and when\n * it ends — and then stops. \"Upgrade your plan\" belongs in the owner's\n * console and their inbox, which is where the usage alert already puts it,\n * not on a page shown to their readers.\n *\n * Deliberately NOT routed through `lockdownNotice`. A lockdown is the panic\n * button — staff-set, security or billing, \"contact support\" — and its copy\n * would be a lie here in both directions: nobody took this site down and\n * support cannot lift it. Reusing the RENDERING channel (the 503 notice page)\n * while keeping the WORDS separate is the split that keeps both honest.\n */\nexport function bandwidthCapNotice(): BandwidthCapNotice {\n return {\n title: 'Over the monthly traffic limit',\n body:\n 'This site has used all of the traffic included with it this month, ' +\n 'so its pages are paused until the start of next month. Nothing is ' +\n 'wrong with your connection, and nothing here has been removed.',\n }\n}\n\n/**\n * The refusal code on the machine-readable half of the verdict, so a caller\n * can tell a capped site from a locked one. The two answer the same HTTP\n * status from the same notice page on purpose — a 503 with `Retry-After` is\n * the only status that tells a crawler \"come back\", where a 402 or a 403\n * risks a customer's site being de-indexed over one busy month — so the\n * status discriminates nothing and this code is the whole discriminator.\n */\nexport const BANDWIDTH_CAP_CODE = 'bandwidth-cap'\n"],"names":["isCustomPricedPlan","planMetersInfraOverage","resolveEffectivePlan","resolveOrgEntitlements","bandwidthCapMonthKey","now","Date","toISOString","slice","BANDWIDTH_CAP_RETRY_AFTER_SECONDS","bandwidthCapEngaged","org","Number","isFinite","bandwidthGb","cap","bandwidthCap","month","bandwidthCapShouldEngage","options","usedBandwidthGb","includedBandwidthGb","bandwidthCapNotice","title","body","BANDWIDTH_CAP_CODE"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuEC,GAGD,SACEA,kBAAkB,EAClBC,sBAAsB,EACtBC,oBAAoB,EACpBC,sBAAsB,QACjB,yBAAqB;AAE5B;;;;;;CAMC,GACD,OAAO,SAASC,qBAAqBC,MAAY,IAAIC,MAAM;IACzD,OAAOD,IAAIE,WAAW,GAAGC,KAAK,CAAC,GAAG;AACpC;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAMC,oCAAoC,KAAI;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GACD,OAAO,SAASC,oBACdC,GAAgD,EAChDN,MAAY,IAAIC,MAAM;IAEtB,IAAI,CAACK,KAAK,OAAO;IACjB,IAAIV,uBAAuBU,MAAM,OAAO;IACxC,IAAIX,mBAAmBE,qBAAqBS,OAAO,OAAO;IAC1D,IAAI,CAACC,OAAOC,QAAQ,CAACV,uBAAuBQ,KAAKG,WAAW,GAAG,OAAO;IACtE,MAAMC,MAAMJ,IAAIK,YAAY;IAC5B,IAAI,CAACD,OAAO,OAAOA,QAAQ,UAAU,OAAO;IAC5C,OAAOA,IAAIE,KAAK,KAAKb,qBAAqBC;AAC5C;AAEA;;;;;;;CAOC,GACD,OAAO,SAASa,yBAAyBC,OAIxC;IACC,MAAM,EAAER,GAAG,EAAES,eAAe,EAAEC,mBAAmB,EAAE,GAAGF;IACtD,IAAIlB,uBAAuBU,MAAM,OAAO;IACxC,2EAA2E;IAC3E,uEAAuE;IACvE,IAAIX,mBAAmBE,qBAAqBS,OAAO,OAAO;IAC1D,IAAI,CAACC,OAAOC,QAAQ,CAACQ,wBAAwB,CAAEA,CAAAA,sBAAsB,CAAA,GAAI;QACvE,OAAO;IACT;IACA,OAAOD,kBAAkBC;AAC3B;AAQA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASC;IACd,OAAO;QACLC,OAAO;QACPC,MACE,wEACA,uEACA;IACJ;AACF;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMC,qBAAqB,gBAAe"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/bandwidth-cap.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The free plan's BANDWIDTH HARD CAP (AGL-1967 / AGL-2070 / AGL-2155).\n *\n * Enforced from before public signups, so the cap is proven under real\n * traffic while the cohort is small and a mistake is cheap. The requirement\n * this module exists to make true: the free/hobby tier hard-caps, so that it\n * always actually stays free.\n *\n * Until this shipped, bandwidth was the ONE billable dimension with no\n * runtime refusal anywhere. Media ingress, forms, contacts, datasets, API and\n * seats all hard-stop on free; `bandwidthGb: 5` appeared only in the plan\n * table, in reporting arithmetic and in UI. A free site that went viral\n * served every page and Aglyn paid the egress with no ceiling.\n *\n * ## Why a MARKER on the org doc, and not a check\n *\n * The obvious implementation — read the page-view counter before rendering —\n * is the one thing this cap must not do. Page views are counted AFTER the\n * fact, by a client beacon into `hosts/{id}/analytics/{day}`; consulting them\n * per render would put a Firestore read on the hot path of every public page\n * on the platform, paid by every PAYING customer too, to answer a question\n * only free orgs can ever fail. That is the exact cost the whole ISR/cache\n * design exists to avoid.\n *\n * So the decision is made where the arithmetic already happens, and its\n * verdict is denormalized onto the org document as {@link OrgBandwidthCap}.\n * The serving path then reads it from `getOrgBilling`, the org doc it ALREADY\n * loads and caches for 60 seconds as \"the suspension gate and every\n * entitlement input for the render\". **The cap costs zero additional reads.**\n *\n * ### TWO writers, and neither is on the render path (AGL-2413)\n *\n * 1. `apps/tenant/app/api/analytics/collect` — the beacon that WRITES the\n * page-view counter, on its existing 1-in-200 sample. It already sums this\n * month with this query and already loads this org doc for the abuse\n * ceiling, so the cap rides both for free.\n * 2. `api/billing/usage-alerts` — the daily sweep, which sums page views\n * ORG-WIDE across up to 100 hosts and is therefore the backstop for any\n * org running more than one site.\n *\n * The beacon is the one that matters, and it exists because the sweep alone\n * was not a cap at all: it skips every org with no `plan` field, and\n * `createOrganization` writes none — so for the whole never-subscribed free\n * tier the marker had never once been written. The general lesson: **a cap\n * that lives only in a scheduled sweep stops existing the moment the sweep\n * does not run.** Every other free dimension refuses at the\n * point of use; this one now does too.\n *\n * ### The latency, stated plainly\n *\n * The beacon closes the window to the sampling slop — at most\n * (200 x instances) page views past the band, against a free band of ~8,700 —\n * so on the common single-site free org the cap engages within a couple of\n * hundred views. It is still not a per-request throttle, and the honest way to\n * describe it is \"a free site cannot run away\", not \"a free site cannot serve\n * byte 5,368,709,121\". Do not close the remaining slop by adding a read to the\n * RENDER path; the place to close it is the CDN, which is Vercel configuration\n * and not a repo change.\n *\n * ### It RELEASES immediately, though\n *\n * Asymmetric on purpose, and this is why {@link bandwidthCapEngaged} re-reads\n * the plan rather than trusting the marker alone. An org that upgrades starts\n * serving again on the next org-doc TTL — about 60 seconds — without waiting\n * for any cron to run or any flag to be cleared. Making somebody who has just\n * paid wait until tomorrow morning for their site to come back is the failure\n * mode that would make this feature worse than the hole it closes.\n *\n * Nothing ever CLEARS the marker either: it names the month it was written\n * for, so a new month simply stops matching. A cap whose lift depends on a\n * write is a cap that stays engaged when the writer fails.\n */\n\nimport type { AglynOrgBilling, OrgBandwidthCap } from '../foundation'\nimport {\n isCustomPricedPlan,\n planMetersInfraOverage,\n resolveEffectivePlan,\n resolveOrgEntitlements,\n} from './plan-entitlements'\n\n/**\n * The month key the marker is stamped with: UTC `YYYY-MM`, the same shape as\n * `utcMonthKey` and the `orgs/{id}/usage/{month}` document id, and the\n * same key `usage-alerts` dedupes its own guards by. One month boundary\n * across the platform — a cap that reset on a different day from the counter\n * it is derived from would engage against last month's traffic.\n */\nexport function bandwidthCapMonthKey(now: Date = new Date()): string {\n return now.toISOString().slice(0, 7)\n}\n\n/**\n * How long a crawler or uptime check is told to wait.\n *\n * NOT \"until the month rolls over\", which is the arithmetically correct\n * answer and can be three weeks. The site can come back at any moment — the\n * owner upgrades and the release path above restores it in about a minute —\n * so a multi-week `Retry-After` would tell Google to stop looking long after\n * the site was serving again. An hour is short enough to be wrong in the\n * recoverable direction.\n */\nexport const BANDWIDTH_CAP_RETRY_AFTER_SECONDS = 3600\n\n/**\n * Is this org's serving currently refused by the free-plan bandwidth cap?\n *\n * THREE conditions, and the second and third are the ones that matter:\n *\n * 1. the marker names the CURRENT month (a stale marker is not a cap);\n * 2. the org's plan does not meter infrastructure overage — i.e. it is the\n * free plan, or a paid plan held as a staff comp (AGL-3034), which has no\n * subscription to bill past the band either. Paid plans BILL bandwidth\n * past the band and are never cut off; that promise is older than this\n * cap and this function is where it is kept. Re-derived from the CURRENT\n * plan on every read, which is what releases an upgraded org without a\n * write;\n * 3. the plan is not custom-priced, and its band is finite. Enterprise also\n * answers false to `planMetersInfraOverage` — its price is negotiated —\n * and since 2026-09-07 its band is a FINITE fallback (twice Agency's), so\n * \"unmetered and finite\" would describe a contracted customer exactly as\n * it describes a free one. Its traffic is paid for by agreement, not by\n * invoice, and this cap is the free plan's rule alone: an enterprise org\n * carrying a marker some future writer left behind is never refused.\n * Belt and braces: today's writer cannot produce that marker, and this\n * makes it impossible rather than merely unreachable.\n *\n * Fail OPEN on anything unrecognised. A missing org, a malformed marker or a\n * plan this build cannot read all serve the site: an unreadable org doc is an\n * outage, and taking a customer's website down over one is not a cap, it is\n * the incident.\n */\nexport function bandwidthCapEngaged(\n org: Partial<AglynOrgBilling> | null | undefined,\n now: Date = new Date(),\n): boolean {\n if (!org) return false\n if (planMetersInfraOverage(org)) return false\n if (isCustomPricedPlan(resolveEffectivePlan(org))) return false\n if (!Number.isFinite(resolveOrgEntitlements(org).bandwidthGb)) return false\n const cap = org.bandwidthCap\n if (!cap || typeof cap !== 'object') return false\n return cap.month === bandwidthCapMonthKey(now)\n}\n\n/**\n * Should the cap be ENGAGED for this org-month? The writer's half of the\n * question, kept beside the reader's so the two cannot drift apart.\n *\n * Takes the measured figure and the band rather than reading anything, so the\n * cron passes in the very numbers it alerts on. `>` and not `>=`: an org\n * exactly at its band has used what it was given and is not over it.\n */\nexport function bandwidthCapShouldEngage(options: {\n org: Partial<AglynOrgBilling> | null | undefined\n usedBandwidthGb: number\n includedBandwidthGb: number\n}): boolean {\n const { org, usedBandwidthGb, includedBandwidthGb } = options\n if (planMetersInfraOverage(org)) return false\n // The reader's third condition, kept in step: a custom-priced plan is paid\n // for by agreement and is never paused, whatever its band resolves to.\n if (isCustomPricedPlan(resolveEffectivePlan(org))) return false\n if (!Number.isFinite(includedBandwidthGb) || !(includedBandwidthGb > 0)) {\n return false\n }\n return usedBandwidthGb > includedBandwidthGb\n}\n\n/**\n * Could this org's traffic EVER engage the cap — is it on a plan that stops\n * at its band rather than billing past it (AGL-3474)?\n *\n * {@link bandwidthCapShouldEngage} with the usage left out, for a writer that\n * has to decide whether measuring is worth its reads at all: the media CDN\n * totals an org's month only when this is true, so a paying org's video costs\n * no read to meter. Answered by the same predicate rather than restated, so\n * the two cannot disagree about which plans the cap is for.\n */\nexport function bandwidthCapApplies(\n org: Partial<AglynOrgBilling> | null | undefined,\n): boolean {\n return bandwidthCapShouldEngage({\n org,\n usedBandwidthGb: Number.POSITIVE_INFINITY,\n includedBandwidthGb: resolveOrgEntitlements(org).bandwidthGb,\n })\n}\n\n/** What a visitor sees on a capped site. */\nexport interface BandwidthCapNotice {\n title: string\n body: string\n}\n\n/**\n * The visitor's sentence.\n *\n * Constrained the same way the form abuse-ceiling copy is (AGL-1666), and for\n * the same reason: the person reading it is a stranger to the site, so it\n * must not disclose the owner's plan, their bill, or anything about their\n * account beyond the one fact that makes the page they asked for absent. It\n * says what happened, that it is not their fault and not permanent, and when\n * it ends — and then stops. \"Upgrade your plan\" belongs in the owner's\n * console and their inbox, which is where the usage alert already puts it,\n * not on a page shown to their readers.\n *\n * Deliberately NOT routed through `lockdownNotice`. A lockdown is the panic\n * button — staff-set, security or billing, \"contact support\" — and its copy\n * would be a lie here in both directions: nobody took this site down and\n * support cannot lift it. Reusing the RENDERING channel (the 503 notice page)\n * while keeping the WORDS separate is the split that keeps both honest.\n */\nexport function bandwidthCapNotice(): BandwidthCapNotice {\n return {\n title: 'Over the monthly traffic limit',\n body:\n 'This site has used all of the traffic included with it this month, ' +\n 'so its pages are paused until the start of next month. Nothing is ' +\n 'wrong with your connection, and nothing here has been removed.',\n }\n}\n\n/**\n * The refusal code on the machine-readable half of the verdict, so a caller\n * can tell a capped site from a locked one. The two answer the same HTTP\n * status from the same notice page on purpose — a 503 with `Retry-After` is\n * the only status that tells a crawler \"come back\", where a 402 or a 403\n * risks a customer's site being de-indexed over one busy month — so the\n * status discriminates nothing and this code is the whole discriminator.\n */\nexport const BANDWIDTH_CAP_CODE = 'bandwidth-cap'\n"],"names":["isCustomPricedPlan","planMetersInfraOverage","resolveEffectivePlan","resolveOrgEntitlements","bandwidthCapMonthKey","now","Date","toISOString","slice","BANDWIDTH_CAP_RETRY_AFTER_SECONDS","bandwidthCapEngaged","org","Number","isFinite","bandwidthGb","cap","bandwidthCap","month","bandwidthCapShouldEngage","options","usedBandwidthGb","includedBandwidthGb","bandwidthCapApplies","POSITIVE_INFINITY","bandwidthCapNotice","title","body","BANDWIDTH_CAP_CODE"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuEC,GAGD,SACEA,kBAAkB,EAClBC,sBAAsB,EACtBC,oBAAoB,EACpBC,sBAAsB,QACjB,yBAAqB;AAE5B;;;;;;CAMC,GACD,OAAO,SAASC,qBAAqBC,MAAY,IAAIC,MAAM;IACzD,OAAOD,IAAIE,WAAW,GAAGC,KAAK,CAAC,GAAG;AACpC;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAMC,oCAAoC,KAAI;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GACD,OAAO,SAASC,oBACdC,GAAgD,EAChDN,MAAY,IAAIC,MAAM;IAEtB,IAAI,CAACK,KAAK,OAAO;IACjB,IAAIV,uBAAuBU,MAAM,OAAO;IACxC,IAAIX,mBAAmBE,qBAAqBS,OAAO,OAAO;IAC1D,IAAI,CAACC,OAAOC,QAAQ,CAACV,uBAAuBQ,KAAKG,WAAW,GAAG,OAAO;IACtE,MAAMC,MAAMJ,IAAIK,YAAY;IAC5B,IAAI,CAACD,OAAO,OAAOA,QAAQ,UAAU,OAAO;IAC5C,OAAOA,IAAIE,KAAK,KAAKb,qBAAqBC;AAC5C;AAEA;;;;;;;CAOC,GACD,OAAO,SAASa,yBAAyBC,OAIxC;IACC,MAAM,EAAER,GAAG,EAAES,eAAe,EAAEC,mBAAmB,EAAE,GAAGF;IACtD,IAAIlB,uBAAuBU,MAAM,OAAO;IACxC,2EAA2E;IAC3E,uEAAuE;IACvE,IAAIX,mBAAmBE,qBAAqBS,OAAO,OAAO;IAC1D,IAAI,CAACC,OAAOC,QAAQ,CAACQ,wBAAwB,CAAEA,CAAAA,sBAAsB,CAAA,GAAI;QACvE,OAAO;IACT;IACA,OAAOD,kBAAkBC;AAC3B;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,oBACdX,GAAgD;IAEhD,OAAOO,yBAAyB;QAC9BP;QACAS,iBAAiBR,OAAOW,iBAAiB;QACzCF,qBAAqBlB,uBAAuBQ,KAAKG,WAAW;IAC9D;AACF;AAQA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASU;IACd,OAAO;QACLC,OAAO;QACPC,MACE,wEACA,uEACA;IACJ;AACF;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMC,qBAAqB,gBAAe"}
|
|
@@ -17,6 +17,25 @@
|
|
|
17
17
|
import type { AglynNodeSchema, NodeId, ReusableComponentIcon, ReusableComponentProp } from '../foundation';
|
|
18
18
|
export * from './reusable-component-keys';
|
|
19
19
|
export * from './reusable-prop-values';
|
|
20
|
+
/**
|
|
21
|
+
* The props a placement whose design root names the design itself (a placed
|
|
22
|
+
* form) renders with, or `undefined` when neither side has any (AGL-3494).
|
|
23
|
+
*
|
|
24
|
+
* The design root's own props are the base — the saved form's submit label,
|
|
25
|
+
* success message and settings, edited once for every page. The placement's
|
|
26
|
+
* own props go over them ONE PROP AT A TIME, and only the ones it actually
|
|
27
|
+
* sets ({@link isSetOnPlacement}), so a page that left the label alone shows
|
|
28
|
+
* the form's, and a page that wrote one keeps it. The placement's `root`
|
|
29
|
+
* attribute slice (AGL-3285), already merged into `rootProps` by the graft, is
|
|
30
|
+
* laid over last so it beats the placement's own props, as it always has.
|
|
31
|
+
*
|
|
32
|
+
* Classes are joined rather than replaced, the design root's first, for the
|
|
33
|
+
* reason the instance merge joins them: the show/hide steps toggle a class on
|
|
34
|
+
* the element, and either author may have named one.
|
|
35
|
+
*
|
|
36
|
+
* Exported for the spec that pins the merge; the graft is its only caller.
|
|
37
|
+
*/
|
|
38
|
+
export declare function placementPropsOverRoot(rootProps: unknown, placementProps: unknown, rootAttrs?: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
20
39
|
/**
|
|
21
40
|
* Whether a node is a placed form that names a form entity — the second kind
|
|
22
41
|
* of node that stands for a tree stored on another document (see
|