From d0b959e369690018160e33e70ec9be8ea7188966 Mon Sep 17 00:00:00 2001 From: Greg Soucy Date: Thu, 19 Mar 2026 22:54:41 -0400 Subject: [PATCH] Tighten validation and release metadata hygiene --- INTEGRATOR.md | 2 +- README.md | 4 +- SECURITY_PROVENANCE.md | 2 +- checksums.txt | 4 +- manifest.json | 2 +- package.json | 7 ++- schemas/v1.1.0/index.json | 4 +- scripts/generate-checksums.mjs | 1 - scripts/validate-all.mjs | 82 +++++----------------------------- scripts/validate-examples.mjs | 69 ++-------------------------- scripts/validate-integrity.mjs | 1 - tsconfig.json | 16 ------- 12 files changed, 29 insertions(+), 165 deletions(-) delete mode 100644 tsconfig.json diff --git a/INTEGRATOR.md b/INTEGRATOR.md index 7629e9b..f8c0fee 100644 --- a/INTEGRATOR.md +++ b/INTEGRATOR.md @@ -61,7 +61,7 @@ The checksum-covered payload for this release is intentionally narrow: - `examples/v1.1.0/` - `manifest.json` -`checksums.txt` is the ledger for that payload. It is not itself inside the hashed set. Prose docs remain authoritative for interpretation and release process, but they are outside the checksum boundary. +`checksums.txt` is the ledger for that payload. It is not itself inside the hashed set, so checksum verification confirms covered files only relative to the checked-in `checksums.txt` ledger and does not independently authenticate that ledger. Prose docs remain authoritative for interpretation and release process, but they are outside the checksum boundary. ## TypeScript guidance diff --git a/README.md b/README.md index 7166d9f..dd7bdfd 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ sha256sum -c checksums.txt - `npm run validate:schemas` checks current-line metadata, schema identity, layout, and manifest/index alignment expectations. - `npm run validate:examples` validates every current-line JSON valid and invalid example against the canonical schemas. - `npm run validate:integrity` verifies the checksum file scope and hash coverage for the current release artifact set. -- `checksums.txt` intentionally covers machine-validated release payloads only: `manifest.json`, `schemas/v1.1.0/index.json`, `schemas/v1.1.0/`, and `examples/v1.1.0/`. +- `checksums.txt` intentionally covers machine-validated release payloads only: `manifest.json`, `schemas/v1.1.0/`, and `examples/v1.1.0/`. ## Agent Cards and Commons alignment @@ -254,7 +254,7 @@ The v1.1.0 checksum-covered machine-artifact set is intentionally limited to: - `examples/v1.1.0/` - `manifest.json` -`checksums.txt` is the generated hash ledger for that machine-artifact set; it describes that surface but is not itself part of the hashed payload. Release-defining prose docs such as `README.md`, `SPEC.md`, `POLICY.md`, `SECURITY_PROVENANCE.md`, `INTEGRATOR.md`, and `ONBOARDING.md` are authoritative guidance, but they are outside the checksum surface unless the tooling is expanded deliberately in a later release. +`checksums.txt` is the generated hash ledger for that machine-artifact set; it describes that surface but is not itself part of the hashed payload, so checksum verification confirms covered files only relative to the checked-in `checksums.txt` ledger and does not independently authenticate that ledger. Release-defining prose docs such as `README.md`, `SPEC.md`, `POLICY.md`, `SECURITY_PROVENANCE.md`, `INTEGRATOR.md`, and `ONBOARDING.md` are authoritative guidance, but they are outside the checksum surface unless the tooling is expanded deliberately in a later release. For external verification, the minimal path is: diff --git a/SECURITY_PROVENANCE.md b/SECURITY_PROVENANCE.md index 91f0081..154c051 100644 --- a/SECURITY_PROVENANCE.md +++ b/SECURITY_PROVENANCE.md @@ -10,7 +10,7 @@ Checksum-covered machine-artifact roots: - `examples/v1.1.0/` - `manifest.json` -`checksums.txt` is the generated SHA-256 ledger for that machine-artifact set. It describes the checksum-covered payload but is not itself part of the hashed payload. Release-defining prose docs in the repository are intentionally outside this checksum boundary and must not be described as checksum-protected. +`checksums.txt` is the generated SHA-256 ledger for that machine-artifact set. It describes the checksum-covered payload but is not itself part of the hashed payload, so checksum verification confirms covered files only relative to the checked-in `checksums.txt` ledger and does not independently authenticate that ledger. Release-defining prose docs in the repository are intentionally outside this checksum boundary and must not be described as checksum-protected. Release integrity state for this repository: diff --git a/checksums.txt b/checksums.txt index adebf58..1564f0b 100644 --- a/checksums.txt +++ b/checksums.txt @@ -28,7 +28,7 @@ a2a5e61fa04e12786a848e03bbabbc3f9d066ca55a6f48cb1ae1140f6373bf94 examples/v1.1. 9492d90ea14ad35eeb8acd03248ce6061ccdc04a7aff4ed538d8c42be3abc015 examples/v1.1.0/commercial/verify/valid/002-verify.request.valid.json 50874f3eea69a51ac132873b05e39318e4c2241078ca5e258e466934935ec945 examples/v1.1.0/commercial/verify/valid/900-verify.receipt.valid.json 455d19ad1b7ef98e436d8f1c675fee7f2716eb17d301da8d2cc4e2e2c51e624a examples/v1.1.0/commercial/verify/valid/901-verify.receipt.valid.json -80fa9124c1560d0e55b83554d83581dabf72505cc4d9c1354157f51fddd9686a manifest.json +0e4a8b868fa2f7d68bd5f65d7687a91ed97cd380b1b979dd7dc15b44a5a28939 manifest.json 4d1178e63f6c5a9e1e4d9cc4d386fbad023dd5a85c000ff193285b1fed9af243 schemas/v1.1.0/commercial/authorize/authorize.receipt.schema.json ef5da55ba5acdd43e8d2715204938762a63819dd370ebc8dfedad014617259c3 schemas/v1.1.0/commercial/authorize/authorize.request.schema.json 66e39d85a503ec2fa096d789b5b3136a451387186fa33424c4bcb07ce9aea49b schemas/v1.1.0/commercial/checkout/checkout.receipt.schema.json @@ -39,4 +39,4 @@ e9b62cf29d5f58fed922e9bc77c8d3e13e6f7ed04785baad7a7e4fc600ab44b8 schemas/v1.1.0 b876f8ffbfd87e5554374de114414f9e4091ba09c80d07b9b99a40ff1befd7c5 schemas/v1.1.0/commercial/ship/ship.request.schema.json 7abc8e8a2dec058298ba5dd0603f20d9f95f6bc411fcd429fdb3c7a116dcbcca schemas/v1.1.0/commercial/verify/verify.receipt.schema.json 09707b90a6317d10d13f6e5339bc17a7ddc4d5938970ff7e25842876b7f2eea5 schemas/v1.1.0/commercial/verify/verify.request.schema.json -1431008b047fc5eb8fe2e0647a7a9d5e27e731ad89b97866c259a5e9937cc549 schemas/v1.1.0/index.json +ef4d9050f892c9d4b86382b9fd3018ee8496e6ad26c2fa9a64cbb44b7d549598 schemas/v1.1.0/index.json diff --git a/manifest.json b/manifest.json index 99a6f4e..4294d8a 100644 --- a/manifest.json +++ b/manifest.json @@ -34,7 +34,7 @@ "actors": "payer/payee/merchant/provider/carrier/verifier", "payment_layers": "payment_requirement, payment_session, payment_proof" }, - "aligns_with": { + "declared_alignment": { "protocol_commons": "1.1.0", "agent_cards": "1.1.0" }, diff --git a/package.json b/package.json index 5ecf023..85122e4 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,9 @@ ], "license": "Apache-2.0", "private": false, + "publishConfig": { + "access": "public" + }, "type": "module", "engines": { "node": ">=20.0.0" @@ -49,7 +52,8 @@ "SECURITY.md", "SECURITY_PROVENANCE.md", "COMPLIANCE.md", - "ONBOARDING.md" + "ONBOARDING.md", + "INTEGRATOR.md" ], "main": "schemas/v1.1.0/index.json", "exports": { @@ -65,6 +69,7 @@ "validate:integrity": "node scripts/validate-integrity.mjs", "validate:all": "npm run validate:schemas && npm run validate:examples && npm run validate:integrity", "validate": "npm run validate:all", + "test": "npm run validate:all", "generate:checksums": "node scripts/generate-checksums.mjs" }, "devDependencies": { diff --git a/schemas/v1.1.0/index.json b/schemas/v1.1.0/index.json index f754c83..59b4707 100644 --- a/schemas/v1.1.0/index.json +++ b/schemas/v1.1.0/index.json @@ -1,10 +1,8 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://commandlayer.org/schemas/v1.1.0/index.json", "name": "@commandlayer/commercial", "version": "1.1.0", "class": "commercial", - "schemas_root": "https://commandlayer.org/schemas/v1.1.0/", + "schemas_root": "schemas/v1.1.0", "verbs": [ { "verb": "authorize", diff --git a/scripts/generate-checksums.mjs b/scripts/generate-checksums.mjs index d6f6c7f..4b3e2fe 100644 --- a/scripts/generate-checksums.mjs +++ b/scripts/generate-checksums.mjs @@ -8,7 +8,6 @@ const OUTPUT_PATH = path.join(ROOT_DIR, "checksums.txt"); const CURRENT_VERSION = "1.1.0"; const TARGETS = [ "manifest.json", - `schemas/v${CURRENT_VERSION}/index.json`, `schemas/v${CURRENT_VERSION}`, `examples/v${CURRENT_VERSION}` ]; diff --git a/scripts/validate-all.mjs b/scripts/validate-all.mjs index db53edc..157449b 100644 --- a/scripts/validate-all.mjs +++ b/scripts/validate-all.mjs @@ -4,6 +4,7 @@ import path from "path"; import Ajv2020 from "ajv/dist/2020.js"; import addFormats from "ajv-formats"; import ajvErrors from "ajv-errors"; +import { loadJsonStrict } from "./load-json-strict.mjs"; const ROOT_DIR = process.cwd(); const CURRENT_VERSION = "1.1.0"; @@ -52,69 +53,6 @@ function assert(condition, message) { if (!condition) throw new Error(message); } -function assertNoDuplicateObjectKeys(source, filePath) { - const objectKeySets = []; - let inString = false; - let escape = false; - - for (let index = 0; index < source.length; index += 1) { - const char = source[index]; - - if (inString) { - if (escape) { - escape = false; - continue; - } - if (char === "\\") { - escape = true; - continue; - } - if (char === '"') inString = false; - continue; - } - - if (char === '"') { - let end = index + 1; - let stringEscape = false; - while (end < source.length) { - const nextChar = source[end]; - if (stringEscape) { - stringEscape = false; - } else if (nextChar === "\\") { - stringEscape = true; - } else if (nextChar === '"') { - break; - } - end += 1; - } - - const raw = source.slice(index, end + 1); - let cursor = end + 1; - while (cursor < source.length && /\s/.test(source[cursor])) cursor += 1; - if (cursor < source.length && source[cursor] === ':' && objectKeySets.length > 0) { - const key = JSON.parse(raw); - const currentKeys = objectKeySets[objectKeySets.length - 1]; - if (currentKeys.has(key)) { - throw new Error(`${filePath} contains a duplicate JSON object key: ${key}`); - } - currentKeys.add(key); - } - - index = end; - continue; - } - - if (char === '{') objectKeySets.push(new Set()); - if (char === '}') objectKeySets.pop(); - } -} - -async function loadJson(filePath) { - const source = await fs.readFile(filePath, "utf8"); - assertNoDuplicateObjectKeys(source, filePath); - return JSON.parse(source); -} - function expectedVerbEntry(verb) { return { verb, @@ -148,12 +86,12 @@ async function loadCurrentSchemas() { return Promise.all(schemaFiles.map(async (file) => ({ file, rel: path.relative(ROOT_DIR, file).replace(/\\/g, "/"), - schema: await loadJson(file) + schema: await loadJsonStrict(file) }))); } async function validateManifest() { - const manifest = await loadJson(path.join(ROOT_DIR, "manifest.json")); + const manifest = await loadJsonStrict(path.join(ROOT_DIR, "manifest.json")); assert(!("$schema" in manifest), "manifest.json must not carry a decorative $schema field"); assert(manifest.version === CURRENT_VERSION, `manifest version must be ${CURRENT_VERSION}`); assert(manifest.status === "current", "manifest status must be current"); @@ -161,14 +99,18 @@ async function validateManifest() { assert(manifest.examples_root === `examples/v${CURRENT_VERSION}`, "manifest examples_root drift"); assert(manifest.current_index === `schemas/v${CURRENT_VERSION}/index.json`, "manifest current_index drift"); assert(manifest.checksums_file === "checksums.txt", "manifest checksums_file drift"); + assert("declared_alignment" in manifest, "manifest must expose declarative alignment metadata"); + assert(!("aligns_with" in manifest), "manifest aligns_with field must not imply verified enforcement"); assert(JSON.stringify(manifest.verbs.map((v) => v.verb)) === JSON.stringify(EXPECTED_VERBS), "manifest verb list drift"); } async function validatePackage() { - const pkg = await loadJson(path.join(ROOT_DIR, "package.json")); + const pkg = await loadJsonStrict(path.join(ROOT_DIR, "package.json")); assert(pkg.version === CURRENT_VERSION, `package version must be ${CURRENT_VERSION}`); assert(pkg.main === `schemas/v${CURRENT_VERSION}/index.json`, "package main drift"); assert(pkg.exports['.'] === `./schemas/v${CURRENT_VERSION}/index.json`, "package exports current entry drift"); + assert(pkg.publishConfig?.access === "public", "package publishConfig.access drift"); + assert(pkg.files.includes("INTEGRATOR.md"), "package files must include INTEGRATOR.md"); } async function validateSchemaTree() { @@ -226,13 +168,13 @@ async function validateSchemaConsistency(currentSchemas) { async function validateIndex() { const indexPath = path.join(SCHEMAS_ROOT, "index.json"); - const indexJson = await loadJson(indexPath); + const indexJson = await loadJsonStrict(indexPath); + assert(!("$schema" in indexJson), "index.json must not carry JSON Schema validator framing"); assert(indexJson.version === CURRENT_VERSION, "index.json version drift"); - assert(indexJson.$id === `https://commandlayer.org/schemas/v${CURRENT_VERSION}/index.json`, "index.json $id drift"); - assert(indexJson.schemas_root === `https://commandlayer.org/schemas/v${CURRENT_VERSION}/`, "index.json schemas_root drift"); + assert(indexJson.schemas_root === `schemas/v${CURRENT_VERSION}`, "index.json schemas_root drift"); assert(JSON.stringify(indexJson.verbs) === JSON.stringify(EXPECTED_VERBS.map(expectedVerbEntry)), "index.json verb inventory drift"); - const manifest = await loadJson(path.join(ROOT_DIR, "manifest.json")); + const manifest = await loadJsonStrict(path.join(ROOT_DIR, "manifest.json")); assert(JSON.stringify(indexJson.verbs) === JSON.stringify(manifest.verbs), "manifest/index verb inventory mismatch"); } diff --git a/scripts/validate-examples.mjs b/scripts/validate-examples.mjs index d05116e..fdce7fe 100644 --- a/scripts/validate-examples.mjs +++ b/scripts/validate-examples.mjs @@ -16,72 +16,9 @@ const ajv = new Ajv2020({ strict: true, allErrors: true, allowUnionTypes: false addFormats(ajv); ajvErrors(ajv); -function assertNoDuplicateObjectKeys(source, filePath) { - const objectKeySets = []; - let inString = false; - let escape = false; - - for (let index = 0; index < source.length; index += 1) { - const char = source[index]; - - if (inString) { - if (escape) { - escape = false; - continue; - } - if (char === "\\") { - escape = true; - continue; - } - if (char === '"') inString = false; - continue; - } - - if (char === '"') { - let end = index + 1; - let stringEscape = false; - while (end < source.length) { - const nextChar = source[end]; - if (stringEscape) { - stringEscape = false; - } else if (nextChar === "\\") { - stringEscape = true; - } else if (nextChar === '"') { - break; - } - end += 1; - } - - const raw = source.slice(index, end + 1); - let cursor = end + 1; - while (cursor < source.length && /\s/.test(source[cursor])) cursor += 1; - if (cursor < source.length && source[cursor] === ':' && objectKeySets.length > 0) { - const key = JSON.parse(raw); - const currentKeys = objectKeySets[objectKeySets.length - 1]; - if (currentKeys.has(key)) { - throw new Error(`${filePath} contains a duplicate JSON object key: ${key}`); - } - currentKeys.add(key); - } - - index = end; - continue; - } - - if (char === '{') objectKeySets.push(new Set()); - if (char === '}') objectKeySets.pop(); - } -} - -async function loadJson(filePath) { - const source = await fs.readFile(filePath, 'utf8'); - assertNoDuplicateObjectKeys(source, filePath); - return JSON.parse(source); -} - async function validateVerb(verb) { - const requestSchema = await loadJson(path.join(SCHEMAS_ROOT, verb, `${verb}.request.schema.json`)); - const receiptSchema = await loadJson(path.join(SCHEMAS_ROOT, verb, `${verb}.receipt.schema.json`)); + const requestSchema = await loadJsonStrict(path.join(SCHEMAS_ROOT, verb, `${verb}.request.schema.json`)); + const receiptSchema = await loadJsonStrict(path.join(SCHEMAS_ROOT, verb, `${verb}.receipt.schema.json`)); const validateRequest = ajv.compile(requestSchema); const validateReceipt = ajv.compile(receiptSchema); @@ -94,7 +31,7 @@ async function validateVerb(verb) { throw new Error(`${verb} ${group} examples must include both request and receipt cases`); } for (const file of files) { - const data = await loadJson(path.join(dir, file)); + const data = await loadJsonStrict(path.join(dir, file)); const validate = file.includes('request') ? validateRequest : validateReceipt; const ok = validate(data); if (group === 'valid' && !ok) throw new Error(`${file} should be valid: ${ajv.errorsText(validate.errors)}`); diff --git a/scripts/validate-integrity.mjs b/scripts/validate-integrity.mjs index 27add19..5e53558 100644 --- a/scripts/validate-integrity.mjs +++ b/scripts/validate-integrity.mjs @@ -8,7 +8,6 @@ const CURRENT_VERSION = "1.1.0"; const CHECKSUMS_PATH = path.join(ROOT_DIR, "checksums.txt"); const TARGETS = [ "manifest.json", - `schemas/v${CURRENT_VERSION}/index.json`, `schemas/v${CURRENT_VERSION}`, `examples/v${CURRENT_VERSION}` ]; diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 3b23de1..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "node", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "noEmit": true - }, - "include": [ - "endpoints/**/*.ts" - ] -}