@warpgogol/werkstatt-shared 0.14.0 → 0.15.0
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/README.md +31 -13
- package/README.uk.md +25 -15
- package/package.json +519 -762
- package/src/{share/agent → agent}/ard-catalog.ts +6 -1
- package/src/{share/agent → agent}/dns-aid.test.ts +2 -2
- package/src/{share/agent → agent}/manifest.ts +15 -1
- package/src/{share/agent → agent}/search.ts +6 -1
- package/src/{share → agent}/tests/api-catalog.test.ts +2 -2
- package/src/{share → agent}/tests/ard-catalog.test.ts +2 -2
- package/src/{share → agent}/tests/capability.test.ts +1 -1
- package/src/{share → agent}/tests/fleet-catalog.test.ts +1 -1
- package/src/{share → agent}/tests/knowledge.test.ts +2 -2
- package/src/{share → agent}/tests/manifest.test.ts +1 -1
- package/src/{share → agent}/tests/mcp-card.test.ts +2 -2
- package/src/agent/tests/omit-empty.test.ts +33 -0
- package/src/{share → agent}/tests/openapi.test.ts +2 -2
- package/src/{share → agent}/tests/proof.test.ts +1 -1
- package/src/{share → attribution}/material-credits.ts +4 -4
- package/src/{share → checks}/check-hints.ts +6 -1
- package/src/checks/index.ts +4 -1
- package/src/checks/lib/astro-site-url.ts +6 -1
- package/src/checks/lib/i18n.ts +14 -0
- package/src/checks/result-helpers.ts +22 -5
- package/src/checks/tests/remediation-auto-populate.test.ts +83 -0
- package/src/{share → client-scripts}/counter-utils.ts +6 -2
- package/src/{share/scripts → client-scripts}/external-link-qr.ts +11 -16
- package/src/{share/scripts → client-scripts}/external-links.ts +12 -1
- package/src/{share/scripts → client-scripts}/gsap-counter.ts +8 -3
- package/src/client-scripts/index.ts +35 -0
- package/src/{share/scripts → client-scripts}/inline-number-animation.ts +1 -1
- package/src/{share/scripts → client-scripts}/lenis.ts +6 -1
- package/src/{share/scripts → client-scripts}/lordicon.ts +6 -1
- package/src/{share/scripts → client-scripts}/orchestrator.ts +10 -1
- package/src/{share/scripts → client-scripts}/scheduler.ts +6 -1
- package/src/client-scripts/scroll-spy.ts +106 -0
- package/src/client-scripts/tests/external-link-qr.test.ts +8 -0
- package/src/client-scripts/tests/external-links.test.ts +25 -0
- package/src/client-scripts/tests/gsap-counter.test.ts +8 -0
- package/src/client-scripts/tests/gsap-mountain-journey.test.ts +8 -0
- package/src/client-scripts/tests/gsap-parallax.test.ts +8 -0
- package/src/client-scripts/tests/gsap-reveal.test.ts +8 -0
- package/src/client-scripts/tests/gsap-stagger.test.ts +8 -0
- package/src/client-scripts/tests/inline-number-animation.test.ts +8 -0
- package/src/client-scripts/tests/lenis.test.ts +8 -0
- package/src/client-scripts/tests/live-photos.test.ts +8 -0
- package/src/client-scripts/tests/lordicon.test.ts +8 -0
- package/src/client-scripts/tests/orchestrator.test.ts +47 -0
- package/src/client-scripts/tests/scheduler.test.ts +23 -0
- package/src/client-scripts/tests/scroll-spy.test.ts +204 -0
- package/src/{share/scripts → client-scripts/tests}/video-player.test.ts +51 -31
- package/src/component/contracts.ts +206 -0
- package/src/component/index.ts +11 -0
- package/src/{share → content}/content-reference.ts +3 -3
- package/src/{share/content → content}/entity-id.ts +6 -3
- package/src/content/index.ts +3 -0
- package/src/{share/content → content}/merge.ts +6 -1
- package/src/content/system-manifest.test.ts +107 -0
- package/src/content/system-manifest.ts +12 -94
- package/src/{share → content}/tests/resolve-field-path.test.ts +1 -1
- package/src/{share → content}/tests/substitute-deep.test.ts +1 -1
- package/src/content-discipline/index.ts +19 -0
- package/src/{share/content-discipline → content-discipline}/parsers.ts +2 -2
- package/src/{share/env.d.ts → env.d.ts} +6 -3
- package/src/fingerprint/canonical-json.ts +731 -0
- package/src/fingerprint/index.ts +12 -0
- package/src/fingerprint/primitives.ts +72 -0
- package/src/{share/i18n → i18n}/localization.ts +6 -2
- package/src/integration/adapters.ts +10 -71
- package/src/integration/dispatch.ts +1 -1
- package/src/integration/funnel.ts +6 -1
- package/src/integration/index.ts +101 -4
- package/src/integration/lagebild-destination-adapter.ts +108 -0
- package/src/integration/lagebild-ingress.ts +7 -12
- package/src/integration/orchestration.ts +11 -10
- package/src/integration/tests/deliver-event.test.ts +1 -1
- package/src/integration/tests/dispatch.test.ts +4 -4
- package/src/integration/tests/hub-runtime.test.ts +22 -14
- package/src/integration/vitest.config.ts +8 -0
- package/src/kernel/desired-state.ts +227 -0
- package/src/kernel/diagnostic.ts +289 -0
- package/src/kernel/fs-atomic.ts +131 -0
- package/src/kernel/index.ts +15 -0
- package/src/kernel/tests/fs-atomic.test.ts +150 -0
- package/src/kernel/tests/workspace-io.test.ts +187 -0
- package/src/kernel/types.ts +459 -0
- package/src/kernel/workspace-io.ts +248 -0
- package/src/{share → knowledge}/tests/claim.test.ts +1 -1
- package/src/{share → knowledge}/tests/derivation.test.ts +1 -1
- package/src/{share → knowledge}/tests/freshness.test.ts +1 -1
- package/src/{share → knowledge}/tests/ledger.test.ts +1 -1
- package/src/{share → knowledge}/tests/plan.test.ts +1 -1
- package/src/{share → knowledge}/tests/source.test.ts +1 -1
- package/src/legal/index.ts +15 -0
- package/src/{share/middleware → middleware}/access-protection.ts +8 -8
- package/src/{share → node}/dev-props-validator.ts +6 -1
- package/src/{share → node}/import-scan.ts +23 -3
- package/src/{share → node}/semantic/derived-prices-loader.ts +4 -2
- package/src/{share → node}/semantic/markdown-twin-provenance.ts +1 -1
- package/src/node/tests/derived-prices-loader.test.ts +59 -0
- package/src/node/tests/markdown-twin-provenance.test.ts +152 -0
- package/src/observability/env-resolver.ts +6 -1
- package/src/observability/index.ts +6 -1
- package/src/observability/otlp-converter.ts +6 -1
- package/src/observability/otlp-transport.ts +6 -1
- package/src/observability/pusher.ts +6 -1
- package/src/observability/redact.ts +6 -1
- package/src/observability/typed-refs.ts +6 -2
- package/src/observability/vitest.config.ts +1 -0
- package/src/{share → offers}/formula-eval.ts +3 -3
- package/src/{share → offers}/tests/formula-eval.test.ts +2 -2
- package/src/onboarding/index.ts +1 -0
- package/src/{share → onboarding}/tests/onboarding-yaml.test.ts +1 -1
- package/src/ontology/archetype-registry.ts +6 -1
- package/src/ontology/archetypes/index.json +33 -1
- package/src/ontology/archetypes/index.yaml +27 -1
- package/src/ontology/archetypes/sections/entity-grid.yaml +46 -0
- package/src/ontology/archetypes/sections/exit-diagram.yaml +43 -0
- package/src/ontology/biome-token-projection.ts +6 -1
- package/src/ontology/cosmic/index.ts +6 -1
- package/src/ontology/cosmic/moon-catalog.ts +6 -1
- package/src/ontology/cosmic/planet-catalog.ts +6 -1
- package/src/ontology/cosmic/star-catalog.ts +6 -1
- package/src/ontology/enums.ts +6 -1
- package/src/ontology/external-surfaces/index.ts +1 -1
- package/src/ontology/index.ts +6 -2
- package/src/ontology/manifest.ts +26 -4
- package/src/ontology/operations/artifact-store.ts +45 -0
- package/src/ontology/operations/dht.ts +98 -0
- package/src/ontology/operations/handoff.ts +92 -0
- package/src/ontology/operations/index.ts +187 -1
- package/src/ontology/operations/leitstand.ts +99 -0
- package/src/ontology/operations/materialization.ts +90 -0
- package/src/ontology/operations/mission.ts +111 -0
- package/src/ontology/operations/naming-policy.ts +132 -0
- package/src/ontology/operations/notausgang.ts +64 -0
- package/src/ontology/operations/release.ts +121 -0
- package/src/ontology/operations/sternsystem.ts +192 -0
- package/src/ontology/operations/werkstatt.ts +48 -0
- package/src/ontology/schemas/biome.ts +7 -2
- package/src/ontology/schemas/capability.ts +1 -1
- package/src/ontology/schemas/constellation.ts +6 -1
- package/src/ontology/schemas/index.ts +6 -2
- package/src/ontology/schemas/manifest-resolver.ts +6 -1
- package/src/ontology/schemas/page-entry.ts +9 -5
- package/src/ontology/schemas/system/growth.ts +6 -1
- package/src/ontology/schemas/system/manifest.ts +40 -1
- package/src/ontology/schemas/system/page-output.ts +7 -6
- package/src/ontology/schemas/system/tests/growth.test.ts +22 -0
- package/src/ontology/schemas/system/tests/integrations.test.ts +12 -0
- package/src/ontology/schemas/system/tests/manifest.test.ts +22 -0
- package/src/ontology/schemas/system/tests/page-output.test.ts +13 -0
- package/src/ontology/schemas/system/tests/release.test.ts +22 -0
- package/src/ontology/schemas/system/tests/text.test.ts +18 -0
- package/src/ontology/schemas/system/tests/verification.test.ts +12 -0
- package/src/ontology/schemas/system/text.ts +15 -0
- package/src/ontology/schemas/system.ts +6 -2
- package/src/ontology/schemas/tests/biome.test.ts +19 -0
- package/src/ontology/schemas/tests/capability.test.ts +22 -0
- package/src/ontology/schemas/tests/constellation.test.ts +22 -0
- package/src/ontology/schemas/tests/dns-records.test.ts +13 -0
- package/src/ontology/schemas/tests/manifest-resolver.test.ts +13 -0
- package/src/ontology/schemas/tests/page-entry.test.ts +22 -0
- package/src/ontology/schemas/tests/section-archetype.test.ts +12 -0
- package/src/ontology/schemas/tests/site-family.test.ts +18 -0
- package/src/ontology/schemas/tests/system.test.ts +20 -0
- package/src/ontology/shared-section-props/body-fragments.ts +2 -0
- package/src/ontology/tests/sternsystem-owner.test.ts +1 -1
- package/src/ontology/vitest.config.ts +1 -0
- package/src/{share → page}/page.ts +13 -9
- package/src/{share → page}/tests/dev-props-validator.test.ts +1 -1
- package/src/{share → page}/tests/hero-section-numbering-parity.test.ts +1 -1
- package/src/passport/index.ts +6 -1
- package/src/passport/key-rotate.ts +10 -2
- package/src/passport/schema.ts +6 -1
- package/src/passport/sign.ts +8 -3
- package/src/passport/vitest.config.ts +1 -0
- package/src/pbt/index.test.ts +95 -0
- package/src/pbt/index.ts +251 -0
- package/src/plugin/plugin-contract.ts +1 -1
- package/src/{share → policy}/entitlement.ts +24 -4
- package/src/{share → policy}/feature-policy.ts +8 -3
- package/src/{share → policy}/visibility.ts +7 -2
- package/src/redirects/redirects.ts +117 -0
- package/src/{share → redirects/tests}/redirects.test.ts +1 -1
- package/src/remediation/remediation-catalog.ts +316 -0
- package/src/{share/routes → routes}/template-filter.ts +1 -1
- package/src/{share → runtime}/runtime-context.ts +7 -2
- package/src/{share → runtime}/shared-context.ts +1 -1
- package/src/{share/schemas → schemas}/claims.ts +2 -2
- package/src/{share/schemas → schemas}/effects.ts +6 -1
- package/src/{share/schemas → schemas}/features.ts +1 -1
- package/src/{share/schemas → schemas}/index.ts +3 -3
- package/src/{share/schemas → schemas}/navigation.ts +6 -1
- package/src/{share/schemas → schemas}/page-base.ts +6 -2
- package/src/{share/schemas → schemas}/person.ts +1 -1
- package/src/{share/schemas → schemas}/standard-list-item.ts +1 -1
- package/src/schemas/tests/claim-records.test.ts +52 -0
- package/src/schemas/tests/claims.test.ts +23 -0
- package/src/schemas/tests/effects.test.ts +116 -0
- package/src/schemas/tests/features.test.ts +61 -0
- package/src/schemas/tests/horizontal-align.test.ts +16 -0
- package/src/schemas/tests/icon-color.test.ts +16 -0
- package/src/schemas/tests/live-photo.test.ts +40 -0
- package/src/schemas/tests/material-credit.test.ts +51 -0
- package/src/schemas/tests/media.test.ts +26 -0
- package/src/schemas/tests/navigation.test.ts +58 -0
- package/src/schemas/tests/page-base.test.ts +19 -0
- package/src/schemas/tests/participant.test.ts +26 -0
- package/src/schemas/tests/person.test.ts +12 -0
- package/src/schemas/tests/print.test.ts +30 -0
- package/src/schemas/tests/section-background.test.ts +31 -0
- package/src/schemas/tests/section-body.test.ts +16 -0
- package/src/schemas/tests/section-cards.test.ts +17 -0
- package/src/schemas/tests/section-cta.test.ts +41 -0
- package/src/schemas/tests/section-header.test.ts +31 -0
- package/src/schemas/tests/section-image.test.ts +24 -0
- package/src/schemas/tests/section-motion.test.ts +29 -0
- package/src/schemas/tests/section-shell.test.ts +42 -0
- package/src/schemas/tests/section-stats.test.ts +17 -0
- package/src/schemas/tests/site-background.test.ts +33 -0
- package/src/schemas/tests/standard-list-item.test.ts +16 -0
- package/src/{share → sections}/rfc0042-utils.ts +1 -1
- package/src/{share/semantic → semantic}/block-extractors/index.ts +20 -1
- package/src/{share/semantic → semantic}/business-projection.ts +95 -1
- package/src/semantic/canonical-uri.ts +54 -0
- package/src/{share/semantic → semantic}/extract.ts +100 -1
- package/src/semantic/fact-extraction.test.ts +47 -0
- package/src/semantic/fact-extraction.ts +46 -0
- package/src/semantic/freshness.ts +67 -0
- package/src/{share/semantic → semantic}/ids.ts +6 -1
- package/src/{share/semantic → semantic}/index.ts +9 -2
- package/src/{share/semantic → semantic}/jsonld/breadcrumb.ts +6 -2
- package/src/{share/semantic → semantic}/jsonld/context.ts +6 -2
- package/src/{share/semantic → semantic}/jsonld/faq.ts +6 -2
- package/src/{share/semantic → semantic}/jsonld/initiative.ts +6 -1
- package/src/{share/semantic → semantic}/jsonld/organization.ts +6 -1
- package/src/{share/semantic → semantic}/jsonld/person.ts +6 -2
- package/src/{share/semantic → semantic}/jsonld/service.ts +6 -2
- package/src/semantic/jsonld/shared.ts +36 -0
- package/src/semantic/jsonld/tests/article.test.ts +89 -0
- package/src/semantic/jsonld/tests/breadcrumb.test.ts +62 -0
- package/src/semantic/jsonld/tests/collection-list.test.ts +39 -0
- package/src/semantic/jsonld/tests/faq.test.ts +60 -0
- package/src/semantic/jsonld/tests/initiative.test.ts +56 -0
- package/src/semantic/jsonld/tests/organization.test.ts +123 -0
- package/src/semantic/jsonld/tests/person.test.ts +89 -0
- package/src/semantic/jsonld/tests/service.test.ts +65 -0
- package/src/semantic/jsonld/tests/shared.test.ts +41 -0
- package/src/semantic/jsonld/tests/video.test.ts +84 -0
- package/src/semantic/jsonld/tests/webpage.test.ts +93 -0
- package/src/semantic/jsonld/tests/website.test.ts +27 -0
- package/src/semantic/jsonld/types.ts +24 -0
- package/src/{share/semantic → semantic}/jsonld/webpage.ts +6 -1
- package/src/semantic/jsonld/website.ts +32 -0
- package/src/{share/semantic → semantic}/jsonld.ts +6 -1
- package/src/{share/semantic → semantic}/llms.ts +41 -3
- package/src/{share/semantic → semantic}/models.ts +27 -38
- package/src/{share/semantic → semantic}/organization-profile.ts +3 -0
- package/src/{share/semantic → semantic}/page-builders/markdown-page.ts +6 -3
- package/src/{share/semantic → semantic}/page-utils.ts +6 -3
- package/src/semantic/tests/ai.test.ts +55 -0
- package/src/semantic/tests/block-extraction.test.ts +34 -0
- package/src/{share/tests/breadcrumbs.test.ts → semantic/tests/breadcrumbs-0229.test.ts} +1 -1
- package/src/semantic/tests/breadcrumbs.test.ts +127 -0
- package/src/{share → semantic}/tests/build-page-price-markers.test.ts +2 -2
- package/src/semantic/tests/build-page.test.ts +122 -0
- package/src/semantic/tests/business-projection.test.ts +367 -0
- package/src/semantic/tests/canonical-uri.test.ts +56 -0
- package/src/semantic/tests/extract.test.ts +209 -0
- package/src/semantic/tests/feed.test.ts +113 -0
- package/src/semantic/tests/freshness.pbt.test.ts +108 -0
- package/src/semantic/tests/helpers.ts +32 -0
- package/src/semantic/tests/ids.test.ts +118 -0
- package/src/{share/tests/image-sitemap.test.ts → semantic/tests/image-sitemap-0150.test.ts} +1 -1
- package/src/semantic/tests/image-sitemap.test.ts +109 -0
- package/src/{share → semantic}/tests/jsonld-service.test.ts +2 -2
- package/src/{share → semantic}/tests/jsonld-video.test.ts +3 -3
- package/src/{share → semantic}/tests/jsonld-webpage.test.ts +3 -3
- package/src/semantic/tests/jsonld.test.ts +61 -0
- package/src/{share → semantic}/tests/llms-0789.test.ts +2 -2
- package/src/semantic/tests/llms-policy.test.ts +60 -0
- package/src/semantic/tests/llms.test.ts +164 -0
- package/src/semantic/tests/markdown-hygiene.test.ts +72 -0
- package/src/semantic/tests/markdown-page.test.ts +97 -0
- package/src/{share → semantic}/tests/organization-jsonld.test.ts +3 -3
- package/src/semantic/tests/organization-profile.test.ts +107 -0
- package/src/{share/tests/output-projection.test.ts → semantic/tests/output-projection-0143.test.ts} +1 -1
- package/src/semantic/tests/output-projection.test.ts +77 -0
- package/src/semantic/tests/page-markdown.test.ts +98 -0
- package/src/semantic/tests/page-utils.test.ts +88 -0
- package/src/{share/tests/price-marker-resolver.test.ts → semantic/tests/price-marker-resolver-0767.test.ts} +2 -2
- package/src/semantic/tests/price-marker-resolver.test.ts +90 -0
- package/src/semantic/tests/project-claims.pbt.test.ts +60 -0
- package/src/semantic/tests/robots.test.ts +70 -0
- package/src/{share/semantic → semantic}/tests/split-sentences.test.ts +1 -1
- package/src/{share → semantic}/tests/twin-path.test.ts +1 -1
- package/src/semantic/tests/update-stamp.test.ts +102 -0
- package/src/signing/index.ts +13 -0
- package/src/signing/key.ts +139 -0
- package/src/signing/sign.ts +66 -0
- package/src/signing/types.ts +29 -0
- package/src/{share/slug → slug}/slug-url.ts +4 -1
- package/src/stack/run-tool.ts +108 -0
- package/src/stack/stack-checks.ts +124 -0
- package/src/stack/tests/run-tool.test.ts +138 -0
- package/src/stack/tests/stack-checks.test.ts +141 -0
- package/src/stack/tests/walk-files.test.ts +131 -0
- package/src/stack/walk-files.ts +114 -0
- package/src/surface/blueprint.ts +342 -66
- package/src/surface/decision-composer.ts +6 -1
- package/src/surface/geo.ts +6 -1
- package/src/surface/index.ts +11 -7
- package/src/surface/types.ts +2 -2
- package/src/surface/vitest.config.ts +1 -0
- package/src/tests/signing-core.test.ts +129 -0
- package/src/{share → text}/css-value-normalize.ts +6 -1
- package/src/text/string-utils.ts +24 -0
- package/src/{share → text}/tests/text-normalize.test.ts +5 -0
- package/src/{share → text}/text-normalize.ts +4 -3
- package/src/{share → text}/wrap-inline-numbers.ts +1 -1
- package/src/types/axiom-study.d.ts +36 -0
- package/src/types/hls-js-light.d.ts +21 -0
- package/src/types/qrcode-browser.d.ts +23 -0
- package/src/typography/fix.pbt.test.ts +208 -0
- package/src/typography/fix.ts +148 -0
- package/src/typography/index.ts +54 -0
- package/src/typography/locale-defaults.ts +97 -0
- package/src/typography/rules-tier1.test.ts +459 -0
- package/src/typography/rules-tier1.ts +925 -0
- package/src/typography/rules-tier2-locale.pbt.test.ts +190 -0
- package/src/typography/rules-tier2-locale.test.ts +286 -0
- package/src/typography/rules-tier2-locale.ts +399 -0
- package/src/typography/rules-tier2-structure.test.ts +330 -0
- package/src/typography/rules-tier2-structure.ts +378 -0
- package/src/typography/rules-tier3-advisory.test.ts +290 -0
- package/src/typography/rules-tier3-advisory.ts +239 -0
- package/src/typography/text-surface.test.ts +244 -0
- package/src/typography/text-surface.ts +448 -0
- package/src/index.ts +0 -14
- package/src/integration/port-barrel.ts +0 -105
- package/src/share/content/index.ts +0 -16
- package/src/share/content-discipline/index.ts +0 -14
- package/src/share/index.ts +0 -86
- package/src/share/legal/index.ts +0 -1
- package/src/share/redirects.ts +0 -54
- package/src/share/scripts/index.ts +0 -26
- package/src/share/semantic/jsonld/shared.ts +0 -31
- package/src/share/semantic/jsonld/types.ts +0 -18
- package/src/share/semantic/jsonld/website.ts +0 -27
- package/src/share/string-utils.ts +0 -19
- package/src/share/types/axiom-study.d.ts +0 -22
- package/src/share/types/hls-js-light.d.ts +0 -16
- package/src/share/types/qrcode-browser.d.ts +0 -18
- package/src/share/utility-registry.yaml +0 -36
- package/src/share/vitest.config.ts +0 -8
- package/src/surface/blueprint-schema.ts +0 -314
- package/src/surface/blueprint-types.ts +0 -256
- /package/src/{share/agent → agent}/api-catalog.ts +0 -0
- /package/src/{share/agent → agent}/capability.ts +0 -0
- /package/src/{share/agent → agent}/dns-aid.ts +0 -0
- /package/src/{share/agent → agent}/fleet-catalog.ts +0 -0
- /package/src/{share/agent → agent}/index.ts +0 -0
- /package/src/{share/agent → agent}/knowledge.ts +0 -0
- /package/src/{share/agent → agent}/mcp-card.ts +0 -0
- /package/src/{share/agent → agent}/omit-empty.ts +0 -0
- /package/src/{share/agent → agent}/openapi.ts +0 -0
- /package/src/{share/agent → agent}/proof.ts +0 -0
- /package/src/{share/agent → agent}/search.test.ts +0 -0
- /package/src/{share → attribution}/attribution-display.ts +0 -0
- /package/src/{share/scripts → client-scripts}/gsap-mountain-journey.ts +0 -0
- /package/src/{share/scripts → client-scripts}/gsap-parallax.ts +0 -0
- /package/src/{share/scripts → client-scripts}/gsap-reveal.ts +0 -0
- /package/src/{share/scripts → client-scripts}/gsap-stagger.ts +0 -0
- /package/src/{share/scripts → client-scripts}/live-photos.ts +0 -0
- /package/src/{share/scripts → client-scripts}/video-player.ts +0 -0
- /package/src/{share/content → content}/resolve-field-path.ts +0 -0
- /package/src/{share/content → content}/substitute-deep.ts +0 -0
- /package/src/{share → content}/tests/content-ref-index.test.ts +0 -0
- /package/src/{share/content-discipline → content-discipline}/types.ts +0 -0
- /package/src/{share → image-provider}/image-provider.ts +0 -0
- /package/src/{share/knowledge → knowledge}/claim.ts +0 -0
- /package/src/{share/knowledge → knowledge}/derivation.ts +0 -0
- /package/src/{share/knowledge → knowledge}/freshness.ts +0 -0
- /package/src/{share/knowledge → knowledge}/index.ts +0 -0
- /package/src/{share/knowledge → knowledge}/ledger.ts +0 -0
- /package/src/{share/knowledge → knowledge}/plan.ts +0 -0
- /package/src/{share/knowledge → knowledge}/source.ts +0 -0
- /package/src/{share/legal → legal}/translation-policy.ts +0 -0
- /package/src/{share/middleware → middleware}/language-redirect.ts +0 -0
- /package/src/{share/middleware → middleware}/tests/access-protection.test.ts +0 -0
- /package/src/{share → node}/fs/index.ts +0 -0
- /package/src/{share/tests/markdown-twin-provenance.test.ts → node/tests/markdown-twin-provenance-0613.test.ts} +0 -0
- /package/src/{share → offers}/offer-capacity.ts +0 -0
- /package/src/{share/onboarding-yaml/index.ts → onboarding/yaml.ts} +0 -0
- /package/src/{share → ontology}/tests/ontology-role-by-cosmic-name.test.ts +0 -0
- /package/src/{share/routes → routes}/tests/template-filter.test.ts +0 -0
- /package/src/{share/schemas → schemas}/claim-records.ts +0 -0
- /package/src/{share/schemas → schemas}/horizontal-align.ts +0 -0
- /package/src/{share/schemas → schemas}/icon-color.ts +0 -0
- /package/src/{share/schemas → schemas}/live-photo.ts +0 -0
- /package/src/{share/schemas → schemas}/material-credit.ts +0 -0
- /package/src/{share/schemas → schemas}/media.ts +0 -0
- /package/src/{share/schemas → schemas}/participant.ts +0 -0
- /package/src/{share/schemas → schemas}/print.ts +0 -0
- /package/src/{share/schemas → schemas}/section-background.ts +0 -0
- /package/src/{share/schemas → schemas}/section-body.ts +0 -0
- /package/src/{share/schemas → schemas}/section-cards.ts +0 -0
- /package/src/{share/schemas → schemas}/section-cta.ts +0 -0
- /package/src/{share/schemas → schemas}/section-header.ts +0 -0
- /package/src/{share/schemas → schemas}/section-image.ts +0 -0
- /package/src/{share/schemas → schemas}/section-motion.ts +0 -0
- /package/src/{share/schemas → schemas}/section-shell.ts +0 -0
- /package/src/{share/schemas → schemas}/section-stats.ts +0 -0
- /package/src/{share/schemas → schemas}/site-background.ts +0 -0
- /package/src/{share/semantic → semantic}/ai.ts +0 -0
- /package/src/{share/semantic → semantic}/block-extraction.ts +0 -0
- /package/src/{share/semantic → semantic}/breadcrumbs.ts +0 -0
- /package/src/{share/semantic → semantic}/build-page.ts +0 -0
- /package/src/{share/semantic → semantic}/feed.ts +0 -0
- /package/src/{share/semantic → semantic}/image-sitemap.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/article.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/collection-list.ts +0 -0
- /package/src/{share/semantic → semantic}/jsonld/video.ts +0 -0
- /package/src/{share/semantic → semantic}/llms-policy.ts +0 -0
- /package/src/{share/semantic → semantic}/markdown-hygiene.ts +0 -0
- /package/src/{share/semantic → semantic}/output-projection.ts +0 -0
- /package/src/{share/semantic → semantic}/page-markdown.ts +0 -0
- /package/src/{share/semantic → semantic}/price-marker-resolver.ts +0 -0
- /package/src/{share/semantic → semantic}/robots.ts +0 -0
- /package/src/{share/semantic → semantic}/update-stamp.ts +0 -0
- /package/src/{share/slug → slug}/heading-slugger.ts +0 -0
- /package/src/{share/slug → slug}/index.ts +0 -0
- /package/src/{share/slug → slug}/slug-id.ts +0 -0
- /package/src/{share/slug → slug}/strategies.ts +0 -0
- /package/src/{share/slug → slug}/tests/slug.test.ts +0 -0
- /package/src/{share → text}/tests/text-normalize.pbt.test.ts +0 -0
- /package/src/{share → text}/text-position.ts +0 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>RFC-0849: Bounded canonical JSON identity bytes — object-only opaque snapshot, strict RFC 8785 JCS encoder, and SHA-256 hash delegation.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not import node:crypto, stableJsonHash, or any plugin module — delegate to byteHash only.</item>
|
|
6
|
+
<item>Do not define certification schemas, Diagnostic, or persistence — those belong to later packets.</item>
|
|
7
|
+
<item>Do not change stableJsonHash or its consumers.</item>
|
|
8
|
+
</non-goals>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
<CHANGE_SUMMARY>
|
|
11
|
+
<item>RFC-0849: initial implementation of werkstatt/canonical-json@1 — bounded object-root profile of RFC 8785 JCS.</item>
|
|
12
|
+
</CHANGE_SUMMARY>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { byteHash, type Sha256Digest } from "./primitives.ts";
|
|
16
|
+
|
|
17
|
+
export const CANONICAL_JSON_V1 = "werkstatt/canonical-json@1" as const;
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Opaque branded type
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
declare const canonicalJsonObjectV1Brand: unique symbol;
|
|
24
|
+
|
|
25
|
+
export type CanonicalJsonObjectV1 = {
|
|
26
|
+
readonly [canonicalJsonObjectV1Brand]: true;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const brandedRegistry = new WeakSet<object>();
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Path segments
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export type CanonicalJsonPathSegmentV1 =
|
|
36
|
+
| { readonly kind: "array-index"; readonly index: number }
|
|
37
|
+
| { readonly kind: "object-key"; readonly sortedIndex: number };
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Failure types
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
export type CanonicalJsonFailureCodeV1 =
|
|
44
|
+
| "CERT-CANONICAL-DOMAIN-01"
|
|
45
|
+
| "CERT-CANONICAL-TRAVERSAL-01"
|
|
46
|
+
| "CERT-CANONICAL-UNICODE-01"
|
|
47
|
+
| "CERT-CANONICAL-LIMIT-01";
|
|
48
|
+
|
|
49
|
+
export interface CanonicalJsonFailureV1 {
|
|
50
|
+
readonly ok: false;
|
|
51
|
+
readonly code: CanonicalJsonFailureCodeV1;
|
|
52
|
+
readonly path: readonly CanonicalJsonPathSegmentV1[];
|
|
53
|
+
readonly omittedPathSegments: number;
|
|
54
|
+
readonly message: string;
|
|
55
|
+
readonly limit?:
|
|
56
|
+
"bytes" | "depth" | "nodes" | "object-keys" | "array-items" | "string-bytes" | "key-bytes";
|
|
57
|
+
readonly actual?: number;
|
|
58
|
+
readonly maximum?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CanonicalJsonSuccessV1 {
|
|
62
|
+
readonly ok: true;
|
|
63
|
+
readonly value: CanonicalJsonObjectV1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type CanonicalJsonObjectSnapshotResultV1 = CanonicalJsonSuccessV1 | CanonicalJsonFailureV1;
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Invariant error
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export class CanonicalJsonInvariantError extends Error {
|
|
73
|
+
readonly code = "CERT-CANONICAL-BRAND-01" as const;
|
|
74
|
+
constructor(message: string) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = "CanonicalJsonInvariantError";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Hard limits
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
const MAX_BYTES = 8 * 1024 * 1024;
|
|
85
|
+
const MAX_DEPTH = 64;
|
|
86
|
+
const MAX_NODES = 250_000;
|
|
87
|
+
const MAX_OBJECT_KEYS = 10_000;
|
|
88
|
+
const MAX_ARRAY_ITEMS = 100_000;
|
|
89
|
+
const MAX_STRING_BYTES = 1024 * 1024;
|
|
90
|
+
const MAX_KEY_BYTES = 1024;
|
|
91
|
+
const MAX_MESSAGE_BYTES = 512;
|
|
92
|
+
const MAX_PATH_SEGMENTS = 64;
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// UTF-16 lone surrogate check
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
function hasLoneSurrogate(str: string): boolean {
|
|
99
|
+
for (let i = 0; i < str.length; i++) {
|
|
100
|
+
const code = str.charCodeAt(i);
|
|
101
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
102
|
+
if (i + 1 >= str.length) return true;
|
|
103
|
+
const next = str.charCodeAt(i + 1);
|
|
104
|
+
if (next < 0xdc00 || next > 0xdfff) return true;
|
|
105
|
+
i++;
|
|
106
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function utf8ByteLength(str: string): number {
|
|
114
|
+
return Buffer.byteLength(str, "utf8");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// RFC 8785 key comparator (lexicographic over UTF-16 code units)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
function compareJcsKeys(a: string, b: string): number {
|
|
122
|
+
const len = Math.min(a.length, b.length);
|
|
123
|
+
for (let i = 0; i < len; i++) {
|
|
124
|
+
const ca = a.charCodeAt(i);
|
|
125
|
+
const cb = b.charCodeAt(i);
|
|
126
|
+
if (ca !== cb) return ca - cb;
|
|
127
|
+
}
|
|
128
|
+
return a.length - b.length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// RFC 8785 number serialization (§3.2.2.3 / ECMA-262 §7.1.12.1)
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
function serializeNumber(value: number): string {
|
|
136
|
+
if (Object.is(value, -0)) {
|
|
137
|
+
throw new CanonicalJsonInvariantError("negative zero rejected by Werkstatt profile");
|
|
138
|
+
}
|
|
139
|
+
if (!Number.isFinite(value)) {
|
|
140
|
+
throw new CanonicalJsonInvariantError("non-finite numbers rejected by Werkstatt profile");
|
|
141
|
+
}
|
|
142
|
+
if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
143
|
+
throw new CanonicalJsonInvariantError("unsafe integer rejected by Werkstatt profile");
|
|
144
|
+
}
|
|
145
|
+
if (value === 0) return "0";
|
|
146
|
+
if (Number.isInteger(value)) return value.toString();
|
|
147
|
+
|
|
148
|
+
// Non-integer finite numbers: use ECMA-262 §7.1.12.1 Number.prototype.toString
|
|
149
|
+
// which produces the shortest representation that round-trips.
|
|
150
|
+
return value.toString();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// RFC 8785 string escaping (§3.2.2.2)
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
function escapeString(str: string): string {
|
|
158
|
+
let out = '"';
|
|
159
|
+
for (let i = 0; i < str.length; i++) {
|
|
160
|
+
const code = str.charCodeAt(i);
|
|
161
|
+
switch (code) {
|
|
162
|
+
case 0x22:
|
|
163
|
+
out += '\\"';
|
|
164
|
+
break;
|
|
165
|
+
case 0x5c:
|
|
166
|
+
out += "\\\\";
|
|
167
|
+
break;
|
|
168
|
+
case 0x08:
|
|
169
|
+
out += "\\b";
|
|
170
|
+
break;
|
|
171
|
+
case 0x09:
|
|
172
|
+
out += "\\t";
|
|
173
|
+
break;
|
|
174
|
+
case 0x0a:
|
|
175
|
+
out += "\\n";
|
|
176
|
+
break;
|
|
177
|
+
case 0x0c:
|
|
178
|
+
out += "\\f";
|
|
179
|
+
break;
|
|
180
|
+
case 0x0d:
|
|
181
|
+
out += "\\r";
|
|
182
|
+
break;
|
|
183
|
+
default:
|
|
184
|
+
if (code < 0x20) {
|
|
185
|
+
out += "\\u" + code.toString(16).padStart(4, "0");
|
|
186
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
187
|
+
// High surrogate — must be followed by low surrogate
|
|
188
|
+
const next = str.charCodeAt(i + 1);
|
|
189
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
190
|
+
// Valid surrogate pair — emit raw UTF-16 code units
|
|
191
|
+
out += String.fromCharCode(code);
|
|
192
|
+
out += String.fromCharCode(next);
|
|
193
|
+
i++;
|
|
194
|
+
} else {
|
|
195
|
+
// This should have been caught by hasLoneSurrogate, but defensive
|
|
196
|
+
throw new CanonicalJsonInvariantError(
|
|
197
|
+
"lone high surrogate in string — should have been rejected",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
out += String.fromCharCode(code);
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
out += '"';
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Traversal state
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
interface TraversalState {
|
|
215
|
+
nodes: number;
|
|
216
|
+
bytes: number;
|
|
217
|
+
output: string[];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type FailureResult = CanonicalJsonFailureV1;
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Deep freeze
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
function deepFreeze(value: unknown): void {
|
|
227
|
+
if (value === null || typeof value !== "object") return;
|
|
228
|
+
if (Array.isArray(value)) {
|
|
229
|
+
for (const item of value) deepFreeze(item);
|
|
230
|
+
Object.freeze(value);
|
|
231
|
+
} else {
|
|
232
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
233
|
+
deepFreeze((value as Record<string, unknown>)[key]);
|
|
234
|
+
}
|
|
235
|
+
Object.freeze(value);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Snapshot builder
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
function isPlainObjectRoot(value: unknown): boolean {
|
|
244
|
+
if (value === null || typeof value !== "object") return false;
|
|
245
|
+
const proto = Object.getPrototypeOf(value);
|
|
246
|
+
return proto === Object.prototype || proto === null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isPlainObject(value: unknown): boolean {
|
|
250
|
+
return isPlainObjectRoot(value);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function buildSnapshot(input: unknown): CanonicalJsonObjectSnapshotResultV1 {
|
|
254
|
+
// Root must be a plain object
|
|
255
|
+
if (!isPlainObjectRoot(input)) {
|
|
256
|
+
return fail("CERT-CANONICAL-DOMAIN-01", [], 0, "root must be a plain object");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const visited = new Set<object>();
|
|
260
|
+
const state: TraversalState = { nodes: 1, bytes: 0, output: [] };
|
|
261
|
+
|
|
262
|
+
if (state.nodes > MAX_NODES) {
|
|
263
|
+
return failLimit("nodes", [], 0, state.nodes, MAX_NODES);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const result = traverseObject(input as Record<string, unknown>, [], 0, state, visited);
|
|
267
|
+
if (result) return result;
|
|
268
|
+
|
|
269
|
+
const outputStr = state.output.join("");
|
|
270
|
+
const outputBytes = Buffer.byteLength(outputStr, "utf8");
|
|
271
|
+
if (outputBytes > MAX_BYTES) {
|
|
272
|
+
return failLimit("bytes", [], 0, outputBytes, MAX_BYTES);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Build the detached frozen copy
|
|
276
|
+
const detached = buildDetached(input);
|
|
277
|
+
deepFreeze(detached);
|
|
278
|
+
const root = detached as unknown as CanonicalJsonObjectV1;
|
|
279
|
+
brandedRegistry.add(detached as object);
|
|
280
|
+
|
|
281
|
+
return { ok: true, value: root };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function traverseObject(
|
|
285
|
+
obj: Record<string, unknown>,
|
|
286
|
+
path: CanonicalJsonPathSegmentV1[],
|
|
287
|
+
depth: number,
|
|
288
|
+
state: TraversalState,
|
|
289
|
+
visited: Set<object>,
|
|
290
|
+
): FailureResult | null {
|
|
291
|
+
if (depth > MAX_DEPTH) {
|
|
292
|
+
return failLimit("depth", path, 0, depth, MAX_DEPTH);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Cycle/alias detection
|
|
296
|
+
if (visited.has(obj)) {
|
|
297
|
+
return fail("CERT-CANONICAL-TRAVERSAL-01", path, 0, "cycle or repeated reference detected");
|
|
298
|
+
}
|
|
299
|
+
visited.add(obj);
|
|
300
|
+
|
|
301
|
+
// Capture own keys
|
|
302
|
+
let ownKeys: string[];
|
|
303
|
+
try {
|
|
304
|
+
ownKeys = Reflect.ownKeys(obj) as string[];
|
|
305
|
+
} catch {
|
|
306
|
+
return fail("CERT-CANONICAL-TRAVERSAL-01", path, 0, "Reflect.ownKeys threw during capture");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check for symbol keys
|
|
310
|
+
const symbols = Object.getOwnPropertySymbols(obj);
|
|
311
|
+
if (symbols.length > 0) {
|
|
312
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, 0, "symbol keys are not permitted");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check all descriptors are enumerable data descriptors
|
|
316
|
+
for (const key of ownKeys) {
|
|
317
|
+
let desc: PropertyDescriptor;
|
|
318
|
+
try {
|
|
319
|
+
desc = Object.getOwnPropertyDescriptor(obj, key)!;
|
|
320
|
+
} catch {
|
|
321
|
+
return fail("CERT-CANONICAL-TRAVERSAL-01", path, 0, "getOwnPropertyDescriptor threw");
|
|
322
|
+
}
|
|
323
|
+
if (!desc) {
|
|
324
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, 0, `missing descriptor for key`);
|
|
325
|
+
}
|
|
326
|
+
if (!desc.enumerable) {
|
|
327
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, 0, "non-enumerable property is not permitted");
|
|
328
|
+
}
|
|
329
|
+
if (desc.get !== undefined || desc.set !== undefined) {
|
|
330
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, 0, "accessor property is not permitted");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check key count
|
|
335
|
+
if (ownKeys.length > MAX_OBJECT_KEYS) {
|
|
336
|
+
return failLimit("object-keys", path, 0, ownKeys.length, MAX_OBJECT_KEYS);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check key bytes and lone surrogates
|
|
340
|
+
for (const key of ownKeys) {
|
|
341
|
+
const keyBytes = utf8ByteLength(key);
|
|
342
|
+
if (keyBytes > MAX_KEY_BYTES) {
|
|
343
|
+
return failLimit("key-bytes", path, 0, keyBytes, MAX_KEY_BYTES);
|
|
344
|
+
}
|
|
345
|
+
if (hasLoneSurrogate(key)) {
|
|
346
|
+
return fail("CERT-CANONICAL-UNICODE-01", path, 0, "lone surrogate in object key");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Sort keys by JCS comparator
|
|
351
|
+
const sortedKeys = [...ownKeys].sort(compareJcsKeys);
|
|
352
|
+
|
|
353
|
+
// Verify key list didn't change during capture
|
|
354
|
+
let postKeys: string[];
|
|
355
|
+
try {
|
|
356
|
+
postKeys = Reflect.ownKeys(obj) as string[];
|
|
357
|
+
} catch {
|
|
358
|
+
return fail(
|
|
359
|
+
"CERT-CANONICAL-TRAVERSAL-01",
|
|
360
|
+
path,
|
|
361
|
+
0,
|
|
362
|
+
"Reflect.ownKeys threw during post-capture",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (postKeys.length !== ownKeys.length || !postKeys.every((k, i) => k === ownKeys[i])) {
|
|
366
|
+
return fail("CERT-CANONICAL-TRAVERSAL-01", path, 0, "own-key shape changed during capture");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Emit object
|
|
370
|
+
state.output.push("{");
|
|
371
|
+
|
|
372
|
+
for (let i = 0; i < sortedKeys.length; i++) {
|
|
373
|
+
const key = sortedKeys[i];
|
|
374
|
+
if (i > 0) state.output.push(",");
|
|
375
|
+
|
|
376
|
+
// Emit key
|
|
377
|
+
state.output.push(escapeString(key));
|
|
378
|
+
|
|
379
|
+
state.output.push(":");
|
|
380
|
+
|
|
381
|
+
// Read value once
|
|
382
|
+
let value: unknown;
|
|
383
|
+
try {
|
|
384
|
+
value = (obj as Record<string, unknown>)[key];
|
|
385
|
+
} catch {
|
|
386
|
+
const seg: CanonicalJsonPathSegmentV1 = { kind: "object-key", sortedIndex: i };
|
|
387
|
+
const newPath = pushPath(path, seg);
|
|
388
|
+
return fail(
|
|
389
|
+
"CERT-CANONICAL-TRAVERSAL-01",
|
|
390
|
+
newPath.path,
|
|
391
|
+
newPath.omitted,
|
|
392
|
+
"property read threw",
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const seg: CanonicalJsonPathSegmentV1 = { kind: "object-key", sortedIndex: i };
|
|
397
|
+
const newPath = pushPath(path, seg);
|
|
398
|
+
const result = traverseValue(value, newPath.path, newPath.omitted, depth + 1, state, visited);
|
|
399
|
+
if (result) return result;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
state.output.push("}");
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function traverseArray(
|
|
407
|
+
arr: unknown[],
|
|
408
|
+
path: CanonicalJsonPathSegmentV1[],
|
|
409
|
+
omitted: number,
|
|
410
|
+
depth: number,
|
|
411
|
+
state: TraversalState,
|
|
412
|
+
visited: Set<object>,
|
|
413
|
+
): FailureResult | null {
|
|
414
|
+
if (depth > MAX_DEPTH) {
|
|
415
|
+
return failLimit("depth", path, omitted, depth, MAX_DEPTH);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (visited.has(arr)) {
|
|
419
|
+
return fail(
|
|
420
|
+
"CERT-CANONICAL-TRAVERSAL-01",
|
|
421
|
+
path,
|
|
422
|
+
omitted,
|
|
423
|
+
"cycle or repeated reference detected",
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
visited.add(arr);
|
|
427
|
+
|
|
428
|
+
// Check array is dense (no holes)
|
|
429
|
+
const len = arr.length;
|
|
430
|
+
for (let i = 0; i < len; i++) {
|
|
431
|
+
if (!(i in arr)) {
|
|
432
|
+
const seg: CanonicalJsonPathSegmentV1 = { kind: "array-index", index: i };
|
|
433
|
+
const newPath = pushPath(path, seg);
|
|
434
|
+
return fail(
|
|
435
|
+
"CERT-CANONICAL-DOMAIN-01",
|
|
436
|
+
newPath.path,
|
|
437
|
+
newPath.omitted,
|
|
438
|
+
"sparse array hole detected",
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Check for extra own keys beyond indices
|
|
444
|
+
const arrKeys = Object.keys(arr);
|
|
445
|
+
for (const k of arrKeys) {
|
|
446
|
+
if (!/^(?:0|[1-9]\d*)$/.test(k) || Number(k) >= len) {
|
|
447
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, "array has extra own keys");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check symbol keys
|
|
452
|
+
const symbols = Object.getOwnPropertySymbols(arr);
|
|
453
|
+
if (symbols.length > 0) {
|
|
454
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, "array has symbol keys");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (len > MAX_ARRAY_ITEMS) {
|
|
458
|
+
return failLimit("array-items", path, omitted, len, MAX_ARRAY_ITEMS);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
state.output.push("[");
|
|
462
|
+
for (let i = 0; i < len; i++) {
|
|
463
|
+
if (i > 0) state.output.push(",");
|
|
464
|
+
const seg: CanonicalJsonPathSegmentV1 = { kind: "array-index", index: i };
|
|
465
|
+
const newPath = pushPath(path, seg);
|
|
466
|
+
const result = traverseValue(arr[i], newPath.path, newPath.omitted, depth + 1, state, visited);
|
|
467
|
+
if (result) return result;
|
|
468
|
+
}
|
|
469
|
+
state.output.push("]");
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function traverseValue(
|
|
474
|
+
value: unknown,
|
|
475
|
+
path: CanonicalJsonPathSegmentV1[],
|
|
476
|
+
omitted: number,
|
|
477
|
+
depth: number,
|
|
478
|
+
state: TraversalState,
|
|
479
|
+
visited: Set<object>,
|
|
480
|
+
): FailureResult | null {
|
|
481
|
+
state.nodes++;
|
|
482
|
+
if (state.nodes > MAX_NODES) {
|
|
483
|
+
return failLimit("nodes", path, omitted, state.nodes, MAX_NODES);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (value === null) {
|
|
487
|
+
state.output.push("null");
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (value === undefined) {
|
|
492
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, "undefined is not permitted");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const type = typeof value;
|
|
496
|
+
|
|
497
|
+
if (type === "boolean") {
|
|
498
|
+
state.output.push(value ? "true" : "false");
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (type === "number") {
|
|
503
|
+
const num = value as number;
|
|
504
|
+
if (Object.is(num, NaN)) {
|
|
505
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, "NaN is not permitted");
|
|
506
|
+
}
|
|
507
|
+
if (!Number.isFinite(num)) {
|
|
508
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, "Infinity is not permitted");
|
|
509
|
+
}
|
|
510
|
+
if (Object.is(num, -0)) {
|
|
511
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, "negative zero is not permitted");
|
|
512
|
+
}
|
|
513
|
+
if (Number.isInteger(num) && !Number.isSafeInteger(num)) {
|
|
514
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, "unsafe integer is not permitted");
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
state.output.push(serializeNumber(num));
|
|
518
|
+
} catch (e) {
|
|
519
|
+
if (e instanceof CanonicalJsonInvariantError) {
|
|
520
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, e.message);
|
|
521
|
+
}
|
|
522
|
+
throw e;
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (type === "bigint") {
|
|
528
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, "bigint is not permitted");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (type === "symbol") {
|
|
532
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, "symbol is not permitted");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (type === "function") {
|
|
536
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, "function is not permitted");
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (type === "string") {
|
|
540
|
+
const strBytes = utf8ByteLength(value as string);
|
|
541
|
+
if (strBytes > MAX_STRING_BYTES) {
|
|
542
|
+
return failLimit("string-bytes", path, omitted, strBytes, MAX_STRING_BYTES);
|
|
543
|
+
}
|
|
544
|
+
if (hasLoneSurrogate(value as string)) {
|
|
545
|
+
return fail("CERT-CANONICAL-UNICODE-01", path, omitted, "lone surrogate in string value");
|
|
546
|
+
}
|
|
547
|
+
state.output.push(escapeString(value as string));
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (type === "object") {
|
|
552
|
+
// Reject host objects and class instances
|
|
553
|
+
if (!isPlainObject(value) && !Array.isArray(value)) {
|
|
554
|
+
// Check for toJSON
|
|
555
|
+
if (typeof (value as { toJSON?: unknown }).toJSON === "function") {
|
|
556
|
+
return fail(
|
|
557
|
+
"CERT-CANONICAL-DOMAIN-01",
|
|
558
|
+
path,
|
|
559
|
+
omitted,
|
|
560
|
+
"toJSON customization is not permitted",
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
// Check for Date, Map, Set, RegExp, Error, typed arrays, etc.
|
|
564
|
+
const proto = Object.getPrototypeOf(value);
|
|
565
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
566
|
+
const ctor = proto?.constructor?.name ?? "unknown";
|
|
567
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, `non-plain object type: ${ctor}`);
|
|
568
|
+
}
|
|
569
|
+
// null-prototype object — treat as plain
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (Array.isArray(value)) {
|
|
573
|
+
return traverseArray(value, path, omitted, depth, state, visited);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return traverseObject(value as Record<string, unknown>, path, depth, state, visited);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return fail("CERT-CANONICAL-DOMAIN-01", path, omitted, `unsupported value type: ${type}`);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
// Detached copy builder
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
|
|
586
|
+
function buildDetached(input: unknown): unknown {
|
|
587
|
+
if (input === null || typeof input !== "object") return input;
|
|
588
|
+
if (Array.isArray(input)) {
|
|
589
|
+
const arr: unknown[] = [];
|
|
590
|
+
for (const item of input) arr.push(buildDetached(item));
|
|
591
|
+
return arr;
|
|
592
|
+
}
|
|
593
|
+
const obj: Record<string, unknown> = Object.create(null);
|
|
594
|
+
for (const key of Object.keys(input as Record<string, unknown>)) {
|
|
595
|
+
obj[key] = buildDetached((input as Record<string, unknown>)[key]);
|
|
596
|
+
}
|
|
597
|
+
return obj;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// Path helpers
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
|
|
604
|
+
function pushPath(
|
|
605
|
+
path: CanonicalJsonPathSegmentV1[],
|
|
606
|
+
seg: CanonicalJsonPathSegmentV1,
|
|
607
|
+
): { path: CanonicalJsonPathSegmentV1[]; omitted: number } {
|
|
608
|
+
if (path.length >= MAX_PATH_SEGMENTS) {
|
|
609
|
+
return { path, omitted: 1 };
|
|
610
|
+
}
|
|
611
|
+
return { path: [...path, seg], omitted: 0 };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
// Failure helpers
|
|
616
|
+
// ---------------------------------------------------------------------------
|
|
617
|
+
|
|
618
|
+
function truncateMessage(msg: string): string {
|
|
619
|
+
const buf = Buffer.from(msg, "utf8");
|
|
620
|
+
if (buf.length <= MAX_MESSAGE_BYTES) return msg;
|
|
621
|
+
return buf.subarray(0, MAX_MESSAGE_BYTES).toString("utf8");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function fail(
|
|
625
|
+
code: CanonicalJsonFailureCodeV1,
|
|
626
|
+
path: CanonicalJsonPathSegmentV1[],
|
|
627
|
+
omitted: number,
|
|
628
|
+
message: string,
|
|
629
|
+
): CanonicalJsonFailureV1 {
|
|
630
|
+
return {
|
|
631
|
+
ok: false,
|
|
632
|
+
code,
|
|
633
|
+
path,
|
|
634
|
+
omittedPathSegments: omitted,
|
|
635
|
+
message: truncateMessage(message),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function failLimit(
|
|
640
|
+
limit: NonNullable<CanonicalJsonFailureV1["limit"]>,
|
|
641
|
+
path: CanonicalJsonPathSegmentV1[],
|
|
642
|
+
omitted: number,
|
|
643
|
+
actual: number,
|
|
644
|
+
maximum: number,
|
|
645
|
+
): CanonicalJsonFailureV1 {
|
|
646
|
+
return {
|
|
647
|
+
ok: false,
|
|
648
|
+
code: "CERT-CANONICAL-LIMIT-01",
|
|
649
|
+
path,
|
|
650
|
+
omittedPathSegments: omitted,
|
|
651
|
+
message: truncateMessage(`${limit} limit exceeded: ${actual} > ${maximum}`),
|
|
652
|
+
limit,
|
|
653
|
+
actual,
|
|
654
|
+
maximum,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ---------------------------------------------------------------------------
|
|
659
|
+
// Public API
|
|
660
|
+
// ---------------------------------------------------------------------------
|
|
661
|
+
|
|
662
|
+
export function snapshotCanonicalJsonObjectV1(input: unknown): CanonicalJsonObjectSnapshotResultV1 {
|
|
663
|
+
return buildSnapshot(input);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function isCanonicalJsonObjectV1(value: unknown): value is CanonicalJsonObjectV1 {
|
|
667
|
+
return typeof value === "object" && value !== null && brandedRegistry.has(value as object);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export function canonicalJsonBytesV1(value: CanonicalJsonObjectV1): Uint8Array {
|
|
671
|
+
if (!isCanonicalJsonObjectV1(value)) {
|
|
672
|
+
throw new CanonicalJsonInvariantError(
|
|
673
|
+
"CERT-CANONICAL-BRAND-01: value is not a runtime-branded CanonicalJsonObjectV1",
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
// Re-encode from the detached frozen copy
|
|
677
|
+
const output: string[] = [];
|
|
678
|
+
encodeValue(value, output);
|
|
679
|
+
const str = output.join("");
|
|
680
|
+
return Buffer.from(str, "utf8");
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function canonicalJsonHashV1(value: CanonicalJsonObjectV1): Sha256Digest {
|
|
684
|
+
const bytes = canonicalJsonBytesV1(value);
|
|
685
|
+
return byteHash(bytes);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ---------------------------------------------------------------------------
|
|
689
|
+
// Encoder (operates on detached frozen copy — no caller mutation possible)
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
|
|
692
|
+
function encodeValue(value: unknown, output: string[]): void {
|
|
693
|
+
if (value === null) {
|
|
694
|
+
output.push("null");
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (typeof value === "boolean") {
|
|
698
|
+
output.push(value ? "true" : "false");
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (typeof value === "number") {
|
|
702
|
+
output.push(serializeNumber(value));
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (typeof value === "string") {
|
|
706
|
+
output.push(escapeString(value));
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (Array.isArray(value)) {
|
|
710
|
+
output.push("[");
|
|
711
|
+
for (let i = 0; i < value.length; i++) {
|
|
712
|
+
if (i > 0) output.push(",");
|
|
713
|
+
encodeValue(value[i], output);
|
|
714
|
+
}
|
|
715
|
+
output.push("]");
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
// Plain object (null-prototype after detachment)
|
|
719
|
+
if (typeof value === "object" && value !== null) {
|
|
720
|
+
const keys = Object.keys(value as Record<string, unknown>).sort(compareJcsKeys);
|
|
721
|
+
output.push("{");
|
|
722
|
+
for (let i = 0; i < keys.length; i++) {
|
|
723
|
+
if (i > 0) output.push(",");
|
|
724
|
+
output.push(escapeString(keys[i]));
|
|
725
|
+
output.push(":");
|
|
726
|
+
encodeValue((value as Record<string, unknown>)[keys[i]], output);
|
|
727
|
+
}
|
|
728
|
+
output.push("}");
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
}
|