From beae0aa53c670d96b2aa13c54b5613a034e4d8f7 Mon Sep 17 00:00:00 2001 From: Amey Parle Date: Mon, 30 Mar 2026 16:57:04 -0400 Subject: [PATCH 1/2] Add DID document validation module and fixtures --- validation/README.md | 41 ++ validation/did-document-validator.js | 375 ++++++++++++++++++ .../invalid-agent-missing-parent-org.json | 38 ++ .../invalid-bad-capability-delegation.json | 30 ++ .../fixtures/invalid-bad-id-format.json | 23 ++ .../fixtures/invalid-bad-key-material.json | 23 ++ .../invalid-bad-service-endpoint.json | 30 ++ .../fixtures/invalid-missing-controller.json | 22 + validation/fixtures/invalid-missing-id.json | 22 + .../invalid-missing-verification-method.json | 11 + validation/fixtures/valid-did-document.json | 42 ++ validation/validate.js | 57 +++ 12 files changed, 714 insertions(+) create mode 100644 validation/README.md create mode 100644 validation/did-document-validator.js create mode 100644 validation/fixtures/invalid-agent-missing-parent-org.json create mode 100644 validation/fixtures/invalid-bad-capability-delegation.json create mode 100644 validation/fixtures/invalid-bad-id-format.json create mode 100644 validation/fixtures/invalid-bad-key-material.json create mode 100644 validation/fixtures/invalid-bad-service-endpoint.json create mode 100644 validation/fixtures/invalid-missing-controller.json create mode 100644 validation/fixtures/invalid-missing-id.json create mode 100644 validation/fixtures/invalid-missing-verification-method.json create mode 100644 validation/fixtures/valid-did-document.json create mode 100644 validation/validate.js diff --git a/validation/README.md b/validation/README.md new file mode 100644 index 0000000..b7a0362 --- /dev/null +++ b/validation/README.md @@ -0,0 +1,41 @@ +# TRAIL Protocol - DID Document Validation + +This directory contains a validation module for checking whether a `did:trail` DID Document conforms to the validation rules defined for Issue #6 and the core structures described in the TRAIL DID Method specification. + +## Files + +- `did-document-validator.js` — validation logic +- `validate.js` — CLI runner +- `fixtures/` — valid and invalid example DID Documents + +## What the validator checks + +The validator currently checks: + +- required top-level fields: + - `id` + - `controller` + - `verificationMethod` + - `authentication` +- `id` format for: + - `did:trail:self:` + - `did:trail:org:-<16hex>` + - `did:trail:agent:-<16hex>` +- `verificationMethod` entries: + - required `id`, `type`, `controller` + - valid key material via `publicKeyJwk` or `publicKeyMultibase` +- `authentication` references: + - each entry must point to an existing `verificationMethod.id` +- `service` entries (if present): + - valid `id`, `type`, and `serviceEndpoint` +- `capabilityDelegation` entries (if present): + - each entry must reference an existing verification method or a valid `did:trail` DID / DID URL +- agent-specific validation: + - `trail:parentOrganization` must be present and must be a valid `did:trail:org` DID + +## Run the CLI + +From the repository root: + +```bash +node validation/validate.js validation/fixtures/valid-did-document.json \ No newline at end of file diff --git a/validation/did-document-validator.js b/validation/did-document-validator.js new file mode 100644 index 0000000..bd2e030 --- /dev/null +++ b/validation/did-document-validator.js @@ -0,0 +1,375 @@ +#!/usr/bin/env node + +/** + * TRAIL Protocol - DID Document Validator + * + * Validates whether a did:trail DID Document conforms to the + * issue-defined validation rules and the core structures shown + * in the did:trail method specification. + */ + +const SELF_DID_RE = /^did:trail:self:z[1-9A-HJ-NP-Za-km-z]+$/; +const ORG_DID_RE = /^did:trail:org:[a-z0-9-]+-[a-f0-9]{16}$/; +const AGENT_DID_RE = /^did:trail:agent:[a-z0-9-]+-[a-f0-9]{16}$/; +const MULTIBASE_RE = /^z[1-9A-HJ-NP-Za-km-z]+$/; + + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function isNonEmptyString(value) { + return typeof value === "string" && value.trim() !== ""; +} + +function isValidTrailDid(value) { + return ( + SELF_DID_RE.test(value) || + ORG_DID_RE.test(value) || + AGENT_DID_RE.test(value) + ); +} + +function isValidTrailDidOrDidUrl(value) { + if (!isNonEmptyString(value)) { + return false; + } + + // Accept plain DID + if (isValidTrailDid(value)) { + return true; + } + + // Accept DID URL forms like did:trail:org:...#key-1 + const [baseDid] = value.split("#"); + return isValidTrailDid(baseDid); +} + +function validateRequiredFields(doc) { + const errors = []; + + if (!isObject(doc)) { + return ["DID Document must be a JSON object."]; + } + + const requiredFields = ["id", "controller", "verificationMethod", "authentication"]; + + requiredFields.forEach((field) => { + if (!(field in doc)) { + errors.push(`Missing required top-level field: ${field}`); + } + }); + + if ("id" in doc && !isNonEmptyString(doc.id)) { + errors.push("Field 'id' must be a non-empty string."); + } + + if ("controller" in doc) { + const isString = isNonEmptyString(doc.controller); + const isStringArray = + Array.isArray(doc.controller) && + doc.controller.length > 0 && + doc.controller.every(isNonEmptyString); + + if (!isString && !isStringArray) { + errors.push("Field 'controller' must be a non-empty string or a non-empty array of strings."); + } + } + + if ("verificationMethod" in doc) { + if (!Array.isArray(doc.verificationMethod) || doc.verificationMethod.length === 0) { + errors.push("Field 'verificationMethod' must be a non-empty array."); + } + } + + if ("authentication" in doc) { + if (!Array.isArray(doc.authentication) || doc.authentication.length === 0) { + errors.push("Field 'authentication' must be a non-empty array."); + } + } + + return errors; +} + +function validateDidId(id) { + const errors = []; + + if (!isNonEmptyString(id)) { + return ["Field 'id' must be a non-empty string."]; + } + + if (!isValidTrailDid(id)) { + errors.push( + "Field 'id' must match one of the TRAIL DID formats: did:trail:self:, did:trail:org:-<16hex>, or did:trail:agent:-<16hex>." + ); + } + + return errors; +} + +function validateControllers(controller) { + const errors = []; + + if (isNonEmptyString(controller)) { + if (!isValidTrailDidOrDidUrl(controller)) { + errors.push("Field 'controller' must contain a valid did:trail DID or DID URL."); + } + return errors; + } + + if (Array.isArray(controller)) { + controller.forEach((value, index) => { + if (!isValidTrailDidOrDidUrl(value)) { + errors.push(`controller[${index}] must be a valid did:trail DID or DID URL.`); + } + }); + } + + return errors; +} + +function validatePublicKeyJwk(jwk, path) { + const errors = []; + + if (!isObject(jwk)) { + return [`${path} must be an object.`]; + } + + if (jwk.kty !== "OKP") { + errors.push(`${path}.kty must be "OKP".`); + } + + if (jwk.crv !== "Ed25519") { + errors.push(`${path}.crv must be "Ed25519".`); + } + + if (!isNonEmptyString(jwk.x)) { + errors.push(`${path}.x must be a non-empty string.`); + } + + return errors; +} + +function validateVerificationMethods(verificationMethods) { + const errors = []; + + if (!Array.isArray(verificationMethods)) { + return ["Field 'verificationMethod' must be an array."]; + } + + verificationMethods.forEach((vm, index) => { + const path = `verificationMethod[${index}]`; + + if (!isObject(vm)) { + errors.push(`${path} must be an object.`); + return; + } + + if (!isNonEmptyString(vm.id)) { + errors.push(`${path}.id must be a non-empty string.`); + } else if (!isValidTrailDidOrDidUrl(vm.id)) { + errors.push(`${path}.id must be a valid did:trail DID URL.`); + } + + if (!isNonEmptyString(vm.type)) { + errors.push(`${path}.type must be a non-empty string.`); + } + + if (!isNonEmptyString(vm.controller)) { + errors.push(`${path}.controller must be a non-empty string.`); + } else if (!isValidTrailDidOrDidUrl(vm.controller)) { + errors.push(`${path}.controller must be a valid did:trail DID or DID URL.`); + } + + const hasJwk = Object.prototype.hasOwnProperty.call(vm, "publicKeyJwk"); + const hasMultibase = Object.prototype.hasOwnProperty.call(vm, "publicKeyMultibase"); + + if (!hasJwk && !hasMultibase) { + errors.push(`${path} must include either publicKeyJwk or publicKeyMultibase.`); + } + + if (hasJwk && hasMultibase) { + errors.push(`${path} must not include both publicKeyJwk and publicKeyMultibase.`); + } + + if (hasJwk) { + errors.push(...validatePublicKeyJwk(vm.publicKeyJwk, `${path}.publicKeyJwk`)); + } + + if (hasMultibase) { + if (!isNonEmptyString(vm.publicKeyMultibase) || !MULTIBASE_RE.test(vm.publicKeyMultibase)) { + errors.push(`${path}.publicKeyMultibase must be a valid multibase string starting with 'z'.`); + } + } + }); + + return errors; +} + +function validateAuthentication(authentication, verificationMethods) { + const errors = []; + + if (!Array.isArray(authentication)) { + return ["Field 'authentication' must be an array."]; + } + + const knownVmIds = new Set( + Array.isArray(verificationMethods) + ? verificationMethods + .filter((vm) => isObject(vm) && isNonEmptyString(vm.id)) + .map((vm) => vm.id) + : [] + ); + + authentication.forEach((entry, index) => { + const path = `authentication[${index}]`; + + if (!isNonEmptyString(entry)) { + errors.push(`${path} must be a non-empty string reference.`); + return; + } + + if (!knownVmIds.has(entry)) { + errors.push(`${path} must reference an existing verificationMethod.id.`); + } + }); + + return errors; +} + +function validateServices(services) { + const errors = []; + + if (services === undefined) { + return errors; + } + + if (!Array.isArray(services)) { + return ["Field 'service' must be an array if present."]; + } + + services.forEach((service, index) => { + const path = `service[${index}]`; + + if (!isObject(service)) { + errors.push(`${path} must be an object.`); + return; + } + + if (!isNonEmptyString(service.id)) { + errors.push(`${path}.id must be a non-empty string.`); + } else if (!isValidTrailDidOrDidUrl(service.id)) { + errors.push(`${path}.id must be a valid did:trail DID URL.`); + } + + if (!isNonEmptyString(service.type)) { + errors.push(`${path}.type must be a non-empty string.`); + } + + if (!isNonEmptyString(service.serviceEndpoint)) { + errors.push(`${path}.serviceEndpoint must be a non-empty string.`); + } + }); + + return errors; +} + +function validateCapabilityDelegation(capabilityDelegation, verificationMethods) { + const errors = []; + + if (capabilityDelegation === undefined) { + return errors; + } + + if (!Array.isArray(capabilityDelegation)) { + return ["Field 'capabilityDelegation' must be an array if present."]; + } + + const knownVmIds = new Set( + Array.isArray(verificationMethods) + ? verificationMethods + .filter((vm) => isObject(vm) && isNonEmptyString(vm.id)) + .map((vm) => vm.id) + : [] + ); + + capabilityDelegation.forEach((entry, index) => { + const path = `capabilityDelegation[${index}]`; + + if (!isNonEmptyString(entry)) { + errors.push(`${path} must be a non-empty string reference.`); + return; + } + + const isKnownVm = knownVmIds.has(entry); + const isDidUrl = isValidTrailDidOrDidUrl(entry); + + if (!isKnownVm && !isDidUrl) { + errors.push( + `${path} must reference an existing verificationMethod.id or a valid did:trail DID URL.` + ); + } + }); + + return errors; +} + +function validateDidDocument(doc) { + const errors = []; + + errors.push(...validateRequiredFields(doc)); + + // Stop early if the basic shape is wrong. + if (errors.length > 0) { + return { + valid: false, + errors, + }; + } + + errors.push(...validateDidId(doc.id)); + errors.push(...validateControllers(doc.controller)); + errors.push(...validateVerificationMethods(doc.verificationMethod)); + errors.push(...validateAuthentication(doc.authentication, doc.verificationMethod)); + errors.push(...validateServices(doc.service)); + errors.push(...validateCapabilityDelegation(doc.capabilityDelegation, doc.verificationMethod)); + errors.push(...validateAgentParentOrganization(doc)); + + return { + valid: errors.length === 0, + errors, + }; +} + +function validateAgentParentOrganization(doc) { + const errors = []; + + if (!isNonEmptyString(doc.id) || !AGENT_DID_RE.test(doc.id)) { + return errors; + } + + const parentOrg = doc["trail:parentOrganization"]; + + if (!isNonEmptyString(parentOrg)) { + errors.push("Agent DID Documents must include 'trail:parentOrganization' as a non-empty string."); + return errors; + } + + if (!ORG_DID_RE.test(parentOrg)) { + errors.push("'trail:parentOrganization' must be a valid did:trail:org DID."); + } + + return errors; +} + + +module.exports = { + validateDidDocument, + validateRequiredFields, + validateDidId, + validateVerificationMethods, + validateAuthentication, + validateServices, + validateCapabilityDelegation, + validateAgentParentOrganization +}; \ No newline at end of file diff --git a/validation/fixtures/invalid-agent-missing-parent-org.json b/validation/fixtures/invalid-agent-missing-parent-org.json new file mode 100644 index 0000000..0736f88 --- /dev/null +++ b/validation/fixtures/invalid-agent-missing-parent-org.json @@ -0,0 +1,38 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5", + "controller": [ + "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#recovery-key-1" + ], + "verificationMethod": [ + { + "id": "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + ], + "authentication": [ + "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5#key-1" + ], + "assertionMethod": [ + "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5#key-1" + ], + "service": [ + { + "id": "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5#trail-registry", + "type": "TrailRegistryService", + "serviceEndpoint": "https://registry.trailprotocol.org/1.0/identifiers/did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5" + } + ], + "trail:aiSystemType": "agent", + "trail:trailTrustTier": 1 +} \ No newline at end of file diff --git a/validation/fixtures/invalid-bad-capability-delegation.json b/validation/fixtures/invalid-bad-capability-delegation.json new file mode 100644 index 0000000..7e77bb0 --- /dev/null +++ b/validation/fixtures/invalid-bad-capability-delegation.json @@ -0,0 +1,30 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5", + "controller": [ + "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#recovery-key-1" + ], + "verificationMethod": [ + { + "id": "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + ], + "authentication": [ + "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5#key-1" + ], + "capabilityDelegation": [ + 42 + ], + "trail:parentOrganization": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a" +} \ No newline at end of file diff --git a/validation/fixtures/invalid-bad-id-format.json b/validation/fixtures/invalid-bad-id-format.json new file mode 100644 index 0000000..324f2b9 --- /dev/null +++ b/validation/fixtures/invalid-bad-id-format.json @@ -0,0 +1,23 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:organization:acme-corp", + "controller": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "verificationMethod": [ + { + "id": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + ], + "authentication": [ + "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1" + ] +} \ No newline at end of file diff --git a/validation/fixtures/invalid-bad-key-material.json b/validation/fixtures/invalid-bad-key-material.json new file mode 100644 index 0000000..a20100b --- /dev/null +++ b/validation/fixtures/invalid-bad-key-material.json @@ -0,0 +1,23 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "controller": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "verificationMethod": [ + { + "id": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "publicKeyJwk": { + "kty": "RSA", + "crv": "Ed25519", + "x": "" + } + } + ], + "authentication": [ + "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1" + ] +} \ No newline at end of file diff --git a/validation/fixtures/invalid-bad-service-endpoint.json b/validation/fixtures/invalid-bad-service-endpoint.json new file mode 100644 index 0000000..a44941f --- /dev/null +++ b/validation/fixtures/invalid-bad-service-endpoint.json @@ -0,0 +1,30 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "controller": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "verificationMethod": [ + { + "id": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + ], + "authentication": [ + "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1" + ], + "service": [ + { + "id": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#trail-registry", + "type": "TrailRegistryService", + "serviceEndpoint": 12345 + } + ] +} \ No newline at end of file diff --git a/validation/fixtures/invalid-missing-controller.json b/validation/fixtures/invalid-missing-controller.json new file mode 100644 index 0000000..3dc555f --- /dev/null +++ b/validation/fixtures/invalid-missing-controller.json @@ -0,0 +1,22 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "verificationMethod": [ + { + "id": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + ], + "authentication": [ + "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1" + ] +} \ No newline at end of file diff --git a/validation/fixtures/invalid-missing-id.json b/validation/fixtures/invalid-missing-id.json new file mode 100644 index 0000000..7c542f5 --- /dev/null +++ b/validation/fixtures/invalid-missing-id.json @@ -0,0 +1,22 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "controller": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "verificationMethod": [ + { + "id": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + ], + "authentication": [ + "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1" + ] +} \ No newline at end of file diff --git a/validation/fixtures/invalid-missing-verification-method.json b/validation/fixtures/invalid-missing-verification-method.json new file mode 100644 index 0000000..f5eedbe --- /dev/null +++ b/validation/fixtures/invalid-missing-verification-method.json @@ -0,0 +1,11 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "controller": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "authentication": [ + "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1" + ] +} \ No newline at end of file diff --git a/validation/fixtures/valid-did-document.json b/validation/fixtures/valid-did-document.json new file mode 100644 index 0000000..6d01d07 --- /dev/null +++ b/validation/fixtures/valid-did-document.json @@ -0,0 +1,42 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://trailprotocol.org/ns/did/v1" + ], + "id": "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5", + "controller": [ + "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#recovery-key-1" + ], + "verificationMethod": [ + { + "id": "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5#key-1", + "type": "JsonWebKey2020", + "controller": "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + ], + "authentication": [ + "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5#key-1" + ], + "assertionMethod": [ + "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5#key-1" + ], + "capabilityDelegation": [ + "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5#key-1" + ], + "service": [ + { + "id": "did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5#trail-registry", + "type": "TrailRegistryService", + "serviceEndpoint": "https://registry.trailprotocol.org/1.0/identifiers/did:trail:agent:acme-corp-eu-rfq-assistant-v1-d4e5f6a7b8c3d4e5" + } + ], + "trail:parentOrganization": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", + "trail:aiSystemType": "agent", + "trail:trailTrustTier": 1 +} \ No newline at end of file diff --git a/validation/validate.js b/validation/validate.js new file mode 100644 index 0000000..2a1f4fc --- /dev/null +++ b/validation/validate.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +/** + * TRAIL Protocol - DID Document Validation CLI + * + * Usage: + * node validation/validate.js validation/fixtures/valid-did-document.json + */ + +const fs = require("fs"); +const path = require("path"); +const { validateDidDocument } = require("./did-document-validator"); + +function main() { + const filePath = process.argv[2]; + + if (!filePath) { + console.error("Usage: node validation/validate.js "); + process.exit(1); + } + + const absolutePath = path.resolve(filePath); + + let raw; + try { + raw = fs.readFileSync(absolutePath, "utf8"); + } catch (error) { + console.error(`Failed to read file: ${absolutePath}`); + console.error(error.message); + process.exit(1); + } + + let document; + try { + document = JSON.parse(raw); + } catch (error) { + console.error("Invalid JSON:"); + console.error(error.message); + process.exit(1); + } + + const result = validateDidDocument(document); + + if (result.valid) { + console.log("VALID: DID Document conforms to current TRAIL validation rules."); + process.exit(0); + } + + console.log(`INVALID: ${result.errors.length} validation error(s)`); + result.errors.forEach((error, index) => { + console.log(`${index + 1}. ${error}`); + }); + + process.exit(2); +} + +main(); \ No newline at end of file From 73267f951823c8f2c08009c7c78b6be84fd866d1 Mon Sep 17 00:00:00 2001 From: Amey Parle Date: Wed, 1 Apr 2026 16:43:48 -0400 Subject: [PATCH 2/2] Add automated validation tests --- validation/README.md | 15 ++++- validation/did-document-validator.js | 2 +- .../invalid-agent-missing-parent-org.json | 2 +- .../invalid-bad-capability-delegation.json | 2 +- .../fixtures/invalid-bad-id-format.json | 2 +- .../fixtures/invalid-bad-key-material.json | 2 +- .../invalid-bad-service-endpoint.json | 2 +- .../fixtures/invalid-missing-controller.json | 2 +- validation/fixtures/invalid-missing-id.json | 2 +- .../invalid-missing-verification-method.json | 2 +- validation/fixtures/valid-did-document.json | 2 +- validation/test.js | 63 +++++++++++++++++++ validation/validate.js | 2 +- 13 files changed, 88 insertions(+), 12 deletions(-) create mode 100644 validation/test.js diff --git a/validation/README.md b/validation/README.md index b7a0362..a39e535 100644 --- a/validation/README.md +++ b/validation/README.md @@ -38,4 +38,17 @@ The validator currently checks: From the repository root: ```bash -node validation/validate.js validation/fixtures/valid-did-document.json \ No newline at end of file +node validation/validate.js validation/fixtures/valid-did-document.json + +``` +## Run the automated tests + +From the repository root: + +```bash +node --test validation/test.js +``` + +The test runner loads all fixtures in `validation/fixtures/` and verifies that: +- `valid-did-document.json` passes validation +- all `invalid-*.json` fixtures fail validation diff --git a/validation/did-document-validator.js b/validation/did-document-validator.js index bd2e030..3be7a01 100644 --- a/validation/did-document-validator.js +++ b/validation/did-document-validator.js @@ -372,4 +372,4 @@ module.exports = { validateServices, validateCapabilityDelegation, validateAgentParentOrganization -}; \ No newline at end of file +}; diff --git a/validation/fixtures/invalid-agent-missing-parent-org.json b/validation/fixtures/invalid-agent-missing-parent-org.json index 0736f88..b170190 100644 --- a/validation/fixtures/invalid-agent-missing-parent-org.json +++ b/validation/fixtures/invalid-agent-missing-parent-org.json @@ -35,4 +35,4 @@ ], "trail:aiSystemType": "agent", "trail:trailTrustTier": 1 -} \ No newline at end of file +} diff --git a/validation/fixtures/invalid-bad-capability-delegation.json b/validation/fixtures/invalid-bad-capability-delegation.json index 7e77bb0..f745a60 100644 --- a/validation/fixtures/invalid-bad-capability-delegation.json +++ b/validation/fixtures/invalid-bad-capability-delegation.json @@ -27,4 +27,4 @@ 42 ], "trail:parentOrganization": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a" -} \ No newline at end of file +} diff --git a/validation/fixtures/invalid-bad-id-format.json b/validation/fixtures/invalid-bad-id-format.json index 324f2b9..2f27d6b 100644 --- a/validation/fixtures/invalid-bad-id-format.json +++ b/validation/fixtures/invalid-bad-id-format.json @@ -20,4 +20,4 @@ "authentication": [ "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1" ] -} \ No newline at end of file +} diff --git a/validation/fixtures/invalid-bad-key-material.json b/validation/fixtures/invalid-bad-key-material.json index a20100b..940b4d6 100644 --- a/validation/fixtures/invalid-bad-key-material.json +++ b/validation/fixtures/invalid-bad-key-material.json @@ -20,4 +20,4 @@ "authentication": [ "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1" ] -} \ No newline at end of file +} diff --git a/validation/fixtures/invalid-bad-service-endpoint.json b/validation/fixtures/invalid-bad-service-endpoint.json index a44941f..a7c1420 100644 --- a/validation/fixtures/invalid-bad-service-endpoint.json +++ b/validation/fixtures/invalid-bad-service-endpoint.json @@ -27,4 +27,4 @@ "serviceEndpoint": 12345 } ] -} \ No newline at end of file +} diff --git a/validation/fixtures/invalid-missing-controller.json b/validation/fixtures/invalid-missing-controller.json index 3dc555f..22f3cd3 100644 --- a/validation/fixtures/invalid-missing-controller.json +++ b/validation/fixtures/invalid-missing-controller.json @@ -19,4 +19,4 @@ "authentication": [ "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1" ] -} \ No newline at end of file +} diff --git a/validation/fixtures/invalid-missing-id.json b/validation/fixtures/invalid-missing-id.json index 7c542f5..941c638 100644 --- a/validation/fixtures/invalid-missing-id.json +++ b/validation/fixtures/invalid-missing-id.json @@ -19,4 +19,4 @@ "authentication": [ "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1" ] -} \ No newline at end of file +} diff --git a/validation/fixtures/invalid-missing-verification-method.json b/validation/fixtures/invalid-missing-verification-method.json index f5eedbe..a2d6ddd 100644 --- a/validation/fixtures/invalid-missing-verification-method.json +++ b/validation/fixtures/invalid-missing-verification-method.json @@ -8,4 +8,4 @@ "authentication": [ "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a#key-1" ] -} \ No newline at end of file +} diff --git a/validation/fixtures/valid-did-document.json b/validation/fixtures/valid-did-document.json index 6d01d07..66928fc 100644 --- a/validation/fixtures/valid-did-document.json +++ b/validation/fixtures/valid-did-document.json @@ -39,4 +39,4 @@ "trail:parentOrganization": "did:trail:org:acme-corp-eu-a7f3b2c1e9d04f5a", "trail:aiSystemType": "agent", "trail:trailTrustTier": 1 -} \ No newline at end of file +} diff --git a/validation/test.js b/validation/test.js new file mode 100644 index 0000000..65871e3 --- /dev/null +++ b/validation/test.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +/** + * TRAIL Protocol - DID Document Validator Tests + * + * Runs all fixtures and checks expected validation outcomes. + * + * Usage: + * node --test validation/test.js + */ + +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const { validateDidDocument } = require("./did-document-validator"); + +const fixturesDir = path.join(__dirname, "fixtures"); + +const cases = [ + { file: "valid-did-document.json", valid: true }, + + { file: "invalid-missing-id.json", valid: false }, + { file: "invalid-missing-controller.json", valid: false }, + { file: "invalid-bad-id-format.json", valid: false }, + { file: "invalid-missing-verification-method.json", valid: false }, + { file: "invalid-bad-key-material.json", valid: false }, + { file: "invalid-bad-service-endpoint.json", valid: false }, + { file: "invalid-bad-capability-delegation.json", valid: false }, + { file: "invalid-agent-missing-parent-org.json", valid: false }, +]; + +function loadFixture(fileName) { + const fullPath = path.join(fixturesDir, fileName); + const raw = fs.readFileSync(fullPath, "utf8"); + return JSON.parse(raw); +} + +test("all validation fixtures produce expected results", () => { + for (const testCase of cases) { + const doc = loadFixture(testCase.file); + const result = validateDidDocument(doc); + + assert.equal( + result.valid, + testCase.valid, + `${testCase.file}: expected valid=${testCase.valid}, got valid=${result.valid}. Errors: ${result.errors.join(" | ")}` + ); + + if (testCase.valid) { + assert.equal( + result.errors.length, + 0, + `${testCase.file}: expected no errors, got ${result.errors.length}` + ); + } else { + assert.ok( + result.errors.length > 0, + `${testCase.file}: expected at least one validation error` + ); + } + } +}); diff --git a/validation/validate.js b/validation/validate.js index 2a1f4fc..aa78d7d 100644 --- a/validation/validate.js +++ b/validation/validate.js @@ -54,4 +54,4 @@ function main() { process.exit(2); } -main(); \ No newline at end of file +main();