From 5eb3c89972a7d3f6130c44db377fd40f7c99df98 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Tue, 30 Jun 2026 12:01:56 +1000 Subject: [PATCH 1/9] Add canvas schema and extension submission checks Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-website.yml | 2 + .../workflows/validate-canvas-extensions.yml | 185 +++++++----------- .schemas/canvas.schema.json | 106 ++++++++++ 3 files changed, 179 insertions(+), 114 deletions(-) create mode 100644 .schemas/canvas.schema.json diff --git a/.github/workflows/build-website.yml b/.github/workflows/build-website.yml index bfdc36c7d..efcd2bfcd 100644 --- a/.github/workflows/build-website.yml +++ b/.github/workflows/build-website.yml @@ -11,6 +11,8 @@ on: - instructions - hooks - workflows + - extensions + - .schemas/canvas.schema.json permissions: contents: read diff --git a/.github/workflows/validate-canvas-extensions.yml b/.github/workflows/validate-canvas-extensions.yml index ceb2124c1..d8ec733fd 100644 --- a/.github/workflows/validate-canvas-extensions.yml +++ b/.github/workflows/validate-canvas-extensions.yml @@ -6,130 +6,87 @@ on: types: [opened, synchronize, reopened] paths: - "extensions/**" + - ".schemas/canvas.schema.json" permissions: contents: read - pull-requests: write jobs: validate: runs-on: ubuntu-latest steps: - - name: Checkout code + - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 - - name: Validate changed canvas extensions - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - script: | - const fs = require('fs'); - const path = require('path'); - - // Collect changed extension directories from the PR diff - const { execSync } = require('child_process'); - const changedFiles = execSync( - `git diff --name-only origin/${{ github.base_ref }}...HEAD` - ).toString().trim().split('\n').filter(Boolean); - - const EXTENSIONS_DIR = 'extensions'; - const EXTERNAL_ASSETS_DIR = 'external-assets'; - - const changedExtDirs = new Set(); - for (const file of changedFiles) { - const parts = file.split('/'); - if (parts[0] === EXTENSIONS_DIR && parts.length >= 2) { - const extName = parts[1]; - // Skip the external-assets directory — it's not a canvas extension - // Also skip external.json and other files at extensions root level - if (extName !== EXTERNAL_ASSETS_DIR && !extName.includes('.')) { - changedExtDirs.add(path.join(EXTENSIONS_DIR, extName)); - } - } - } - - if (changedExtDirs.size === 0) { - console.log('No canvas extension directories changed — skipping validation.'); - return; - } - - console.log(`Validating ${changedExtDirs.size} extension(s): ${[...changedExtDirs].join(', ')}`); - - const errors = []; - - for (const extDir of changedExtDirs) { - if (!fs.existsSync(extDir)) { - // Directory was deleted — skip - console.log(`${extDir} no longer exists (deleted?), skipping.`); - continue; - } - - const extName = path.basename(extDir); - - // Rule 1: must contain extension.mjs - const mainFile = path.join(extDir, 'extension.mjs'); - if (!fs.existsSync(mainFile)) { - errors.push( - `**\`${extDir}\`**: missing required \`extension.mjs\`. ` + - `Canvas extensions must have their entry point named \`extension.mjs\`.` - ); - } - - // Rule 2: must contain assets/preview.png - const previewFile = path.join(extDir, 'assets', 'preview.png'); - if (!fs.existsSync(previewFile)) { - errors.push( - `**\`${extDir}\`**: missing required \`assets/preview.png\`. ` + - `Canvas extensions must include a screenshot at \`assets/preview.png\` ` + - `so reviewers and users can preview the extension before installing it.` - ); - } - } - - if (errors.length === 0) { - console.log('✅ All changed canvas extensions pass validation.'); - return; - } - - const isFork = context.payload.pull_request.head.repo.fork; - const body = [ - '❌ **Canvas extension validation failed**', - '', - 'The following issue(s) were found in changed canvas extension(s):', - '', - ...errors.map(e => `- ${e}`), - '', - '---', - '', - '### Required structure for canvas extensions', - '', - 'Each extension folder under `extensions/` must contain:', - '', - '| Path | Required | Description |', - '|------|----------|-------------|', - '| `extension.mjs` | ✅ | Entry point for the canvas extension |', - '| `assets/preview.png` | ✅ | Screenshot shown on the website and in the marketplace |', - '', - 'Please add the missing file(s) and push an update to this PR.', - ].join('\n'); - - if (!isFork) { - try { - await github.rest.pulls.createReview({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - event: 'REQUEST_CHANGES', - body - }); - } catch (error) { - core.warning(`Could not post PR review: ${error.message}`); - core.warning(body); - } - } else { - core.warning('PR is from a fork — skipping createReview to avoid permission errors.'); - core.warning(body); - } - - core.setFailed(`Canvas extension validation failed with ${errors.length} error(s).`); + node-version: "22" + cache: "npm" + + - name: Validate changed canvas extensions + run: | + set -euo pipefail + + mapfile -t changed_extensions < <( + git diff --name-only "origin/${{ github.base_ref }}...HEAD" | + awk -F/ '$1 == "extensions" && $2 != "" && $2 != "external-assets" && $2 !~ /\./ { print "extensions/" $2 }' | + sort -u + ) + + if [ "${#changed_extensions[@]}" -eq 0 ]; then + echo "No canvas extension directories changed — skipping validation." + exit 0 + fi + + echo "Validating ${#changed_extensions[@]} extension(s): ${changed_extensions[*]}" + + errors=() + + for ext_dir in "${changed_extensions[@]}"; do + if [ ! -d "$ext_dir" ]; then + echo "$ext_dir no longer exists (deleted?), skipping." + continue + fi + + if [ ! -f "$ext_dir/extension.mjs" ]; then + errors+=("\`$ext_dir\`: missing required \`extension.mjs\`.") + fi + + if [ ! -f "$ext_dir/canvas.json" ]; then + errors+=("\`$ext_dir\`: missing required \`canvas.json\`.") + continue + fi + + if [ ! -f "$ext_dir/assets/preview.png" ]; then + errors+=("\`$ext_dir\`: missing required \`assets/preview.png\`.") + fi + + if ! schema_output="$(npx --yes ajv-cli@5 validate --spec=draft7 --strict=false -s .schemas/canvas.schema.json -d "$ext_dir/canvas.json" 2>&1)"; then + condensed_output="$(echo "$schema_output" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')" + errors+=("\`$ext_dir/canvas.json\`: schema validation failed against \`.schemas/canvas.schema.json\` ($condensed_output).") + continue + fi + + mapfile -t screenshot_paths < <( + node -e 'const fs = require("fs"); const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const paths = [manifest?.screenshots?.icon?.path, manifest?.screenshots?.gallery?.path].filter((value) => typeof value === "string" && value.trim().length > 0); for (const value of [...new Set(paths)]) { console.log(value); }' "$ext_dir/canvas.json" + ) + + for screenshot_path in "${screenshot_paths[@]}"; do + if [ ! -f "$ext_dir/$screenshot_path" ]; then + errors+=("\`$ext_dir/canvas.json\`: screenshot path \`$screenshot_path\` does not exist in the extension directory.") + fi + done + done + + if [ "${#errors[@]}" -ne 0 ]; then + echo "❌ Canvas extension validation failed:" + for error in "${errors[@]}"; do + echo "- $error" + done + exit 1 + fi + + echo "✅ All changed canvas extensions passed validation." diff --git a/.schemas/canvas.schema.json b/.schemas/canvas.schema.json new file mode 100644 index 000000000..c10cfd070 --- /dev/null +++ b/.schemas/canvas.schema.json @@ -0,0 +1,106 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Canvas Extension Manifest", + "description": "Schema for extensions//canvas.json files", + "type": "object", + "required": ["id", "name", "description", "version", "keywords", "screenshots"], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the canvas extension", + "pattern": "^[a-z0-9-]+$", + "minLength": 1, + "maxLength": 100 + }, + "name": { + "type": "string", + "description": "Display name for the extension", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": "string", + "description": "Human-friendly description of what the extension does", + "minLength": 1, + "maxLength": 500 + }, + "version": { + "type": "string", + "description": "Semantic version of the extension metadata", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$" + }, + "author": { + "type": "object", + "description": "Optional author metadata", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "url": { + "type": "string", + "format": "uri", + "maxLength": 2048 + } + } + }, + "keywords": { + "type": "array", + "description": "Keywords used for search and filtering", + "items": { + "type": "string", + "pattern": "^[a-z0-9-]+$", + "minLength": 1, + "maxLength": 50 + }, + "minItems": 1, + "maxItems": 20, + "uniqueItems": true + }, + "screenshots": { + "type": "object", + "description": "Screenshot metadata for icon and gallery cards", + "required": ["icon", "gallery"], + "additionalProperties": false, + "properties": { + "icon": { + "$ref": "#/definitions/screenshot" + }, + "gallery": { + "$ref": "#/definitions/screenshot" + } + } + } + }, + "definitions": { + "screenshot": { + "type": "object", + "required": ["path", "type"], + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "description": "Path relative to the extension root", + "pattern": "^assets/[A-Za-z0-9._/-]+\\.(png|jpg|jpeg|gif|webp|svg)$", + "minLength": 1, + "maxLength": 200 + }, + "type": { + "type": "string", + "description": "MIME type for the referenced image", + "enum": [ + "image/png", + "image/jpeg", + "image/gif", + "image/webp", + "image/svg+xml" + ] + } + } + } + } +} From 5c75ec0d7c9571e92377acf559fe6869cba61861 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 03:48:42 +0000 Subject: [PATCH 2/9] fix: use namespace import for js-yaml Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- eng/yaml-parser.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/yaml-parser.mjs b/eng/yaml-parser.mjs index 19bb3f73c..3e202668a 100644 --- a/eng/yaml-parser.mjs +++ b/eng/yaml-parser.mjs @@ -1,6 +1,6 @@ // YAML parser for frontmatter parsing using vfile-matter import fs from "fs"; -import yaml from "js-yaml"; +import * as yaml from "js-yaml"; import path from "path"; import { VFile } from "vfile"; import { matter } from "vfile-matter"; From 6e6c4b5cf1b605c255ec208db2afe1f7562232c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 04:07:42 +0000 Subject: [PATCH 3/9] Fix contributors page build markup Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- website/src/pages/contributors.astro | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/website/src/pages/contributors.astro b/website/src/pages/contributors.astro index 2f9a4e9ff..42b0450c7 100644 --- a/website/src/pages/contributors.astro +++ b/website/src/pages/contributors.astro @@ -499,9 +499,8 @@ import PageHeader from '../components/PageHeader.astro'; - - Add your contributions - + + Add your contributions From 85fca7f8db08368f3d1cd298d643e7e8869aa3a4 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Tue, 30 Jun 2026 14:19:10 +1000 Subject: [PATCH 4/9] Address PR feedback on canvas schema validation - Add ajv-cli@5 as a pinned devDependency; install via npm ci in CI instead of npx --yes - Fix screenshot path regex to prevent .. traversal segments - Validate canvas.schema.json is parseable JSON even on schema-only PRs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../workflows/validate-canvas-extensions.yml | 14 +- .schemas/canvas.schema.json | 25 +- package-lock.json | 297 ++++++++++++++++++ package.json | 1 + 4 files changed, 331 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate-canvas-extensions.yml b/.github/workflows/validate-canvas-extensions.yml index d8ec733fd..3c64d367b 100644 --- a/.github/workflows/validate-canvas-extensions.yml +++ b/.github/workflows/validate-canvas-extensions.yml @@ -26,10 +26,22 @@ jobs: node-version: "22" cache: "npm" + - name: Install dependencies + run: npm ci + - name: Validate changed canvas extensions run: | set -euo pipefail + # Validate the schema file itself is parseable JSON whenever it is present + if [ -f ".schemas/canvas.schema.json" ]; then + if ! node -e "JSON.parse(require('fs').readFileSync('.schemas/canvas.schema.json','utf8'))" 2>/dev/null; then + echo "❌ .schemas/canvas.schema.json is not valid JSON" + exit 1 + fi + echo "✅ .schemas/canvas.schema.json is valid JSON" + fi + mapfile -t changed_extensions < <( git diff --name-only "origin/${{ github.base_ref }}...HEAD" | awk -F/ '$1 == "extensions" && $2 != "" && $2 != "external-assets" && $2 !~ /\./ { print "extensions/" $2 }' | @@ -64,7 +76,7 @@ jobs: errors+=("\`$ext_dir\`: missing required \`assets/preview.png\`.") fi - if ! schema_output="$(npx --yes ajv-cli@5 validate --spec=draft7 --strict=false -s .schemas/canvas.schema.json -d "$ext_dir/canvas.json" 2>&1)"; then + if ! schema_output="$(npx ajv-cli validate --spec=draft7 --strict=false -s .schemas/canvas.schema.json -d "$ext_dir/canvas.json" 2>&1)"; then condensed_output="$(echo "$schema_output" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')" errors+=("\`$ext_dir/canvas.json\`: schema validation failed against \`.schemas/canvas.schema.json\` ($condensed_output).") continue diff --git a/.schemas/canvas.schema.json b/.schemas/canvas.schema.json index c10cfd070..d1dc26569 100644 --- a/.schemas/canvas.schema.json +++ b/.schemas/canvas.schema.json @@ -3,7 +3,14 @@ "title": "Canvas Extension Manifest", "description": "Schema for extensions//canvas.json files", "type": "object", - "required": ["id", "name", "description", "version", "keywords", "screenshots"], + "required": [ + "id", + "name", + "description", + "version", + "keywords", + "screenshots" + ], "additionalProperties": false, "properties": { "id": { @@ -33,7 +40,9 @@ "author": { "type": "object", "description": "Optional author metadata", - "required": ["name"], + "required": [ + "name" + ], "additionalProperties": false, "properties": { "name": { @@ -64,7 +73,10 @@ "screenshots": { "type": "object", "description": "Screenshot metadata for icon and gallery cards", - "required": ["icon", "gallery"], + "required": [ + "icon", + "gallery" + ], "additionalProperties": false, "properties": { "icon": { @@ -79,13 +91,16 @@ "definitions": { "screenshot": { "type": "object", - "required": ["path", "type"], + "required": [ + "path", + "type" + ], "additionalProperties": false, "properties": { "path": { "type": "string", "description": "Path relative to the extension root", - "pattern": "^assets/[A-Za-z0-9._/-]+\\.(png|jpg|jpeg|gif|webp|svg)$", + "pattern": "^assets/(?:[A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)*\\.(png|jpg|jpeg|gif|webp|svg)$", "minLength": 1, "maxLength": 200 }, diff --git a/package-lock.json b/package-lock.json index bcd194d18..922856352 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "vfile-matter": "^5.0.1" }, "devDependencies": { + "ajv-cli": "^5.0.0", "all-contributors-cli": "^6.26.1" } }, @@ -33,6 +34,74 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-cli": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ajv-cli/-/ajv-cli-5.0.0.tgz", + "integrity": "sha512-LY4m6dUv44HTyhV+u2z5uX4EhPYTM38Iv1jdgDJJJCyOOuqB8KtZEGjPZ2T+sh5ZIJrXUfgErYx/j3gLd3+PlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0", + "fast-json-patch": "^2.0.0", + "glob": "^7.1.0", + "js-yaml": "^3.14.0", + "json-schema-migrate": "^2.0.0", + "json5": "^2.1.3", + "minimist": "^1.2.0" + }, + "bin": { + "ajv": "dist/index.js" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } + } + }, + "node_modules/ajv-cli/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/ajv-cli/node_modules/js-yaml": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/all-contributors-cli": { "version": "6.26.1", "resolved": "https://registry.npmjs.org/all-contributors-cli/-/all-contributors-cli-6.26.1.tgz", @@ -116,6 +185,24 @@ "dev": true, "license": "MIT" }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -205,6 +292,13 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -239,6 +333,20 @@ "node": ">=0.8.0" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -254,6 +362,50 @@ "node": ">=4" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-patch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-2.2.1.tgz", + "integrity": "sha512-4j5uBaTnsYAV5ebkidvxiLUYOwjQ+JSFljeqfTxCrH9bDmlCQaOJFS84oDJ2rAXZq2yskmk3ORfoP9DCwqFNig==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fast-json-patch/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -284,6 +436,13 @@ "node": ">=8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -294,6 +453,28 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -317,6 +498,25 @@ "node": ">=0.10.0" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/inquirer": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", @@ -389,6 +589,36 @@ "node": ">=10" } }, + "node_modules/json-schema-migrate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/json-schema-migrate/-/json-schema-migrate-2.0.0.tgz", + "integrity": "sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -419,6 +649,29 @@ "node": ">=6" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -447,6 +700,16 @@ } } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -522,6 +785,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pegjs": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", @@ -575,6 +848,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -640,6 +923,13 @@ "dev": true, "license": "ISC" }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -823,6 +1113,13 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index b7faf9d4b..f630629ee 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "author": "GitHub", "license": "MIT", "devDependencies": { + "ajv-cli": "^5.0.0", "all-contributors-cli": "^6.26.1" }, "dependencies": { From ddd08f09b7029cb72beacd41bbe9ae30810a3ffd Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Tue, 30 Jun 2026 14:32:16 +1000 Subject: [PATCH 5/9] Harden canvas extension workflow against injection attacks Switch from newline to null-terminated git diff output (git diff -z) so filenames containing newlines are read atomically, matching the existing skill-check.yml pattern. Add an allowlist regex guard on the extracted extension directory name immediately after it is parsed from git diff output. Any name not matching ^[a-z0-9][a-z0-9-]*$ (e.g. names containing dollar signs, parentheses, spaces, or other shell metacharacters) is silently skipped before being used anywhere in the script. Add a matching allowlist guard on each screenshot path extracted from canvas.json before the file-existence check, so a crafted manifest cannot supply a path with shell metacharacters or traversal segments even after the schema check passes. Follows the same defence-in-depth pattern introduced after the injection PoCs in #1236 and #1240. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../workflows/validate-canvas-extensions.yml | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate-canvas-extensions.yml b/.github/workflows/validate-canvas-extensions.yml index 3c64d367b..e5fcbc406 100644 --- a/.github/workflows/validate-canvas-extensions.yml +++ b/.github/workflows/validate-canvas-extensions.yml @@ -42,11 +42,31 @@ jobs: echo "✅ .schemas/canvas.schema.json is valid JSON" fi - mapfile -t changed_extensions < <( - git diff --name-only "origin/${{ github.base_ref }}...HEAD" | - awk -F/ '$1 == "extensions" && $2 != "" && $2 != "external-assets" && $2 !~ /\./ { print "extensions/" $2 }' | - sort -u - ) + # Collect changed extension directories. + # Use null-terminated (-z) output from git diff so filenames containing newlines + # or other special characters are read atomically (matches the pattern in skill-check.yml). + # Each extracted name is then validated against a strict allowlist regex before use, + # rejecting anything containing shell metacharacters ($, (, ), spaces, etc.). + declare -A seen_dirs=() + changed_extensions=() + + while IFS= read -r -d '' file; do + case "$file" in + extensions/*) + ext_name="${file#extensions/}" + ext_name="${ext_name%%/*}" + # Allowlist: extension directory names must be lowercase alphanumeric + hyphens only. + # Any name that does not match (e.g. $(id), spaces, slashes) is silently skipped — + # it cannot be a valid extension and cannot produce a valid canvas.json. + if [[ "$ext_name" =~ ^[a-z0-9][a-z0-9-]*$ ]] && \ + [ "$ext_name" != "external-assets" ] && \ + [ -z "${seen_dirs[$ext_name]+x}" ]; then + seen_dirs["$ext_name"]=1 + changed_extensions+=("extensions/$ext_name") + fi + ;; + esac + done < <(git diff --name-only -z "origin/${{ github.base_ref }}...HEAD") if [ "${#changed_extensions[@]}" -eq 0 ]; then echo "No canvas extension directories changed — skipping validation." @@ -87,6 +107,13 @@ jobs: ) for screenshot_path in "${screenshot_paths[@]}"; do + # Allowlist: screenshot paths must follow assets/. — no .., no shell metacharacters. + # This mirrors the schema regex and guards against a crafted canvas.json producing an + # unexpected value from node stdout that could bypass the schema check. + if [[ ! "$screenshot_path" =~ ^assets/([A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*\.[A-Za-z0-9]+$ ]]; then + errors+=("\`$ext_dir/canvas.json\`: screenshot path \`$screenshot_path\` is not a valid assets path.") + continue + fi if [ ! -f "$ext_dir/$screenshot_path" ]; then errors+=("\`$ext_dir/canvas.json\`: screenshot path \`$screenshot_path\` does not exist in the extension directory.") fi @@ -101,4 +128,4 @@ jobs: exit 1 fi - echo "✅ All changed canvas extensions passed validation." + echo "✅ All changed canvas extensions passed validation." \ No newline at end of file From a752f672b96dab353579c663626a15266a0ece23 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Tue, 30 Jun 2026 14:43:19 +1000 Subject: [PATCH 6/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- website/src/pages/contributors.astro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/contributors.astro b/website/src/pages/contributors.astro index 42b0450c7..fe77a4fd3 100644 --- a/website/src/pages/contributors.astro +++ b/website/src/pages/contributors.astro @@ -499,7 +499,7 @@ import PageHeader from '../components/PageHeader.astro'; - + Add your contributions From 1ed1dcc71ba088c1bccf9e690902eba4b5236cf2 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Tue, 30 Jun 2026 14:52:23 +1000 Subject: [PATCH 7/9] Replace ajv-cli with in-repo schema validator - Remove ajv-cli to avoid vulnerable/deprecated transitive dependencies - Add eng/validate-json-schema.mjs using ajv + ajv-formats - Update validate-canvas-extensions workflow to use local script - Use npm ci --ignore-scripts in PR validation job Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../workflows/validate-canvas-extensions.yml | 15 +- eng/validate-json-schema.mjs | 75 ++++++ package-lock.json | 237 +----------------- package.json | 3 +- 4 files changed, 92 insertions(+), 238 deletions(-) create mode 100644 eng/validate-json-schema.mjs diff --git a/.github/workflows/validate-canvas-extensions.yml b/.github/workflows/validate-canvas-extensions.yml index e5fcbc406..8e3068ad6 100644 --- a/.github/workflows/validate-canvas-extensions.yml +++ b/.github/workflows/validate-canvas-extensions.yml @@ -27,19 +27,16 @@ jobs: cache: "npm" - name: Install dependencies - run: npm ci + run: npm ci --ignore-scripts - name: Validate changed canvas extensions run: | set -euo pipefail - # Validate the schema file itself is parseable JSON whenever it is present - if [ -f ".schemas/canvas.schema.json" ]; then - if ! node -e "JSON.parse(require('fs').readFileSync('.schemas/canvas.schema.json','utf8'))" 2>/dev/null; then - echo "❌ .schemas/canvas.schema.json is not valid JSON" - exit 1 - fi - echo "✅ .schemas/canvas.schema.json is valid JSON" + # Validate schema structure once, even for schema-only PRs. + if ! node ./eng/validate-json-schema.mjs --schema .schemas/canvas.schema.json; then + echo "❌ .schemas/canvas.schema.json failed schema compilation" + exit 1 fi # Collect changed extension directories. @@ -96,7 +93,7 @@ jobs: errors+=("\`$ext_dir\`: missing required \`assets/preview.png\`.") fi - if ! schema_output="$(npx ajv-cli validate --spec=draft7 --strict=false -s .schemas/canvas.schema.json -d "$ext_dir/canvas.json" 2>&1)"; then + if ! schema_output="$(node ./eng/validate-json-schema.mjs --schema .schemas/canvas.schema.json --data "$ext_dir/canvas.json" 2>&1)"; then condensed_output="$(echo "$schema_output" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')" errors+=("\`$ext_dir/canvas.json\`: schema validation failed against \`.schemas/canvas.schema.json\` ($condensed_output).") continue diff --git a/eng/validate-json-schema.mjs b/eng/validate-json-schema.mjs new file mode 100644 index 000000000..e1eba3bf7 --- /dev/null +++ b/eng/validate-json-schema.mjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node + +import fs from "fs"; +import path from "path"; +import Ajv from "ajv"; +import addFormats from "ajv-formats"; + +function parseArgs(argv) { + const args = { schema: null, data: null }; + for (let i = 2; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === "--schema") { + args.schema = argv[i + 1] || null; + i += 1; + } else if (arg === "--data") { + args.data = argv[i + 1] || null; + i += 1; + } + } + return args; +} + +function readJson(filePath) { + const content = fs.readFileSync(filePath, "utf8"); + return JSON.parse(content); +} + +const args = parseArgs(process.argv); +if (!args.schema) { + console.error("Missing required argument: --schema "); + process.exit(1); +} + +const schemaPath = path.resolve(process.cwd(), args.schema); +let schema; +try { + schema = readJson(schemaPath); +} catch (error) { + console.error(`Invalid schema JSON at ${args.schema}: ${error.message}`); + process.exit(1); +} + +const ajv = new Ajv({ strict: false, allErrors: true }); +addFormats(ajv); + +let validate; +try { + validate = ajv.compile(schema); +} catch (error) { + console.error(`Invalid schema at ${args.schema}: ${error.message}`); + process.exit(1); +} + +if (!args.data) { + console.log(`Schema is valid: ${args.schema}`); + process.exit(0); +} + +const dataPath = path.resolve(process.cwd(), args.data); +let data; +try { + data = readJson(dataPath); +} catch (error) { + console.error(`Invalid data JSON at ${args.data}: ${error.message}`); + process.exit(1); +} + +const valid = validate(data); +if (!valid) { + const message = ajv.errorsText(validate.errors, { separator: "; " }); + console.error(`Schema validation failed for ${args.data}: ${message}`); + process.exit(1); +} + +console.log(`Schema validation passed: ${args.data}`); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 922856352..4bd553e68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "vfile-matter": "^5.0.1" }, "devDependencies": { - "ajv-cli": "^5.0.0", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", "all-contributors-cli": "^6.26.1" } }, @@ -51,57 +52,24 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-cli": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ajv-cli/-/ajv-cli-5.0.0.tgz", - "integrity": "sha512-LY4m6dUv44HTyhV+u2z5uX4EhPYTM38Iv1jdgDJJJCyOOuqB8KtZEGjPZ2T+sh5ZIJrXUfgErYx/j3gLd3+PlQ==", + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^8.0.0", - "fast-json-patch": "^2.0.0", - "glob": "^7.1.0", - "js-yaml": "^3.14.0", - "json-schema-migrate": "^2.0.0", - "json5": "^2.1.3", - "minimist": "^1.2.0" - }, - "bin": { - "ajv": "dist/index.js" + "ajv": "^8.0.0" }, "peerDependencies": { - "ts-node": ">=9.0.0" + "ajv": "^8.0.0" }, "peerDependenciesMeta": { - "ts-node": { + "ajv": { "optional": true } } }, - "node_modules/ajv-cli/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/ajv-cli/node_modules/js-yaml": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", - "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/all-contributors-cli": { "version": "6.26.1", "resolved": "https://registry.npmjs.org/all-contributors-cli/-/all-contributors-cli-6.26.1.tgz", @@ -185,24 +153,6 @@ "dev": true, "license": "MIT" }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -292,13 +242,6 @@ "dev": true, "license": "MIT" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -333,20 +276,6 @@ "node": ">=0.8.0" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -369,26 +298,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-json-patch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-2.2.1.tgz", - "integrity": "sha512-4j5uBaTnsYAV5ebkidvxiLUYOwjQ+JSFljeqfTxCrH9bDmlCQaOJFS84oDJ2rAXZq2yskmk3ORfoP9DCwqFNig==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^2.0.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/fast-json-patch/node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-uri": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", @@ -436,13 +345,6 @@ "node": ">=8" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -453,28 +355,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -498,25 +378,6 @@ "node": ">=0.10.0" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, "node_modules/inquirer": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", @@ -589,16 +450,6 @@ "node": ">=10" } }, - "node_modules/json-schema-migrate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/json-schema-migrate/-/json-schema-migrate-2.0.0.tgz", - "integrity": "sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - } - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -606,19 +457,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -649,29 +487,6 @@ "node": ">=6" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -700,16 +515,6 @@ } } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -785,16 +590,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pegjs": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", @@ -923,13 +718,6 @@ "dev": true, "license": "ISC" }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1113,13 +901,6 @@ "node": ">=8" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index f630629ee..17d1331fe 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "author": "GitHub", "license": "MIT", "devDependencies": { - "ajv-cli": "^5.0.0", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", "all-contributors-cli": "^6.26.1" }, "dependencies": { From b48bca2cc870a35b11c15eb9b7cd8e3be552d2c0 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 1 Jul 2026 09:50:07 +1000 Subject: [PATCH 8/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../workflows/validate-canvas-extensions.yml | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/validate-canvas-extensions.yml b/.github/workflows/validate-canvas-extensions.yml index 8e3068ad6..4b36dc487 100644 --- a/.github/workflows/validate-canvas-extensions.yml +++ b/.github/workflows/validate-canvas-extensions.yml @@ -46,18 +46,26 @@ jobs: # rejecting anything containing shell metacharacters ($, (, ), spaces, etc.). declare -A seen_dirs=() changed_extensions=() + errors=() while IFS= read -r -d '' file; do case "$file" in extensions/*) ext_name="${file#extensions/}" ext_name="${ext_name%%/*}" + + if [ "$ext_name" = "external-assets" ]; then + continue + fi + # Allowlist: extension directory names must be lowercase alphanumeric + hyphens only. - # Any name that does not match (e.g. $(id), spaces, slashes) is silently skipped — - # it cannot be a valid extension and cannot produce a valid canvas.json. - if [[ "$ext_name" =~ ^[a-z0-9][a-z0-9-]*$ ]] && \ - [ "$ext_name" != "external-assets" ] && \ - [ -z "${seen_dirs[$ext_name]+x}" ]; then + # Fail for disallowed names so a PR cannot bypass validation by using a nonconforming folder. + if [[ ! "$ext_name" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then + errors+=("\`extensions/$ext_name\`: invalid extension directory name (must match ^[a-z0-9][a-z0-9-]*$).") + continue + fi + + if [ -z "${seen_dirs[$ext_name]+x}" ]; then seen_dirs["$ext_name"]=1 changed_extensions+=("extensions/$ext_name") fi @@ -66,14 +74,19 @@ jobs: done < <(git diff --name-only -z "origin/${{ github.base_ref }}...HEAD") if [ "${#changed_extensions[@]}" -eq 0 ]; then + if [ "${#errors[@]}" -ne 0 ]; then + echo "❌ Canvas extension validation failed:" + for error in "${errors[@]}"; do + echo "- $error" + done + exit 1 + fi echo "No canvas extension directories changed — skipping validation." exit 0 fi echo "Validating ${#changed_extensions[@]} extension(s): ${changed_extensions[*]}" - errors=() - for ext_dir in "${changed_extensions[@]}"; do if [ ! -d "$ext_dir" ]; then echo "$ext_dir no longer exists (deleted?), skipping." From cea8540962391d8ea004d1597718508e04eb4978 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 1 Jul 2026 09:50:30 +1000 Subject: [PATCH 9/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/validate-canvas-extensions.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/validate-canvas-extensions.yml b/.github/workflows/validate-canvas-extensions.yml index 4b36dc487..558a5766c 100644 --- a/.github/workflows/validate-canvas-extensions.yml +++ b/.github/workflows/validate-canvas-extensions.yml @@ -117,10 +117,7 @@ jobs: ) for screenshot_path in "${screenshot_paths[@]}"; do - # Allowlist: screenshot paths must follow assets/. — no .., no shell metacharacters. - # This mirrors the schema regex and guards against a crafted canvas.json producing an - # unexpected value from node stdout that could bypass the schema check. - if [[ ! "$screenshot_path" =~ ^assets/([A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*\.[A-Za-z0-9]+$ ]]; then + if [[ ! "$screenshot_path" =~ ^assets/([A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*\.(png|jpe?g|gif|webp|svg)$ ]]; then errors+=("\`$ext_dir/canvas.json\`: screenshot path \`$screenshot_path\` is not a valid assets path.") continue fi