#! /usr/bin/env node // SPDX-FileCopyrightText: 2026 Kerstin Humm // // SPDX-License-Identifier: AGPL-3.0-or-later import { validate } from "../src/index.js"; import { Annotation, foldAnnotatedJson, isJsonObject, isUrl, JsonValue, } from "../src/types.js"; import { Command, Option } from "commander"; import { readFile } from "fs"; // Synchronously fetch application/activity+json from an URL and validate the returning JSON or abort on error async function fetchUrl(url: string): Promise { console.log(`Fetching ${url}`); return fetch(url, { headers: { Accept: "application/activity+json" }, }).then( (response) => { if (response.ok) { return response.json(); } else { throw new Error(`Returned bad HTTP status code ${response.status}`); } }, ).catch((error) => { console.error("Unhandled fetch error: ", error); process.exit(1); }); } type AccumulatedValidationResult = { violations: number; warnings: number; notes: number; correct: number; }; const initialAccumulatedValidationResult: AccumulatedValidationResult = { violations: 0, warnings: 0, notes: 0, correct: 0, }; function countAnnotations( acc: AccumulatedValidationResult, annotation: Annotation, ): AccumulatedValidationResult { switch (annotation.kind) { case "Violation": acc.violations += 1; break; case "Warning": acc.warnings += 1; break; case "Note": acc.notes += 1; break; case "Correct": acc.correct += 1; break; } return acc; } function validateJson(data: JsonValue, strictness: "Warning" | "Violation") { if (!isJsonObject(data)) { console.error( `The content has to be a JSON object, but it is ${typeof data}`, ); process.exit(1); } const validationResult = validate(data); const acc = foldAnnotatedJson( countAnnotations, initialAccumulatedValidationResult, validationResult, ); // For now we just dump the annotation result and some accumulated metrics. Eventually this will look really pretty, promised :) console.log(JSON.stringify(validationResult, null, 2)); console.log(JSON.stringify(acc, null, 2)); if ( strictness === "Warning" && acc.violations === 0 && acc.warnings === 0 ) { process.exit(0); } else if (strictness === "Violation" && acc.violations === 0) { process.exit(0); } else { process.exit(1); } } async function main() { const program = new Command(); program.version("").argument( "", "Validate a file or URL providing an ActivityStreams Event object. Use `-` for reading from stdin.", ).addOption( new Option( "-s, --strictness ", "On which kind of urgency level to fail", ).choices(["Violation", "Warning"]).default("Violation"), ) .action(async (filenameOrUrl, options) => { if (isUrl(filenameOrUrl)) { const url = filenameOrUrl; const data = await fetchUrl(url); validateJson(data, options.strictness); } else { // We read from stdin if user supplied '-' as filenameOrUrl const filename = process.stdin.fd ? (filenameOrUrl === "-") : filenameOrUrl; readFile(filename, "utf8", (err, content) => { if (err) { console.error(`Can't read file ${filename}`); return; } const data = (() => { try { return JSON.parse(content) as Record; } catch (err) { if (err instanceof SyntaxError) { console.error("Content is not valid JSON:"); console.error(content); process.exit(1); } return {} as never; } })(); validateJson(data, options.strictness); }); } }); program.parse(process.argv); } main().catch((err) => { console.error(err); process.exit(1); });