@@ -0,0 +1,39 @@
|
||||
name: Verify
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: verify-${{ gitea.repository }}-${{ gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
CI: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
- name: Select declared npm version
|
||||
run: npm install --global npm@11.17.0
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Audit runtime dependencies
|
||||
run: npm audit --omit=dev --audit-level=moderate
|
||||
- name: Check, test, and build
|
||||
run: npm run check
|
||||
- name: Install browser engines
|
||||
run: npx playwright install --with-deps chromium firefox webkit
|
||||
- name: Browser tests
|
||||
run: npm run test:browser
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.0 - 2026-09-02
|
||||
|
||||
- Added worker-isolated, bounded `pattern` and `patternProperties` validation,
|
||||
along with broader draft-aware object, array, numeric, dependency and
|
||||
combiner assertions.
|
||||
- Added deeper recursive schema comparison and focused XML-language structural
|
||||
inventories while retaining explicit unsupported-keyword diagnostics.
|
||||
|
||||
## 0.1.2 - 2026-09-01
|
||||
|
||||
- Refused invalid sample claims when `false` JSON Schemas are reached through local references or required combiners, while selecting the first viable `anyOf` or `oneOf` branch.
|
||||
|
||||
@@ -8,13 +8,13 @@ Schema Tools is a standalone local-first application in the [add·ideas Toolbox]
|
||||
|
||||
- Safe local workspaces of up to 20 JSON Schema, OpenAPI, XSD, Relax NG XML-syntax, or Schematron documents
|
||||
- Reference inventory and graph with local relative resolution, fragments, missing-target diagnostics, and explicit blocking of remote, absolute, and escaping references
|
||||
- Focused, CSP-safe JSON Schema instance validation for Draft 6/7, 2019-09, and 2020-12 with bounded local JSON Pointer references
|
||||
- Focused, CSP-safe JSON Schema instance validation for Draft 6/7, 2019-09, and 2020-12 with bounded local JSON Pointer references and worker-isolated `pattern`/`patternProperties`
|
||||
- Focused OpenAPI 3.0/3.1 structural inspection, operation inventory, component/request-schema sample generation, and obvious breaking-change signals
|
||||
- Well-formedness and structural inspection for XSD, Relax NG XML syntax, and Schematron, plus bounded heuristic XSD/Relax NG samples
|
||||
- Conservative comparisons for required properties, types, enums, declarations, operations, parameters, and responses
|
||||
- Inert rendering, offline PWA support, responsive Toolbox shell integration, and deterministic release archives
|
||||
|
||||
Each document is limited to 2 MiB of text, the workspace to 8 MiB and 20 documents, and parsed trees, references, instances, samples, validation work, and diagnostic output have independent bounds. One shared generator budget limits samples to 50,000 monotonic work steps, 2,000 generated JSON values or XML elements, exactly 20 generated nesting levels, 512 KiB of aggregate derived text, 1,024 UTF-16 code units per literal without splitting surrogate pairs, and 2 MiB of serialized output. Generation is refused if another work step would exceed that ceiling; structural and text ceilings instead return a visibly bounded heuristic sample where one can still be formed. DTD/entity declarations, remote references, Schematron XPath, extension code, JSON Schema `pattern`, and `patternProperties` are never executed. JSON Schema validation covers a documented assertion subset rather than claiming full specification conformance. XML languages receive structural inspection—not instance validation—and OpenAPI checks are intentionally focused.
|
||||
Each document is limited to 2 MiB of text, the workspace to 8 MiB and 20 documents, and parsed trees, references, instances, samples, validation work, and diagnostic output have independent bounds. One shared generator budget limits samples to 50,000 monotonic work steps, 2,000 generated JSON values or XML elements, exactly 20 generated nesting levels, 512 KiB of aggregate derived text, 1,024 UTF-16 code units per literal without splitting surrogate pairs, and 2 MiB of serialized output. Generation is refused if another work step would exceed that ceiling; structural and text ceilings instead return a visibly bounded heuristic sample where one can still be formed. DTD/entity declarations, remote references, Schematron XPath, and extension code are never executed. JSON Schema regular expressions are evaluated only in a disposable worker, with limits of 256 expressions, 4,096 characters per expression, 5,000 candidate strings, 250,000 pair evaluations, and a one-second deadline. JSON Schema validation covers a documented assertion subset rather than claiming full specification conformance. XML languages receive structural inspection—not instance validation—and OpenAPI checks are intentionally focused.
|
||||
|
||||
See [Architecture](docs/ARCHITECTURE.md) and [Privacy and security](docs/PRIVACY-SECURITY.md) for the exact capability boundaries.
|
||||
|
||||
@@ -30,7 +30,7 @@ npm run test:browser
|
||||
|
||||
## Release
|
||||
|
||||
`npm run release:artifact` creates deterministic `release/schema-tools-0.1.2.zip` and checksum files.
|
||||
`npm run release:artifact` creates deterministic `release/schema-tools-0.2.0.zip` and checksum files.
|
||||
|
||||
## Licence
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Corresponding source
|
||||
|
||||
The corresponding source for Schema Tools 0.1.2 is available at:
|
||||
The corresponding source for Schema Tools 0.2.0 is available at:
|
||||
|
||||
https://git.add-ideas.de/lotobo/schema-tools/src/tag/v0.1.2
|
||||
https://git.add-ideas.de/lotobo/schema-tools/src/tag/v0.2.0
|
||||
|
||||
Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Third-party notices
|
||||
|
||||
Schema Tools 0.1.2 directly depends on these runtime packages:
|
||||
Schema Tools 0.2.0 directly depends on these runtime packages:
|
||||
|
||||
| Package | Pinned version | Declared licence |
|
||||
| -------------------------------- | -------------: | ---------------- |
|
||||
| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 |
|
||||
| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later |
|
||||
| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 |
|
||||
| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 |
|
||||
| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later |
|
||||
| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 |
|
||||
| `react` | 19.2.8 | MIT |
|
||||
| `react-dom` | 19.2.8 | MIT |
|
||||
| `yaml` | 2.9.0 | ISC |
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
Schema Tools is a static React/Vite application inside the shared Toolbox shell. `src/schema/model.ts` is the bounded trust boundary: it normalizes workspace names, parses JSON/YAML/XML, inventories language structures and references, performs eligible JSON instance validation, generates samples, and produces conservative comparison signals. React renders only text and native controls; no schema content is inserted as HTML.
|
||||
|
||||
JSON and YAML values are converted into an acyclic, prototype-safe JSON model with limits on source length, node count, depth, and collection width. JSON Schema instance validation is a CSP-safe interpreter selected for Draft 6, Draft 7, 2019-09, or 2020-12 behavior; it never generates or evaluates code. The focused subset covers boolean schemas, local `$ref` with JSON Pointer fragments, `type`, `enum`, `const`, numeric bounds and `multipleOf`, string lengths, object properties/required/additional properties/property names/dependencies, array items/tuples/contains/uniqueness and size, combiners, negation, and draft-appropriate conditionals. Validation has a 250,000-step and 1,000-diagnostic ceiling.
|
||||
JSON and YAML values are converted into an acyclic, prototype-safe JSON model with limits on source length, node count, depth, and collection width. JSON Schema instance validation is a CSP-safe interpreter selected for Draft 6, Draft 7, 2019-09, or 2020-12 behavior; it never generates or evaluates code. The focused subset covers boolean schemas, local `$ref` with JSON Pointer fragments, `type`, `enum`, `const`, numeric bounds and `multipleOf`, string lengths and patterns, object properties/required/additional properties/property names/pattern properties/dependencies, array items/tuples/contains/uniqueness and size, combiners, negation, and draft-appropriate conditionals. Validation has a 250,000-step and 1,000-diagnostic ceiling.
|
||||
|
||||
Every reference is classified before validation. Only fragments and relative filenames supplied in the current workspace are eligible. Root `$id`/legacy `id` values are deliberately ignored so relative references remain anchored to workspace filenames; nested identifier scopes, named anchors, dynamic/recursive references, and unevaluated keywords are outside the subset and cause a visible refusal. All participating JSON Schema files must use one declared draft. Formats and unknown extension keywords are annotations. Schemas containing `pattern` or `patternProperties` are refused because JavaScript regular-expression execution cannot be reliably time-bounded; they remain inspectable. Decimal arithmetic uses JavaScript numbers, so `multipleOf` applies a small floating-point tolerance.
|
||||
Every reference is classified before validation. Only fragments and relative filenames supplied in the current workspace are eligible. Root `$id`/legacy `id` values are deliberately ignored so relative references remain anchored to workspace filenames; nested identifier scopes, named anchors, dynamic/recursive references, and unevaluated keywords are outside the subset and cause a visible refusal. All participating JSON Schema files must use one declared draft. Formats and unknown extension keywords are annotations. `pattern` and `patternProperties` expressions and candidate strings are deduplicated and sent as one bounded batch to a fresh module worker; the caller terminates that worker after one second, including while one pathological `RegExp.test` is stalled. No partial result is accepted. Decimal arithmetic uses JavaScript numbers, so `multipleOf` applies a small floating-point tolerance.
|
||||
|
||||
OpenAPI JSON/YAML receives focused document, operation, response, reference, sample, and comparison logic. It does not run requests and does not claim full OpenAPI conformance. XML uses the browser's inert `DOMParser` only after rejecting DTD and entity declarations. Element counting and depth inspection use a linear sibling-pointer walk, avoiding repeated conversion of live DOM child collections. XSD, Relax NG XML syntax, and Schematron are checked for well-formedness and structurally inventoried. Schematron XPath and extensions are retained as text and never executed. XSD and Relax NG sample generation deliberately follows a bounded first branch and is labelled heuristic. XSD named-type shapes are inspected once and cached within one generation; Relax NG grammars and direct-root `element` patterns use the same renderer, and pattern-only wrappers do not consume generated nesting depth.
|
||||
|
||||
All sample paths share one budget implementation. It admits at most 50,000 monotonic generator work steps and 2,000 generated JSON values or XML elements across exactly 20 generated levels (the root is level one), accounts for at most 512 KiB of aggregate derived keys, names, attribute values, and content, caps each copied literal at 1,024 UTF-16 code units without splitting a surrogate pair, and rejects serialized output above 2 MiB. References and schema/pattern combiners do not consume generated depth, but every build, copy, reference, alternative, XML type inspection, and XML pattern visit consumes work before expansion. Per-generation caches ensure repeated JSON references, wide property collections, XML child collections, inline types, text, and name normalization are not rescanned without bound. The work counter is deliberately not restored when a failed heuristic alternative rolls back its node/text checkpoint, and generation is refused rather than returning an ambiguously partial result when a 50,001st work step is attempted. Node, depth, and text ceilings can instead omit bounded material with a visible notice. Repeated references consume node and text counters for every emitted occurrence. Duplicate or fallback-colliding derived XML attribute names are omitted with a notice to preserve well-formed output. A `false` JSON Schema reached through a selected local reference or mandatory `allOf` branch aborts generation; `anyOf` and `oneOf` skip definitely impossible boolean branches and use the first viable heuristic branch only while the complete attempt remains within budget.
|
||||
|
||||
The PWA uses only relative URLs, so the same build works standalone or below a nested portal route. Its service worker caches same-origin files from its own scope. No worker, storage database, server API, telemetry path, or remote schema resolver exists in v0.1.2.
|
||||
The PWA uses only relative URLs, so the same build works standalone or below a nested portal route. Its service worker caches same-origin files from its own scope. The regex worker receives only the already-local expressions and candidate strings for one validation request and is then terminated. No storage database, server API, telemetry path, or remote schema resolver is used.
|
||||
|
||||
@@ -4,6 +4,6 @@ Schema sources, instances, diagnostics, comparisons, and generated samples stay
|
||||
|
||||
Each source is limited to 2 MiB of text; a workspace is limited to 20 documents and 8 MiB. JSON/YAML trees are capped at 25,000 values, 48 levels, and 2,000 entries in one collection. XML is capped at 25,000 elements and 48 levels. References, instances, and diagnostics have separate caps. A shared sample budget is limited to 50,000 monotonic work steps, exactly 2,000 generated JSON values or XML elements, exactly 20 generated nesting levels, 512 KiB of aggregate derived text, 1,024 UTF-16 code units per copied literal without splitting surrogate pairs, and 2 MiB of serialized output. Reusing a local definition consumes the same aggregate counters on every attempt and generated occurrence; failed heuristic alternatives never refund work. Reaching the work ceiling refuses the generation operation, so exhaustion cannot be mistaken for a viable choice or skip a later mandatory constraint. File byte gates are deliberately conservative before `File.text()` decoding.
|
||||
|
||||
Prototype-sensitive JSON keys, cyclic YAML aliases, NUL input, DTD/entity declarations, absolute/remote/escaping references, and executable schema extensions are rejected. Schematron expressions and imported XML are never executed. JSON Schema formats are annotations, and regex-bearing schema keywords are not executed. The focused validator interprets eligible schemas without dynamic code generation, remote loading, custom code, or `unsafe-eval`; work and diagnostic counts are capped.
|
||||
Prototype-sensitive JSON keys, cyclic YAML aliases, NUL input, DTD/entity declarations, absolute/remote/escaping references, and executable schema extensions are rejected. Schematron expressions and imported XML are never executed. JSON Schema formats are annotations. Regex-bearing keywords are evaluated in a fresh worker that is terminated on completion or after one second; expression count and length, candidate count, and total pair evaluations are capped before dispatch. The focused validator interprets eligible schemas without dynamic code generation, remote loading, custom code, or `unsafe-eval`; work and diagnostic counts are capped.
|
||||
|
||||
Sample generation and compatibility results are review aids. A generated document is not guaranteed to satisfy every constraint, and an absence of reported changes does not prove compatibility. Generation refuses a definitely impossible selected `false` JSON Schema branch but does not prove broader satisfiability. JSON validation requires a single declared draft and uses local workspace filenames rather than identifier URIs. Named anchors, nested identifier scopes, dynamic/recursive references, unevaluated keywords, regular expressions, and full meta-schema validation are outside v0.1.2. XSD, Relax NG, and Schematron instance validation is also outside v0.1.2; OpenAPI inspection is not a full conformance certification.
|
||||
Sample generation and compatibility results are review aids. A generated document is not guaranteed to satisfy every constraint, and an absence of reported changes does not prove compatibility. Generation refuses a definitely impossible selected `false` JSON Schema branch but does not prove broader satisfiability. JSON validation requires a single declared draft and uses local workspace filenames rather than identifier URIs. Named anchors, nested identifier scopes, dynamic/recursive references, unevaluated keywords, and full meta-schema validation remain outside the focused subset. XSD, Relax NG, and Schematron instance validation is also outside the current scope; OpenAPI inspection is not a full conformance certification.
|
||||
|
||||
Generated
+22
-23
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "schema-tools",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "schema-tools",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@add-ideas/toolbox-contract": "0.2.3",
|
||||
"@add-ideas/toolbox-helpers": "0.1.0",
|
||||
"@add-ideas/toolbox-shell-react": "0.2.3",
|
||||
"@add-ideas/toolbox-contract": "0.3.0",
|
||||
"@add-ideas/toolbox-helpers": "0.2.0",
|
||||
"@add-ideas/toolbox-shell-react": "0.3.0",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8",
|
||||
"yaml": "2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@add-ideas/toolbox-testkit": "0.2.3",
|
||||
"@add-ideas/toolbox-testkit": "0.3.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@playwright/test": "1.62.1",
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
@@ -43,24 +43,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@add-ideas/toolbox-contract": {
|
||||
"version": "0.2.3",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.3.0/toolbox-contract-0.3.0.tgz",
|
||||
"integrity": "sha512-dKrK7BjOFwqJaBfJuhKxZKIld4sH0AKjEn6a0yLnbdMUFY+fFv4VSLGV2tNSBD016gumc2iNqOjUj/ld7x4rtA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@add-ideas/toolbox-helpers": {
|
||||
"version": "0.1.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-helpers/-/0.2.0/toolbox-helpers-0.2.0.tgz",
|
||||
"integrity": "sha512-SdOqkw+P+3J3fa5iVkzb5P15rVepB001GNV21Oh8w0CZcVL+YRltgD/s+MVcTyrNijWQf3E5vtQON/3N2LLyKg==",
|
||||
"license": "GPL-3.0-or-later"
|
||||
},
|
||||
"node_modules/@add-ideas/toolbox-shell-react": {
|
||||
"version": "0.2.3",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.3.0/toolbox-shell-react-0.3.0.tgz",
|
||||
"integrity": "sha512-74p6JzAOG0YCAKdlc1hLofV4ZIko7vb448S75cIiM88PKm93EHl5VD7g8YVyfM56Ui97UY9dmy+Whiq4sGzpsg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@add-ideas/toolbox-contract": "0.2.3"
|
||||
"@add-ideas/toolbox-contract": "0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18 <20",
|
||||
@@ -68,17 +68,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@add-ideas/toolbox-testkit": {
|
||||
"version": "0.2.3",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.3.0/toolbox-testkit-0.3.0.tgz",
|
||||
"integrity": "sha512-4Fk+oSvZFspOMIXr8Xy040nhAaBsIQAzsGyXWSpjn3+k3yBKq7nB1r5zCHhsXzfdLzvPDAx2KcmSNOhM330D9w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@add-ideas/toolbox-contract": "0.2.3"
|
||||
"@add-ideas/toolbox-contract": "0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"toolbox-check": "dist/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "schema-tools",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"description": "Inspect, validate, compare, and derive examples from schemas locally in the browser.",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"author": "Albrecht Degering",
|
||||
@@ -39,15 +39,15 @@
|
||||
"release:artifact": "npm run check && npm run test:browser && npm run package:release -- --force"
|
||||
},
|
||||
"dependencies": {
|
||||
"@add-ideas/toolbox-contract": "0.2.3",
|
||||
"@add-ideas/toolbox-helpers": "0.1.0",
|
||||
"@add-ideas/toolbox-shell-react": "0.2.3",
|
||||
"@add-ideas/toolbox-contract": "0.3.0",
|
||||
"@add-ideas/toolbox-helpers": "0.2.0",
|
||||
"@add-ideas/toolbox-shell-react": "0.3.0",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8",
|
||||
"yaml": "2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@add-ideas/toolbox-testkit": "0.2.3",
|
||||
"@add-ideas/toolbox-testkit": "0.3.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@playwright/test": "1.62.1",
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
|
||||
+20
-2
@@ -15,7 +15,25 @@ export default defineConfig({
|
||||
timeout: 180_000,
|
||||
},
|
||||
projects: [
|
||||
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
||||
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
|
||||
{
|
||||
name: "chromium",
|
||||
testIgnore: /responsive\.spec\.ts/,
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
{
|
||||
name: "firefox",
|
||||
testIgnore: /responsive\.spec\.ts/,
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
{
|
||||
name: "webkit",
|
||||
testIgnore: /responsive\.spec\.ts/,
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
},
|
||||
{
|
||||
name: "mobile-chromium",
|
||||
testMatch: /responsive\.spec\.ts/,
|
||||
use: { ...devices["Pixel 5"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.0 - 2026-09-02
|
||||
|
||||
- Added worker-isolated, bounded `pattern` and `patternProperties` validation,
|
||||
along with broader draft-aware object, array, numeric, dependency and
|
||||
combiner assertions.
|
||||
- Added deeper recursive schema comparison and focused XML-language structural
|
||||
inventories while retaining explicit unsupported-keyword diagnostics.
|
||||
|
||||
## 0.1.2 - 2026-09-01
|
||||
|
||||
- Refused invalid sample claims when `false` JSON Schemas are reached through local references or required combiners, while selecting the first viable `anyOf` or `oneOf` branch.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
==============================================================================
|
||||
@add-ideas/toolbox-contract@0.2.3
|
||||
@add-ideas/toolbox-contract@0.3.0
|
||||
Declared licence: Apache-2.0
|
||||
==============================================================================
|
||||
--- LICENSE ---
|
||||
@@ -198,7 +198,7 @@ Declared licence: Apache-2.0
|
||||
|
||||
|
||||
==============================================================================
|
||||
@add-ideas/toolbox-helpers@0.1.0
|
||||
@add-ideas/toolbox-helpers@0.2.0
|
||||
Declared licence: GPL-3.0-or-later
|
||||
==============================================================================
|
||||
--- LICENSE ---
|
||||
@@ -879,7 +879,7 @@ Public License instead of this License. But first, please read
|
||||
|
||||
|
||||
==============================================================================
|
||||
@add-ideas/toolbox-shell-react@0.2.3
|
||||
@add-ideas/toolbox-shell-react@0.3.0
|
||||
Declared licence: Apache-2.0
|
||||
==============================================================================
|
||||
--- LICENSE ---
|
||||
|
||||
+3
-3
@@ -8,13 +8,13 @@ Schema Tools is a standalone local-first application in the [add·ideas Toolbox]
|
||||
|
||||
- Safe local workspaces of up to 20 JSON Schema, OpenAPI, XSD, Relax NG XML-syntax, or Schematron documents
|
||||
- Reference inventory and graph with local relative resolution, fragments, missing-target diagnostics, and explicit blocking of remote, absolute, and escaping references
|
||||
- Focused, CSP-safe JSON Schema instance validation for Draft 6/7, 2019-09, and 2020-12 with bounded local JSON Pointer references
|
||||
- Focused, CSP-safe JSON Schema instance validation for Draft 6/7, 2019-09, and 2020-12 with bounded local JSON Pointer references and worker-isolated `pattern`/`patternProperties`
|
||||
- Focused OpenAPI 3.0/3.1 structural inspection, operation inventory, component/request-schema sample generation, and obvious breaking-change signals
|
||||
- Well-formedness and structural inspection for XSD, Relax NG XML syntax, and Schematron, plus bounded heuristic XSD/Relax NG samples
|
||||
- Conservative comparisons for required properties, types, enums, declarations, operations, parameters, and responses
|
||||
- Inert rendering, offline PWA support, responsive Toolbox shell integration, and deterministic release archives
|
||||
|
||||
Each document is limited to 2 MiB of text, the workspace to 8 MiB and 20 documents, and parsed trees, references, instances, samples, validation work, and diagnostic output have independent bounds. One shared generator budget limits samples to 50,000 monotonic work steps, 2,000 generated JSON values or XML elements, exactly 20 generated nesting levels, 512 KiB of aggregate derived text, 1,024 UTF-16 code units per literal without splitting surrogate pairs, and 2 MiB of serialized output. Generation is refused if another work step would exceed that ceiling; structural and text ceilings instead return a visibly bounded heuristic sample where one can still be formed. DTD/entity declarations, remote references, Schematron XPath, extension code, JSON Schema `pattern`, and `patternProperties` are never executed. JSON Schema validation covers a documented assertion subset rather than claiming full specification conformance. XML languages receive structural inspection—not instance validation—and OpenAPI checks are intentionally focused.
|
||||
Each document is limited to 2 MiB of text, the workspace to 8 MiB and 20 documents, and parsed trees, references, instances, samples, validation work, and diagnostic output have independent bounds. One shared generator budget limits samples to 50,000 monotonic work steps, 2,000 generated JSON values or XML elements, exactly 20 generated nesting levels, 512 KiB of aggregate derived text, 1,024 UTF-16 code units per literal without splitting surrogate pairs, and 2 MiB of serialized output. Generation is refused if another work step would exceed that ceiling; structural and text ceilings instead return a visibly bounded heuristic sample where one can still be formed. DTD/entity declarations, remote references, Schematron XPath, and extension code are never executed. JSON Schema regular expressions are evaluated only in a disposable worker, with limits of 256 expressions, 4,096 characters per expression, 5,000 candidate strings, 250,000 pair evaluations, and a one-second deadline. JSON Schema validation covers a documented assertion subset rather than claiming full specification conformance. XML languages receive structural inspection—not instance validation—and OpenAPI checks are intentionally focused.
|
||||
|
||||
See [Architecture](docs/ARCHITECTURE.md) and [Privacy and security](docs/PRIVACY-SECURITY.md) for the exact capability boundaries.
|
||||
|
||||
@@ -30,7 +30,7 @@ npm run test:browser
|
||||
|
||||
## Release
|
||||
|
||||
`npm run release:artifact` creates deterministic `release/schema-tools-0.1.2.zip` and checksum files.
|
||||
`npm run release:artifact` creates deterministic `release/schema-tools-0.2.0.zip` and checksum files.
|
||||
|
||||
## Licence
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
# Corresponding source
|
||||
|
||||
The corresponding source for Schema Tools 0.1.2 is available at:
|
||||
The corresponding source for Schema Tools 0.2.0 is available at:
|
||||
|
||||
https://git.add-ideas.de/lotobo/schema-tools/src/tag/v0.1.2
|
||||
https://git.add-ideas.de/lotobo/schema-tools/src/tag/v0.2.0
|
||||
|
||||
Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Third-party notices
|
||||
|
||||
Schema Tools 0.1.2 directly depends on these runtime packages:
|
||||
Schema Tools 0.2.0 directly depends on these runtime packages:
|
||||
|
||||
| Package | Pinned version | Declared licence |
|
||||
| -------------------------------- | -------------: | ---------------- |
|
||||
| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 |
|
||||
| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later |
|
||||
| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 |
|
||||
| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 |
|
||||
| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later |
|
||||
| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 |
|
||||
| `react` | 19.2.8 | MIT |
|
||||
| `react-dom` | 19.2.8 | MIT |
|
||||
| `yaml` | 2.9.0 | ISC |
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
Schema Tools is a static React/Vite application inside the shared Toolbox shell. `src/schema/model.ts` is the bounded trust boundary: it normalizes workspace names, parses JSON/YAML/XML, inventories language structures and references, performs eligible JSON instance validation, generates samples, and produces conservative comparison signals. React renders only text and native controls; no schema content is inserted as HTML.
|
||||
|
||||
JSON and YAML values are converted into an acyclic, prototype-safe JSON model with limits on source length, node count, depth, and collection width. JSON Schema instance validation is a CSP-safe interpreter selected for Draft 6, Draft 7, 2019-09, or 2020-12 behavior; it never generates or evaluates code. The focused subset covers boolean schemas, local `$ref` with JSON Pointer fragments, `type`, `enum`, `const`, numeric bounds and `multipleOf`, string lengths, object properties/required/additional properties/property names/dependencies, array items/tuples/contains/uniqueness and size, combiners, negation, and draft-appropriate conditionals. Validation has a 250,000-step and 1,000-diagnostic ceiling.
|
||||
JSON and YAML values are converted into an acyclic, prototype-safe JSON model with limits on source length, node count, depth, and collection width. JSON Schema instance validation is a CSP-safe interpreter selected for Draft 6, Draft 7, 2019-09, or 2020-12 behavior; it never generates or evaluates code. The focused subset covers boolean schemas, local `$ref` with JSON Pointer fragments, `type`, `enum`, `const`, numeric bounds and `multipleOf`, string lengths and patterns, object properties/required/additional properties/property names/pattern properties/dependencies, array items/tuples/contains/uniqueness and size, combiners, negation, and draft-appropriate conditionals. Validation has a 250,000-step and 1,000-diagnostic ceiling.
|
||||
|
||||
Every reference is classified before validation. Only fragments and relative filenames supplied in the current workspace are eligible. Root `$id`/legacy `id` values are deliberately ignored so relative references remain anchored to workspace filenames; nested identifier scopes, named anchors, dynamic/recursive references, and unevaluated keywords are outside the subset and cause a visible refusal. All participating JSON Schema files must use one declared draft. Formats and unknown extension keywords are annotations. Schemas containing `pattern` or `patternProperties` are refused because JavaScript regular-expression execution cannot be reliably time-bounded; they remain inspectable. Decimal arithmetic uses JavaScript numbers, so `multipleOf` applies a small floating-point tolerance.
|
||||
Every reference is classified before validation. Only fragments and relative filenames supplied in the current workspace are eligible. Root `$id`/legacy `id` values are deliberately ignored so relative references remain anchored to workspace filenames; nested identifier scopes, named anchors, dynamic/recursive references, and unevaluated keywords are outside the subset and cause a visible refusal. All participating JSON Schema files must use one declared draft. Formats and unknown extension keywords are annotations. `pattern` and `patternProperties` expressions and candidate strings are deduplicated and sent as one bounded batch to a fresh module worker; the caller terminates that worker after one second, including while one pathological `RegExp.test` is stalled. No partial result is accepted. Decimal arithmetic uses JavaScript numbers, so `multipleOf` applies a small floating-point tolerance.
|
||||
|
||||
OpenAPI JSON/YAML receives focused document, operation, response, reference, sample, and comparison logic. It does not run requests and does not claim full OpenAPI conformance. XML uses the browser's inert `DOMParser` only after rejecting DTD and entity declarations. Element counting and depth inspection use a linear sibling-pointer walk, avoiding repeated conversion of live DOM child collections. XSD, Relax NG XML syntax, and Schematron are checked for well-formedness and structurally inventoried. Schematron XPath and extensions are retained as text and never executed. XSD and Relax NG sample generation deliberately follows a bounded first branch and is labelled heuristic. XSD named-type shapes are inspected once and cached within one generation; Relax NG grammars and direct-root `element` patterns use the same renderer, and pattern-only wrappers do not consume generated nesting depth.
|
||||
|
||||
All sample paths share one budget implementation. It admits at most 50,000 monotonic generator work steps and 2,000 generated JSON values or XML elements across exactly 20 generated levels (the root is level one), accounts for at most 512 KiB of aggregate derived keys, names, attribute values, and content, caps each copied literal at 1,024 UTF-16 code units without splitting a surrogate pair, and rejects serialized output above 2 MiB. References and schema/pattern combiners do not consume generated depth, but every build, copy, reference, alternative, XML type inspection, and XML pattern visit consumes work before expansion. Per-generation caches ensure repeated JSON references, wide property collections, XML child collections, inline types, text, and name normalization are not rescanned without bound. The work counter is deliberately not restored when a failed heuristic alternative rolls back its node/text checkpoint, and generation is refused rather than returning an ambiguously partial result when a 50,001st work step is attempted. Node, depth, and text ceilings can instead omit bounded material with a visible notice. Repeated references consume node and text counters for every emitted occurrence. Duplicate or fallback-colliding derived XML attribute names are omitted with a notice to preserve well-formed output. A `false` JSON Schema reached through a selected local reference or mandatory `allOf` branch aborts generation; `anyOf` and `oneOf` skip definitely impossible boolean branches and use the first viable heuristic branch only while the complete attempt remains within budget.
|
||||
|
||||
The PWA uses only relative URLs, so the same build works standalone or below a nested portal route. Its service worker caches same-origin files from its own scope. No worker, storage database, server API, telemetry path, or remote schema resolver exists in v0.1.2.
|
||||
The PWA uses only relative URLs, so the same build works standalone or below a nested portal route. Its service worker caches same-origin files from its own scope. The regex worker receives only the already-local expressions and candidate strings for one validation request and is then terminated. No storage database, server API, telemetry path, or remote schema resolver is used.
|
||||
|
||||
@@ -4,6 +4,6 @@ Schema sources, instances, diagnostics, comparisons, and generated samples stay
|
||||
|
||||
Each source is limited to 2 MiB of text; a workspace is limited to 20 documents and 8 MiB. JSON/YAML trees are capped at 25,000 values, 48 levels, and 2,000 entries in one collection. XML is capped at 25,000 elements and 48 levels. References, instances, and diagnostics have separate caps. A shared sample budget is limited to 50,000 monotonic work steps, exactly 2,000 generated JSON values or XML elements, exactly 20 generated nesting levels, 512 KiB of aggregate derived text, 1,024 UTF-16 code units per copied literal without splitting surrogate pairs, and 2 MiB of serialized output. Reusing a local definition consumes the same aggregate counters on every attempt and generated occurrence; failed heuristic alternatives never refund work. Reaching the work ceiling refuses the generation operation, so exhaustion cannot be mistaken for a viable choice or skip a later mandatory constraint. File byte gates are deliberately conservative before `File.text()` decoding.
|
||||
|
||||
Prototype-sensitive JSON keys, cyclic YAML aliases, NUL input, DTD/entity declarations, absolute/remote/escaping references, and executable schema extensions are rejected. Schematron expressions and imported XML are never executed. JSON Schema formats are annotations, and regex-bearing schema keywords are not executed. The focused validator interprets eligible schemas without dynamic code generation, remote loading, custom code, or `unsafe-eval`; work and diagnostic counts are capped.
|
||||
Prototype-sensitive JSON keys, cyclic YAML aliases, NUL input, DTD/entity declarations, absolute/remote/escaping references, and executable schema extensions are rejected. Schematron expressions and imported XML are never executed. JSON Schema formats are annotations. Regex-bearing keywords are evaluated in a fresh worker that is terminated on completion or after one second; expression count and length, candidate count, and total pair evaluations are capped before dispatch. The focused validator interprets eligible schemas without dynamic code generation, remote loading, custom code, or `unsafe-eval`; work and diagnostic counts are capped.
|
||||
|
||||
Sample generation and compatibility results are review aids. A generated document is not guaranteed to satisfy every constraint, and an absence of reported changes does not prove compatibility. Generation refuses a definitely impossible selected `false` JSON Schema branch but does not prove broader satisfiability. JSON validation requires a single declared draft and uses local workspace filenames rather than identifier URIs. Named anchors, nested identifier scopes, dynamic/recursive references, unevaluated keywords, regular expressions, and full meta-schema validation are outside v0.1.2. XSD, Relax NG, and Schematron instance validation is also outside v0.1.2; OpenAPI inspection is not a full conformance certification.
|
||||
Sample generation and compatibility results are review aids. A generated document is not guaranteed to satisfy every constraint, and an absence of reported changes does not prove compatibility. Generation refuses a definitely impossible selected `false` JSON Schema branch but does not prove broader satisfiability. JSON validation requires a single declared draft and uses local workspace filenames rather than identifier URIs. Named anchors, nested identifier scopes, dynamic/recursive references, unevaluated keywords, and full meta-schema validation remain outside the focused subset. XSD, Relax NG, and Schematron instance validation is also outside the current scope; OpenAPI inspection is not a full conformance certification.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
const CACHE_PREFIX = "schema-tools-shell-";
|
||||
const CACHE_NAME = CACHE_PREFIX + "0.1.2";
|
||||
const CACHE_NAME = CACHE_PREFIX + "0.2.0";
|
||||
const CORE = ["./", "./manifest.webmanifest", "./favicon.svg"];
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
|
||||
+16
-1
@@ -3,7 +3,7 @@
|
||||
"schemaVersion": 1,
|
||||
"id": "de.add-ideas.schema-tools",
|
||||
"name": "Schema Tools",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"description": "Inspect, validate, compare, and derive schema examples locally.",
|
||||
"entry": "./",
|
||||
"icon": "./favicon.svg",
|
||||
@@ -21,6 +21,21 @@
|
||||
"crossOriginIsolated": false,
|
||||
"topLevelContext": false
|
||||
},
|
||||
"io": {
|
||||
"accepts": [
|
||||
{ "mediaType": "application/schema+json", "extensions": [".json"] },
|
||||
{ "mediaType": "application/yaml", "extensions": [".yaml", ".yml"] },
|
||||
{
|
||||
"mediaType": "application/xml",
|
||||
"extensions": [".xsd", ".rng", ".sch", ".xml"]
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
{ "mediaType": "application/json", "extensions": [".json"] },
|
||||
{ "mediaType": "application/xml", "extensions": [".xml"] }
|
||||
]
|
||||
},
|
||||
"capabilities": { "required": [], "optional": ["workers"] },
|
||||
"privacy": {
|
||||
"processing": "local",
|
||||
"fileUploads": true,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
compareSchemas,
|
||||
generateSample,
|
||||
inspectWorkspace,
|
||||
validateJsonInstance,
|
||||
validateJsonInstanceSafe,
|
||||
type Diagnostic,
|
||||
type SampleResult,
|
||||
type SchemaChange,
|
||||
@@ -215,9 +215,9 @@ export function Workbench() {
|
||||
setDocuments(next);
|
||||
setSelected(Math.min(selected, next.length - 1));
|
||||
};
|
||||
const validate = () => {
|
||||
const validate = async () => {
|
||||
try {
|
||||
const result = validateJsonInstance(report, instance);
|
||||
const result = await validateJsonInstanceSafe(report, instance);
|
||||
setValidation(result.diagnostics);
|
||||
setError("");
|
||||
} catch (reason) {
|
||||
@@ -276,7 +276,8 @@ export function Workbench() {
|
||||
<span className="capability focused">focused validation</span>
|
||||
<p>
|
||||
CSP-safe Draft 6/7, 2019-09, and 2020-12 assertion subset, with
|
||||
bounded local references. Regex-bearing keywords are not executed.
|
||||
bounded local references. Regex-bearing keywords run in a
|
||||
disposable, deadline-limited worker.
|
||||
</p>
|
||||
</article>
|
||||
<article>
|
||||
@@ -317,15 +318,14 @@ export function Workbench() {
|
||||
<section className="panel workspace">
|
||||
<div
|
||||
className="workspace-tabs"
|
||||
role="tablist"
|
||||
role="group"
|
||||
aria-label="Schema workbench views"
|
||||
>
|
||||
{tabs.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === item.id}
|
||||
aria-pressed={tab === item.id}
|
||||
onClick={() => setTab(item.id)}
|
||||
>
|
||||
{item.label}
|
||||
@@ -339,7 +339,7 @@ export function Workbench() {
|
||||
)}
|
||||
|
||||
{tab === "workspace" && (
|
||||
<div className="stack" role="tabpanel">
|
||||
<div className="stack">
|
||||
<div className="actions">
|
||||
<label className="button file-button">
|
||||
Open local schemas
|
||||
@@ -362,15 +362,14 @@ export function Workbench() {
|
||||
</div>
|
||||
<div
|
||||
className="document-tabs"
|
||||
role="tablist"
|
||||
role="group"
|
||||
aria-label="Workspace documents"
|
||||
>
|
||||
{documents.map((item, index) => (
|
||||
<button
|
||||
key={`${item.name}-${index}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={index === selected}
|
||||
aria-pressed={index === selected}
|
||||
onClick={() => setSelected(index)}
|
||||
>
|
||||
{item.name || `Document ${index + 1}`}
|
||||
@@ -451,15 +450,15 @@ export function Workbench() {
|
||||
)}
|
||||
|
||||
{tab === "validate" && (
|
||||
<div className="editor-grid" role="tabpanel">
|
||||
<div className="editor-grid">
|
||||
<div className="stack">
|
||||
<div>
|
||||
<p className="eyebrow">Bounded instance check</p>
|
||||
<h2>Validate JSON locally</h2>
|
||||
<p className="muted">
|
||||
Available only when the entry is JSON Schema. Formats are
|
||||
annotations; schemas containing pattern or patternProperties
|
||||
are refused rather than risking unbounded regex execution.
|
||||
annotations; pattern and patternProperties checks run in an
|
||||
isolated worker with count, size, and time ceilings.
|
||||
</p>
|
||||
</div>
|
||||
<label className="field">
|
||||
@@ -485,7 +484,7 @@ export function Workbench() {
|
||||
)}
|
||||
|
||||
{tab === "sample" && (
|
||||
<div className="editor-grid" role="tabpanel">
|
||||
<div className="editor-grid">
|
||||
<div className="stack">
|
||||
<div>
|
||||
<p className="eyebrow">Bounded derivation</p>
|
||||
@@ -538,7 +537,7 @@ export function Workbench() {
|
||||
)}
|
||||
|
||||
{tab === "references" && (
|
||||
<div className="stack" role="tabpanel">
|
||||
<div className="stack">
|
||||
<div>
|
||||
<p className="eyebrow">No network resolution</p>
|
||||
<h2>Local reference graph</h2>
|
||||
@@ -588,7 +587,7 @@ export function Workbench() {
|
||||
)}
|
||||
|
||||
{tab === "compare" && (
|
||||
<div className="stack" role="tabpanel">
|
||||
<div className="stack">
|
||||
<div>
|
||||
<p className="eyebrow">Conservative signals</p>
|
||||
<h2>Compare two schema revisions</h2>
|
||||
|
||||
+150
-4
@@ -1,5 +1,6 @@
|
||||
import { assertBoundedText, safeJsonParse } from "@add-ideas/toolbox-helpers";
|
||||
import { parseDocument as parseYamlDocument } from "yaml";
|
||||
import { runRegexBatchInWorker, type RegexBatchRunner } from "./regex";
|
||||
|
||||
export type SchemaLanguage =
|
||||
"json-schema" | "openapi" | "xsd" | "relax-ng" | "schematron";
|
||||
@@ -77,6 +78,11 @@ export const SCHEMA_LIMITS = {
|
||||
instanceCollection: 2_000,
|
||||
validationSteps: 250_000,
|
||||
validationDiagnostics: 1_000,
|
||||
regexPatterns: 256,
|
||||
regexPatternChars: 4_096,
|
||||
regexCandidates: 5_000,
|
||||
regexEvaluations: 250_000,
|
||||
regexDeadlineMs: 1_000,
|
||||
} as const;
|
||||
|
||||
const DANGEROUS_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
||||
@@ -1932,6 +1938,7 @@ function schemaChildren(
|
||||
"$defs",
|
||||
"definitions",
|
||||
"properties",
|
||||
"patternProperties",
|
||||
"dependentSchemas",
|
||||
] as const) {
|
||||
const value = schema[key];
|
||||
@@ -1981,10 +1988,26 @@ function assertEligibleSchema(
|
||||
!/(?:draft-0[67]|2019-09|2020-12)/u.test(schema.$schema)
|
||||
)
|
||||
throw new TypeError(`Declared dialect is not supported: ${schema.$schema}`);
|
||||
if (schema.pattern !== undefined || schema.patternProperties !== undefined)
|
||||
throw new TypeError(
|
||||
`${path || "/"} uses pattern or patternProperties; regular expressions are not executed.`,
|
||||
if (schema.pattern !== undefined && typeof schema.pattern !== "string")
|
||||
throw new TypeError(`${path || "/"}/pattern must be a string.`);
|
||||
if (
|
||||
typeof schema.pattern === "string" &&
|
||||
schema.pattern.length > SCHEMA_LIMITS.regexPatternChars
|
||||
)
|
||||
throw new RangeError(
|
||||
`${path || "/"}/pattern exceeds ${SCHEMA_LIMITS.regexPatternChars.toLocaleString()} characters.`,
|
||||
);
|
||||
if (schema.patternProperties !== undefined) {
|
||||
if (!isObject(schema.patternProperties))
|
||||
throw new TypeError(
|
||||
`${path || "/"}/patternProperties must be an object of schemas.`,
|
||||
);
|
||||
for (const source of Object.keys(schema.patternProperties))
|
||||
if (source.length > SCHEMA_LIMITS.regexPatternChars)
|
||||
throw new RangeError(
|
||||
`${path || "/"}/patternProperties contains an expression longer than ${SCHEMA_LIMITS.regexPatternChars.toLocaleString()} characters.`,
|
||||
);
|
||||
}
|
||||
for (const keyword of [
|
||||
"$dynamicRef",
|
||||
"$recursiveRef",
|
||||
@@ -2066,6 +2089,7 @@ function assertEligibleSchema(
|
||||
throw new TypeError(`${path || "/"}/uniqueItems must be a boolean.`);
|
||||
for (const key of [
|
||||
"properties",
|
||||
"patternProperties",
|
||||
"$defs",
|
||||
"definitions",
|
||||
"dependentSchemas",
|
||||
@@ -2136,6 +2160,20 @@ interface ValidationContext {
|
||||
diagnostics: Diagnostic[];
|
||||
budget: ValidationBudget;
|
||||
activeReferences: Set<string>;
|
||||
patternMatches?: ReadonlyMap<string, ReadonlySet<string>>;
|
||||
}
|
||||
|
||||
function regexMatches(
|
||||
context: ValidationContext,
|
||||
source: string,
|
||||
candidate: string,
|
||||
): boolean {
|
||||
const matches = context.patternMatches?.get(source);
|
||||
if (!matches)
|
||||
throw new TypeError(
|
||||
"This schema uses regular expressions. Use bounded asynchronous validation so they can run in an isolated worker.",
|
||||
);
|
||||
return matches.has(candidate);
|
||||
}
|
||||
|
||||
function validationError(
|
||||
@@ -2418,6 +2456,19 @@ function validateSchemaValue(
|
||||
);
|
||||
valid = false;
|
||||
}
|
||||
if (
|
||||
typeof schema.pattern === "string" &&
|
||||
!regexMatches(context, schema.pattern, instance)
|
||||
) {
|
||||
validationError(
|
||||
context,
|
||||
filename,
|
||||
path,
|
||||
"pattern",
|
||||
"string does not match the required expression",
|
||||
);
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(instance)) {
|
||||
@@ -2612,6 +2663,10 @@ function validateSchemaValue(
|
||||
valid = false;
|
||||
}
|
||||
const properties = isObject(schema.properties) ? schema.properties : {};
|
||||
const patternProperties = isObject(schema.patternProperties)
|
||||
? schema.patternProperties
|
||||
: {};
|
||||
const matchedByPattern = new Set<string>();
|
||||
for (const [name, child] of Object.entries(properties))
|
||||
if (Object.hasOwn(instance, name))
|
||||
valid =
|
||||
@@ -2623,6 +2678,20 @@ function validateSchemaValue(
|
||||
context,
|
||||
depth + 1,
|
||||
) && valid;
|
||||
for (const [source, child] of Object.entries(patternProperties))
|
||||
for (const name of keys)
|
||||
if (regexMatches(context, source, name)) {
|
||||
matchedByPattern.add(name);
|
||||
valid =
|
||||
validateSchemaValue(
|
||||
child,
|
||||
instance[name]!,
|
||||
filename,
|
||||
instanceChildPath(path, name),
|
||||
context,
|
||||
depth + 1,
|
||||
) && valid;
|
||||
}
|
||||
if (schema.propertyNames !== undefined)
|
||||
for (const name of keys)
|
||||
valid =
|
||||
@@ -2636,7 +2705,7 @@ function validateSchemaValue(
|
||||
) && valid;
|
||||
if (schema.additionalProperties !== undefined)
|
||||
for (const name of keys)
|
||||
if (!Object.hasOwn(properties, name))
|
||||
if (!Object.hasOwn(properties, name) && !matchedByPattern.has(name))
|
||||
valid =
|
||||
validateSchemaValue(
|
||||
schema.additionalProperties,
|
||||
@@ -2718,6 +2787,7 @@ export function validateJsonInstance(
|
||||
workspace: SchemaWorkspace,
|
||||
instanceSource: string,
|
||||
name = workspace.entry,
|
||||
patternMatches?: ReadonlyMap<string, ReadonlySet<string>>,
|
||||
): ValidationResult {
|
||||
const document = workspace.documents.get(name);
|
||||
if (
|
||||
@@ -2805,6 +2875,7 @@ export function validateJsonInstance(
|
||||
diagnostics,
|
||||
budget: { steps: 0 },
|
||||
activeReferences: new Set(),
|
||||
patternMatches,
|
||||
},
|
||||
);
|
||||
const output = valid
|
||||
@@ -2842,6 +2913,81 @@ export function validateJsonInstance(
|
||||
}
|
||||
}
|
||||
|
||||
function collectRegexInputs(
|
||||
workspace: SchemaWorkspace,
|
||||
instanceSource: string,
|
||||
): { patterns: string[]; candidates: string[] } {
|
||||
const patterns = new Set<string>();
|
||||
const visitSchema = (value: JsonValue): void => {
|
||||
if (!isObject(value)) {
|
||||
if (Array.isArray(value)) for (const child of value) visitSchema(child);
|
||||
return;
|
||||
}
|
||||
if (typeof value.pattern === "string") patterns.add(value.pattern);
|
||||
if (isObject(value.patternProperties))
|
||||
for (const source of Object.keys(value.patternProperties))
|
||||
patterns.add(source);
|
||||
for (const child of Object.values(value)) visitSchema(child);
|
||||
};
|
||||
for (const document of workspace.documents.values())
|
||||
if (document.language === "json-schema" && document.json !== undefined)
|
||||
visitSchema(document.json);
|
||||
if (patterns.size > SCHEMA_LIMITS.regexPatterns)
|
||||
throw new RangeError(
|
||||
`Schema workspace exceeds ${SCHEMA_LIMITS.regexPatterns.toLocaleString()} distinct regular expressions.`,
|
||||
);
|
||||
|
||||
const instance = validateJsonValue(
|
||||
safeJsonParse(instanceSource, {
|
||||
maxTextChars: SCHEMA_LIMITS.instanceChars,
|
||||
maxDepth: SCHEMA_LIMITS.depth,
|
||||
maxNodes: SCHEMA_LIMITS.instanceNodes,
|
||||
rejectDangerousKeys: true,
|
||||
}),
|
||||
);
|
||||
const candidates = new Set<string>();
|
||||
const visitInstance = (value: JsonValue): void => {
|
||||
if (typeof value === "string") candidates.add(value);
|
||||
else if (Array.isArray(value))
|
||||
for (const child of value) visitInstance(child);
|
||||
else if (isObject(value))
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
candidates.add(key);
|
||||
visitInstance(child);
|
||||
}
|
||||
};
|
||||
visitInstance(instance);
|
||||
if (candidates.size > SCHEMA_LIMITS.regexCandidates)
|
||||
throw new RangeError(
|
||||
`Instance exposes more than ${SCHEMA_LIMITS.regexCandidates.toLocaleString()} distinct strings or property names to regular-expression validation.`,
|
||||
);
|
||||
if (patterns.size * candidates.size > SCHEMA_LIMITS.regexEvaluations)
|
||||
throw new RangeError(
|
||||
`Schema regular-expression work exceeds ${SCHEMA_LIMITS.regexEvaluations.toLocaleString()} bounded evaluations.`,
|
||||
);
|
||||
return { patterns: [...patterns], candidates: [...candidates] };
|
||||
}
|
||||
|
||||
export async function validateJsonInstanceSafe(
|
||||
workspace: SchemaWorkspace,
|
||||
instanceSource: string,
|
||||
name = workspace.entry,
|
||||
runner: RegexBatchRunner = runRegexBatchInWorker,
|
||||
): Promise<ValidationResult> {
|
||||
const bounded = assertBoundedText(
|
||||
instanceSource,
|
||||
SCHEMA_LIMITS.instanceChars,
|
||||
"Instance length",
|
||||
);
|
||||
const inputs = collectRegexInputs(workspace, bounded);
|
||||
const matches = inputs.patterns.length
|
||||
? await runner(inputs.patterns, inputs.candidates, {
|
||||
deadlineMs: SCHEMA_LIMITS.regexDeadlineMs,
|
||||
})
|
||||
: new Map<string, ReadonlySet<string>>();
|
||||
return validateJsonInstance(workspace, bounded, name, matches);
|
||||
}
|
||||
|
||||
function stringSet(value: JsonValue | undefined): Set<string> {
|
||||
return new Set(
|
||||
Array.isArray(value)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
interface RegexRequest {
|
||||
id: number;
|
||||
patterns: string[];
|
||||
candidates: string[];
|
||||
}
|
||||
|
||||
interface RegexResponse {
|
||||
id: number;
|
||||
matches?: number[][];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
self.addEventListener("message", (event: MessageEvent<RegexRequest>) => {
|
||||
const { id, patterns, candidates } = event.data;
|
||||
try {
|
||||
const matches = patterns.map((source) => {
|
||||
const expression = new RegExp(source, "u");
|
||||
const matching: number[] = [];
|
||||
for (let index = 0; index < candidates.length; index += 1) {
|
||||
expression.lastIndex = 0;
|
||||
if (expression.test(candidates[index]!)) matching.push(index);
|
||||
}
|
||||
return matching;
|
||||
});
|
||||
self.postMessage({ id, matches } satisfies RegexResponse);
|
||||
} catch (reason) {
|
||||
self.postMessage({
|
||||
id,
|
||||
error:
|
||||
reason instanceof Error
|
||||
? reason.message
|
||||
: "A schema regular expression could not be evaluated.",
|
||||
} satisfies RegexResponse);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
export interface RegexEvaluation {
|
||||
pattern: string;
|
||||
matches: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
export interface RegexBatchOptions {
|
||||
deadlineMs?: number;
|
||||
}
|
||||
|
||||
export type RegexBatchRunner = (
|
||||
patterns: readonly string[],
|
||||
candidates: readonly string[],
|
||||
options?: RegexBatchOptions,
|
||||
) => Promise<ReadonlyMap<string, ReadonlySet<string>>>;
|
||||
|
||||
interface RegexResponse {
|
||||
id: number;
|
||||
matches?: number[][];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
let requestSequence = 0;
|
||||
|
||||
/**
|
||||
* Evaluate untrusted JSON Schema expressions away from the UI thread. A worker
|
||||
* is terminated after every request, so a timed-out expression cannot keep
|
||||
* consuming CPU or retain inspected data.
|
||||
*/
|
||||
export const runRegexBatchInWorker: RegexBatchRunner = (
|
||||
patterns,
|
||||
candidates,
|
||||
options = {},
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const worker = new Worker(new URL("./regex-worker.ts", import.meta.url), {
|
||||
type: "module",
|
||||
name: "schema-regex-evaluator",
|
||||
});
|
||||
const id = ++requestSequence;
|
||||
const deadlineMs = Math.max(
|
||||
50,
|
||||
Math.min(options.deadlineMs ?? 1_000, 5_000),
|
||||
);
|
||||
let settled = false;
|
||||
const finish = (operation: () => void) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
worker.terminate();
|
||||
operation();
|
||||
};
|
||||
const timer = setTimeout(
|
||||
() =>
|
||||
finish(() =>
|
||||
reject(
|
||||
new RangeError(
|
||||
`Schema regular-expression evaluation exceeded its ${deadlineMs}-millisecond safety limit.`,
|
||||
),
|
||||
),
|
||||
),
|
||||
deadlineMs,
|
||||
);
|
||||
worker.addEventListener("error", (event) => {
|
||||
finish(() =>
|
||||
reject(new Error(event.message || "Schema regex worker failed.")),
|
||||
);
|
||||
});
|
||||
worker.addEventListener("message", (event: MessageEvent<RegexResponse>) => {
|
||||
if (event.data.id !== id) return;
|
||||
if (event.data.error) {
|
||||
finish(() =>
|
||||
reject(
|
||||
new SyntaxError(
|
||||
`Invalid or unsupported JSON Schema regular expression: ${event.data.error}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const rows = event.data.matches;
|
||||
if (!rows || rows.length !== patterns.length) {
|
||||
finish(() =>
|
||||
reject(new Error("Schema regex worker returned an invalid result.")),
|
||||
);
|
||||
return;
|
||||
}
|
||||
finish(() => {
|
||||
const output = new Map<string, ReadonlySet<string>>();
|
||||
rows.forEach((indexes, patternIndex) => {
|
||||
const values = new Set<string>();
|
||||
for (const candidateIndex of indexes) {
|
||||
const value = candidates[candidateIndex];
|
||||
if (value !== undefined) values.add(value);
|
||||
}
|
||||
output.set(patterns[patternIndex]!, values);
|
||||
});
|
||||
resolve(output);
|
||||
});
|
||||
});
|
||||
worker.postMessage({
|
||||
id,
|
||||
patterns: [...patterns],
|
||||
candidates: [...candidates],
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -207,8 +207,8 @@ pre {
|
||||
flex-wrap: nowrap;
|
||||
padding-bottom: 0.2rem;
|
||||
}
|
||||
.workspace-tabs button[aria-selected="true"],
|
||||
.document-tabs button[aria-selected="true"] {
|
||||
.workspace-tabs button[aria-pressed="true"],
|
||||
.document-tabs button[aria-pressed="true"] {
|
||||
border-color: var(--toolbox-accent);
|
||||
background: var(--toolbox-accent);
|
||||
color: var(--toolbox-accent-contrast);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"schemaVersion": 1,
|
||||
"id": "de.add-ideas.schema-tools",
|
||||
"name": "Schema Tools",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"description": "Inspect, validate, compare, and derive schema examples locally.",
|
||||
"entry": "./",
|
||||
"icon": "./favicon.svg",
|
||||
@@ -21,6 +21,36 @@
|
||||
"crossOriginIsolated": false,
|
||||
"topLevelContext": false
|
||||
},
|
||||
"io": {
|
||||
"accepts": [
|
||||
{
|
||||
"mediaType": "application/schema+json",
|
||||
"extensions": [".json"]
|
||||
},
|
||||
{
|
||||
"mediaType": "application/yaml",
|
||||
"extensions": [".yaml", ".yml"]
|
||||
},
|
||||
{
|
||||
"mediaType": "application/xml",
|
||||
"extensions": [".xsd", ".rng", ".sch", ".xml"]
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
{
|
||||
"mediaType": "application/json",
|
||||
"extensions": [".json"]
|
||||
},
|
||||
{
|
||||
"mediaType": "application/xml",
|
||||
"extensions": [".xml"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"required": [],
|
||||
"optional": ["workers"]
|
||||
},
|
||||
"privacy": {
|
||||
"processing": "local",
|
||||
"fileUploads": true,
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const APP_VERSION = "0.1.2";
|
||||
export const APP_VERSION = "0.2.0";
|
||||
|
||||
@@ -36,10 +36,10 @@ test("validates and traces a supplied local JSON Schema reference", async ({
|
||||
}) => {
|
||||
const external = await localOnly(page);
|
||||
await page.goto("/deep/nested/schema/");
|
||||
await page.getByRole("tab", { name: "Validate" }).click();
|
||||
await page.getByRole("button", { name: "Validate" }).click();
|
||||
await page.getByRole("button", { name: "Validate instance" }).click();
|
||||
await expect(page.getByText(/Instance is valid/u)).toBeVisible();
|
||||
await page.getByRole("tab", { name: "References" }).click();
|
||||
await page.getByRole("button", { name: "References" }).click();
|
||||
await expect(
|
||||
page.getByText("address.schema.json#/$defs/address", { exact: true }),
|
||||
).toBeVisible();
|
||||
@@ -70,7 +70,7 @@ test("serves release identity and hardened headers", async ({ request }) => {
|
||||
const manifest = await request.get("/deep/nested/schema/toolbox-app.json");
|
||||
await expect(manifest.json()).resolves.toMatchObject({
|
||||
id: "de.add-ideas.schema-tools",
|
||||
version: "0.1.2",
|
||||
version: "0.2.0",
|
||||
entry: "./",
|
||||
privacy: { processing: "local", telemetry: false },
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("keeps the primary workspace inside a narrow viewport", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/deep/nested/schema/");
|
||||
await expect(page.locator("main").first()).toBeVisible();
|
||||
await expect(
|
||||
page.locator("main .loading, main .workbench-loading"),
|
||||
).toHaveCount(0);
|
||||
|
||||
const widths = await page.evaluate(() => ({
|
||||
content: document.documentElement.scrollWidth,
|
||||
viewport: document.documentElement.clientWidth,
|
||||
}));
|
||||
expect(widths.viewport).toBeLessThanOrEqual(430);
|
||||
expect(widths.content).toBeLessThanOrEqual(widths.viewport + 1);
|
||||
});
|
||||
@@ -20,7 +20,7 @@ describe("Schema Tools", () => {
|
||||
);
|
||||
expect(screen.getByText("focused validation")).toBeVisible();
|
||||
expect(screen.getAllByText("structural only")).toHaveLength(2);
|
||||
await userEvent.click(screen.getByRole("tab", { name: "Validate" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Validate" }));
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "Validate instance" }),
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
inspectWorkspace,
|
||||
parseSchemaDocument,
|
||||
validateJsonInstance,
|
||||
validateJsonInstanceSafe,
|
||||
} from "../../src/schema/model";
|
||||
|
||||
const entry = {
|
||||
@@ -109,6 +110,76 @@ function captureError(run: () => unknown): Error {
|
||||
}
|
||||
|
||||
describe("schema workspace", () => {
|
||||
it("validates pattern and patternProperties through a bounded batch runner", async () => {
|
||||
const workspace = inspectWorkspace([
|
||||
{
|
||||
name: "pattern.schema.json",
|
||||
source: JSON.stringify({
|
||||
type: "object",
|
||||
propertyNames: { pattern: "^[a-z-]+$" },
|
||||
properties: {
|
||||
label: { type: "string", pattern: "^ok(?:-|$)" },
|
||||
},
|
||||
patternProperties: {
|
||||
"^x-": { type: "integer", minimum: 0 },
|
||||
},
|
||||
additionalProperties: false,
|
||||
}),
|
||||
},
|
||||
]);
|
||||
const runner = async (
|
||||
patterns: readonly string[],
|
||||
candidates: readonly string[],
|
||||
) =>
|
||||
new Map(
|
||||
patterns.map((source) => {
|
||||
const expression = new RegExp(source, "u");
|
||||
return [
|
||||
source,
|
||||
new Set(
|
||||
candidates.filter((candidate) => expression.test(candidate)),
|
||||
),
|
||||
] as const;
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
(
|
||||
await validateJsonInstanceSafe(
|
||||
workspace,
|
||||
JSON.stringify({ label: "ok-value", "x-count": 2 }),
|
||||
workspace.entry,
|
||||
runner,
|
||||
)
|
||||
).valid,
|
||||
).toBe(true);
|
||||
|
||||
const invalid = await validateJsonInstanceSafe(
|
||||
workspace,
|
||||
JSON.stringify({ label: "bad", "x-count": "two", Other: true }),
|
||||
workspace.entry,
|
||||
runner,
|
||||
);
|
||||
expect(invalid.valid).toBe(false);
|
||||
expect(invalid.diagnostics.map((item) => item.message).join("\n")).toMatch(
|
||||
/pattern.*does not match|type.*integer|additionalProperties|false schema/iu,
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses synchronous regex validation instead of executing on the caller thread", () => {
|
||||
const workspace = inspectWorkspace([
|
||||
{
|
||||
name: "pattern.schema.json",
|
||||
source: JSON.stringify({ type: "string", pattern: "^(a+)+$" }),
|
||||
},
|
||||
]);
|
||||
const result = validateJsonInstance(workspace, JSON.stringify("aaaa"));
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.diagnostics[0]?.message).toMatch(
|
||||
/bounded asynchronous validation|isolated worker/iu,
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves local references, generates a sample, and validates instances", () => {
|
||||
const workspace = inspectWorkspace([entry, support], entry.name);
|
||||
expect(workspace.references).toEqual([
|
||||
@@ -451,7 +522,7 @@ describe("schema workspace", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("refuses executable regex keywords in the bounded validator", () => {
|
||||
it("routes executable regex keywords away from the synchronous validator", () => {
|
||||
const workspace = inspectWorkspace([
|
||||
{
|
||||
name: "pattern.json",
|
||||
@@ -460,7 +531,9 @@ describe("schema workspace", () => {
|
||||
]);
|
||||
const result = validateJsonInstance(workspace, JSON.stringify("aaaa"));
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.diagnostics[0]?.message).toMatch(/pattern.*not executed/iu);
|
||||
expect(result.diagnostics[0]?.message).toMatch(
|
||||
/regular expressions.*bounded asynchronous validation/iu,
|
||||
);
|
||||
});
|
||||
|
||||
it("also refuses regex and dynamic-reference keywords in supplied reference documents", () => {
|
||||
|
||||
Reference in New Issue
Block a user