| // Copyright 2026 will Farrell, and ajv-cmd contributors. | ||
| // SPDX-License-Identifier: MIT | ||
| import { readFile, stat } from "node:fs/promises"; | ||
| export const assertFile = async (filepath) => { | ||
| const stats = await stat(filepath); | ||
| if (!stats.isFile()) { | ||
| throw new Error(`${filepath} is not a file`); | ||
| } | ||
| }; | ||
| export const readJson = async (filepath) => { | ||
| const raw = await readFile(filepath, { encoding: "utf8" }); | ||
| return JSON.parse(raw); | ||
| }; | ||
| export const loadRefSchemas = async (paths) => { | ||
| if (!paths?.length) return undefined; | ||
| return Promise.all(paths.map(readJson)); | ||
| }; |
+55
| "use strict"; | ||
| var __defProp = Object.defineProperty; | ||
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
| var __getOwnPropNames = Object.getOwnPropertyNames; | ||
| var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
| var __export = (target, all) => { | ||
| for (var name in all) | ||
| __defProp(target, name, { get: all[name], enumerable: true }); | ||
| }; | ||
| var __copyProps = (to, from, except, desc) => { | ||
| if (from && typeof from === "object" || typeof from === "function") { | ||
| for (let key of __getOwnPropNames(from)) | ||
| if (!__hasOwnProp.call(to, key) && key !== except) | ||
| __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
| } | ||
| return to; | ||
| }; | ||
| var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
| var deref_exports = {}; | ||
| __export(deref_exports, { | ||
| default: () => deref_default, | ||
| deref: () => deref | ||
| }); | ||
| module.exports = __toCommonJS(deref_exports); | ||
| var import_json_schema_ref_parser = require("@apidevtools/json-schema-ref-parser"); | ||
| const deref = async (schema, options = {}) => { | ||
| if (options.schemas?.length) installFetchCache(options.schemas); | ||
| return (0, import_json_schema_ref_parser.dereference)(schema); | ||
| }; | ||
| var deref_default = deref; | ||
| const installFetchCache = (schemas) => { | ||
| const originalFetch = fetch; | ||
| const cache = {}; | ||
| const enc = new TextEncoder(); | ||
| for (let i = schemas.length; i--; ) { | ||
| const schema = schemas[i]; | ||
| if (schema.$id) { | ||
| cache[schema.$id] = enc.encode(JSON.stringify(schema)); | ||
| } | ||
| } | ||
| globalThis.fetch = async (...args) => { | ||
| if (cache[args[0].href]) { | ||
| return { | ||
| status: 200, | ||
| body: true, | ||
| arrayBuffer: async () => cache[args[0].href] | ||
| }; | ||
| } | ||
| return originalFetch(...args); | ||
| }; | ||
| }; | ||
| // Annotate the CommonJS export names for ESM import in node: | ||
| 0 && (module.exports = { | ||
| deref | ||
| }); |
+34
| // Copyright 2026 will Farrell, and ajv-cmd contributors. | ||
| // SPDX-License-Identifier: MIT | ||
| import { dereference } from "@apidevtools/json-schema-ref-parser"; | ||
| export const deref = async (schema, options = {}) => { | ||
| if (options.schemas?.length) installFetchCache(options.schemas); | ||
| return dereference(schema); | ||
| }; | ||
| export default deref; | ||
| const installFetchCache = (schemas) => { | ||
| const originalFetch = fetch; | ||
| const cache = {}; | ||
| const enc = new TextEncoder(); | ||
| for (let i = schemas.length; i--; ) { | ||
| const schema = schemas[i]; | ||
| if (schema.$id) { | ||
| cache[schema.$id] = enc.encode(JSON.stringify(schema)); | ||
| } | ||
| } | ||
| globalThis.fetch = async (...args) => { | ||
| if (cache[args[0].href]) { | ||
| return { | ||
| status: 200, | ||
| body: true, | ||
| arrayBuffer: async () => cache[args[0].href], | ||
| }; | ||
| } | ||
| return originalFetch(...args); | ||
| }; | ||
| }; |
+29
-7
| #!/usr/bin/env -S node --disable-warning=DEP0040 | ||
| // Copyright 2026 will Farrell, and ajv-cmd contributors. | ||
| // SPDX-License-Identifier: MIT | ||
| // --disable-warning=DEP0040 [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. | ||
| // #!/usr/bin/env -S node --experimental-json-modules --no-warnings --no-deprecation | ||
| // --disable-warning=DEP0040 suppresses: [DEP0040] DeprecationWarning: The `punycode` module is deprecated. | ||
| import { createRequire } from "node:module"; | ||
| import { Command, Option } from "commander"; | ||
@@ -14,9 +14,9 @@ import deref from "./commands/deref.js"; | ||
| //import metadata from './package.json' assert { type: 'json' } | ||
| const { version } = createRequire(import.meta.url)("./package.json"); | ||
| const program = new Command() | ||
| .name("ajv") | ||
| //.version(metadata.version) | ||
| .version(version) | ||
| .description( | ||
| "Transpile JSON-Schema (.json) files to JavaScript (.js or .mjs) using ajv", | ||
| "Validate, transpile, dereference, and audit JSON-Schema files using AJV", | ||
| ); | ||
@@ -75,4 +75,2 @@ | ||
| .argument("<input>", "Path to the JSON-Schema file to transpile") | ||
| //.addOption(new Option('--ftl <ftl>', 'Path to ftl file') | ||
| // Docs: https://ajv.js.org/packages/ajv-cli.html | ||
@@ -172,2 +170,26 @@ .addOption( | ||
| new Option( | ||
| "--ignore <ignore...>", | ||
| "Suppress errors by `instancePath` or `instancePath:keyword` (exact match).", | ||
| ), | ||
| ) | ||
| .addOption( | ||
| new Option( | ||
| "--offline", | ||
| "Skip DNS lookups for remote $ref URLs (disables SSRF resolution).", | ||
| ).preset(true), | ||
| ) | ||
| .addOption( | ||
| new Option( | ||
| "--dns-timeout-ms <dnsTimeoutMs>", | ||
| "Per-hostname DNS lookup timeout in ms for SSRF checks (default 5000).", | ||
| ), | ||
| ) | ||
| .addOption( | ||
| new Option( | ||
| "--dns-concurrency <dnsConcurrency>", | ||
| "Max concurrent DNS lookups for SSRF checks (default 10).", | ||
| ), | ||
| ) | ||
| .addOption( | ||
| new Option( | ||
| "-o, --output <output>", | ||
@@ -174,0 +196,0 @@ "Path to store the resulting JSON issues file.", |
+8
-47
| // Copyright 2026 will Farrell, and ajv-cmd contributors. | ||
| // SPDX-License-Identifier: MIT | ||
| import { readFile, stat, writeFile } from "node:fs/promises"; | ||
| import { dereference } from "@apidevtools/json-schema-ref-parser"; | ||
| import { writeFile } from "node:fs/promises"; | ||
| import deref from "../deref.js"; | ||
| import { assertFile, loadRefSchemas, readJson } from "./_utils.js"; | ||
| const fileExists = async (filepath) => { | ||
| const stats = await stat(filepath); | ||
| if (!stats.isFile()) { | ||
| throw new Error(`${filepath} is not a file`); | ||
| } | ||
| }; | ||
| export default async (input, options) => { | ||
| await fileExists(input); | ||
| const jsonSchema = await readFile(input, { encoding: "utf8" }).then((res) => | ||
| JSON.parse(res), | ||
| ); | ||
| await assertFile(input); | ||
| const jsonSchema = await readJson(input); | ||
| if (options?.refSchemaFiles) { | ||
| const refSchemas = []; | ||
| for (const schemaFilePath of options.refSchemaFiles) { | ||
| const refSchemaFile = await readFile(schemaFilePath, { | ||
| encoding: "utf8", | ||
| }).then((res) => JSON.parse(res)); | ||
| refSchemas.push(refSchemaFile); | ||
| } | ||
| mockFetch(refSchemas); | ||
| options.schemas = await loadRefSchemas(options.refSchemaFiles); | ||
| } | ||
| const json = await dereference(jsonSchema); | ||
| const json = await deref(jsonSchema, options); | ||
@@ -40,26 +26,1 @@ if (typeof options.output === "string") { | ||
| }; | ||
| const mockFetch = (schemas) => { | ||
| const _fetch = fetch; | ||
| const cache = {}; | ||
| const enc = new TextEncoder(); | ||
| for (let i = schemas.length; i--; ) { | ||
| const schema = schemas[i]; | ||
| if (schema.$id) { | ||
| cache[schema.$id] = enc.encode(JSON.stringify(schema)); | ||
| } | ||
| } | ||
| globalThis.fetch = async (...args) => { | ||
| if (cache[args[0].href]) { | ||
| return Promise.resolve({ | ||
| status: 200, | ||
| body: true, | ||
| arrayBuffer: async () => cache[args[0].href], | ||
| }); | ||
| } | ||
| return _fetch(...args); | ||
| }; | ||
| }; |
+4
-12
| // Copyright 2026 will Farrell, and ajv-cmd contributors. | ||
| // SPDX-License-Identifier: MIT | ||
| import { readFile, stat, writeFile } from "node:fs/promises"; | ||
| import { readFile, writeFile } from "node:fs/promises"; | ||
| import transpile from "../ftl.js"; | ||
| import { assertFile } from "./_utils.js"; | ||
| const fileExists = async (filepath) => { | ||
| const stats = await stat(filepath); | ||
| if (!stats.isFile()) { | ||
| throw new Error(`${filepath} is not a file`); | ||
| } | ||
| }; | ||
| export default async (input, options) => { | ||
| await assertFile(input); | ||
| const ftl = async (input, options) => { | ||
| await fileExists(input); | ||
| const ftl = await readFile(input, { encoding: "utf8" }); | ||
@@ -26,3 +20,1 @@ | ||
| }; | ||
| export default ftl; |
+8
-322
| // Copyright 2026 will Farrell, and ajv-cmd contributors. | ||
| // SPDX-License-Identifier: MIT | ||
| import { lookup } from "node:dns/promises"; | ||
| import { readFile, stat, writeFile } from "node:fs/promises"; | ||
| import { isSafePattern } from "redos-detector"; | ||
| import sast from "../sast.js"; | ||
| import { writeFile } from "node:fs/promises"; | ||
| import { analyze } from "../sast.js"; | ||
| import { assertFile, loadRefSchemas, readJson } from "./_utils.js"; | ||
| const fileExists = async (filepath) => { | ||
| const stats = await stat(filepath); | ||
| if (!stats.isFile()) { | ||
| throw new Error(`${filepath} is not a file`); | ||
| } | ||
| }; | ||
| export default async (input, options) => { | ||
| await fileExists(input); | ||
| await assertFile(input); | ||
| const jsonSchema = await readFile(input, { encoding: "utf8" }).then((res) => | ||
| JSON.parse(res), | ||
| ); | ||
| const jsonSchema = await readJson(input); | ||
| if (options?.refSchemaFiles) { | ||
| const refSchemas = []; | ||
| for (const schemaFilePath of options.refSchemaFiles) { | ||
| const refSchemaFile = await readFile(schemaFilePath, { | ||
| encoding: "utf8", | ||
| }).then((res) => JSON.parse(res)); | ||
| refSchemas.push(refSchemaFile); | ||
| } | ||
| options.schemas = refSchemas; | ||
| options.schemas = await loadRefSchemas(options.refSchemaFiles); | ||
| } | ||
| const maxDepth = | ||
| options.overrideMaxDepth != null | ||
| ? Number(options.overrideMaxDepth) | ||
| : MAX_DEPTH; | ||
| const errors = await analyze(jsonSchema, options); | ||
| // Single crawl: depth, min/max, patterns, $ref collection | ||
| const crawl = crawlSchema(jsonSchema, maxDepth); | ||
| let errors = []; | ||
| if (crawl.depthExceeded) { | ||
| errors.push({ | ||
| instancePath: "", | ||
| schemaPath: "#/depth", | ||
| keyword: "depth", | ||
| params: { depth: crawl.depth, limit: maxDepth }, | ||
| message: `must NOT have depth greater than ${maxDepth}`, | ||
| }); | ||
| } else { | ||
| // Only run meta-schema validation if depth is safe | ||
| const validate = sast(jsonSchema, options); | ||
| validate(jsonSchema, options); | ||
| if (validate.errors) errors.push(...validate.errors); | ||
| // Append crawl findings after validate errors | ||
| errors.push(...crawl.errors); | ||
| const ssrfErrors = await resolveSSRFRefs(crawl.refs); | ||
| errors.push(...ssrfErrors); | ||
| if (options.overrideMaxItems != null && errors.length) { | ||
| const limit = Number(options.overrideMaxItems); | ||
| errors = errors.filter((err) => { | ||
| if (err.schemaPath === "#/definitions/safeArrayItemsLimits/maxItems") { | ||
| const arr = resolveInstancePath(jsonSchema, err.instancePath); | ||
| return !Array.isArray(arr) || arr.length > limit; | ||
| } | ||
| return true; | ||
| }); | ||
| } | ||
| if (options.overrideMaxProperties != null && errors.length) { | ||
| const limit = Number(options.overrideMaxProperties); | ||
| errors = errors.filter((err) => { | ||
| if ( | ||
| err.schemaPath === | ||
| "#/definitions/safeObjectPropertiesLimits/maxProperties" | ||
| ) { | ||
| const obj = resolveInstancePath(jsonSchema, err.instancePath); | ||
| if (typeof obj !== "object" || obj === null) return true; | ||
| return Object.keys(obj).length > limit; | ||
| } | ||
| return true; | ||
| }); | ||
| } | ||
| } | ||
| if (errors.length) { | ||
@@ -95,3 +24,3 @@ if (typeof options.output === "string") { | ||
| } else { | ||
| console.log(input, "has issues", stringify(errors)); | ||
| console.log(input, "has issues", JSON.stringify(errors, null, 2)); | ||
| } | ||
@@ -105,244 +34,1 @@ if (options.fail) { | ||
| }; | ||
| const MAX_DEPTH = 32; | ||
| const crawlSchema = (obj, maxDepth) => { | ||
| const result = { depth: 0, depthExceeded: false, errors: [], refs: [] }; | ||
| if (typeof obj !== "object" || obj === null) return result; | ||
| result.depth = 1; | ||
| const stack = [[obj, "", 1]]; | ||
| while (stack.length > 0) { | ||
| const [current, path, currentDepth] = stack.pop(); | ||
| // minLength / maxLength | ||
| if ( | ||
| Object.hasOwn(current, "minLength") && | ||
| Object.hasOwn(current, "maxLength") && | ||
| current.minLength > current.maxLength | ||
| ) { | ||
| result.errors.push({ | ||
| instancePath: path, | ||
| schemaPath: "#/minLength", | ||
| keyword: "minLength", | ||
| params: { | ||
| minLength: current.minLength, | ||
| maxLength: current.maxLength, | ||
| }, | ||
| message: "minLength must be less than or equal to maxLength", | ||
| }); | ||
| } | ||
| // minimum / exclusiveMinimum / maximum / exclusiveMaximum | ||
| { | ||
| const hasMin = Object.hasOwn(current, "minimum"); | ||
| const hasExMin = Object.hasOwn(current, "exclusiveMinimum"); | ||
| const hasMax = Object.hasOwn(current, "maximum"); | ||
| const hasExMax = Object.hasOwn(current, "exclusiveMaximum"); | ||
| if ((hasMin || hasExMin) && (hasMax || hasExMax)) { | ||
| const effectiveMin = | ||
| hasMin && hasExMin | ||
| ? Math.max(current.minimum, current.exclusiveMinimum) | ||
| : hasMin | ||
| ? current.minimum | ||
| : current.exclusiveMinimum; | ||
| const effectiveMax = | ||
| hasMax && hasExMax | ||
| ? Math.min(current.maximum, current.exclusiveMaximum) | ||
| : hasMax | ||
| ? current.maximum | ||
| : current.exclusiveMaximum; | ||
| if (!(effectiveMin < effectiveMax)) { | ||
| result.errors.push({ | ||
| instancePath: path, | ||
| schemaPath: "#/minimum", | ||
| keyword: "minimum", | ||
| params: { | ||
| ...(hasMin && { minimum: current.minimum }), | ||
| ...(hasExMin && { | ||
| exclusiveMinimum: current.exclusiveMinimum, | ||
| }), | ||
| ...(hasMax && { maximum: current.maximum }), | ||
| ...(hasExMax && { | ||
| exclusiveMaximum: current.exclusiveMaximum, | ||
| }), | ||
| }, | ||
| message: "minimum must be less than maximum", | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| // minItems / maxItems | ||
| if ( | ||
| Object.hasOwn(current, "minItems") && | ||
| Object.hasOwn(current, "maxItems") && | ||
| current.minItems > current.maxItems | ||
| ) { | ||
| result.errors.push({ | ||
| instancePath: path, | ||
| schemaPath: "#/minItems", | ||
| keyword: "minItems", | ||
| params: { | ||
| minItems: current.minItems, | ||
| maxItems: current.maxItems, | ||
| }, | ||
| message: "minItems must be less than or equal to maxItems", | ||
| }); | ||
| } | ||
| // minContains / maxContains | ||
| if ( | ||
| Object.hasOwn(current, "minContains") && | ||
| Object.hasOwn(current, "maxContains") && | ||
| current.minContains > current.maxContains | ||
| ) { | ||
| result.errors.push({ | ||
| instancePath: path, | ||
| schemaPath: "#/minContains", | ||
| keyword: "minContains", | ||
| params: { | ||
| minContains: current.minContains, | ||
| maxContains: current.maxContains, | ||
| }, | ||
| message: "minContains must be less than or equal to maxContains", | ||
| }); | ||
| } | ||
| // ReDoS pattern check | ||
| if ( | ||
| Object.hasOwn(current, "pattern") && | ||
| typeof current.pattern === "string" | ||
| ) { | ||
| const patternResult = isSafePattern(current.pattern); | ||
| if (!patternResult.safe) { | ||
| result.errors.push({ | ||
| instancePath: `${path}/pattern`, | ||
| schemaPath: "#/redos", | ||
| keyword: "pattern", | ||
| params: { pattern: current.pattern }, | ||
| message: "pattern is vulnerable to ReDoS", | ||
| }); | ||
| } | ||
| } | ||
| // Collect remote $ref URLs for DNS resolution | ||
| if ( | ||
| Object.hasOwn(current, "$ref") && | ||
| typeof current.$ref === "string" && | ||
| !current.$ref.startsWith("#") | ||
| ) { | ||
| try { | ||
| const url = new URL(current.$ref); | ||
| result.refs.push({ | ||
| hostname: url.hostname, | ||
| ref: current.$ref, | ||
| path: `${path}/$ref`, | ||
| }); | ||
| } catch { | ||
| // not a valid URL, skip | ||
| } | ||
| } | ||
| // Traverse children, tracking depth | ||
| for (const key in current) { | ||
| if (Object.hasOwn(current, key)) { | ||
| const value = current[key]; | ||
| if (typeof value === "object" && value !== null) { | ||
| const newDepth = currentDepth + 1; | ||
| if (newDepth > result.depth) result.depth = newDepth; | ||
| if (result.depth > maxDepth) { | ||
| result.depthExceeded = true; | ||
| return result; | ||
| } | ||
| stack.push([value, `${path}/${key}`, newDepth]); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| }; | ||
| const isPrivateIP = (ip) => { | ||
| const parts = ip.split(".").map(Number); | ||
| if (parts.length === 4 && parts.every((p) => p >= 0 && p <= 255)) { | ||
| if (parts[0] === 10) return true; // 10.0.0.0/8 | ||
| if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; // 172.16.0.0/12 | ||
| if (parts[0] === 192 && parts[1] === 168) return true; // 192.168.0.0/16 | ||
| if (parts[0] === 127) return true; // 127.0.0.0/8 | ||
| if (parts[0] === 169 && parts[1] === 254) return true; // 169.254.0.0/16 | ||
| if (parts[0] === 0) return true; // 0.0.0.0/8 | ||
| if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) return true; // 100.64.0.0/10 (CGN) | ||
| if (parts[0] === 198 && parts[1] >= 18 && parts[1] <= 19) return true; // 198.18.0.0/15 | ||
| } | ||
| // IPv6 private/reserved | ||
| const lower = ip.toLowerCase(); | ||
| if (lower === "::1" || lower === "::") return true; | ||
| if (lower.startsWith("fc") || lower.startsWith("fd")) return true; // unique local | ||
| if (lower.startsWith("fe80")) return true; // link-local | ||
| if (lower.startsWith("::ffff:")) { | ||
| return isPrivateIP(lower.slice(7)); // IPv4-mapped IPv6 | ||
| } | ||
| return false; | ||
| }; | ||
| const resolveSSRFRefs = async (refs) => { | ||
| const errors = []; | ||
| const hostnameMap = new Map(); | ||
| for (const entry of refs) { | ||
| if (!hostnameMap.has(entry.hostname)) { | ||
| hostnameMap.set(entry.hostname, []); | ||
| } | ||
| hostnameMap.get(entry.hostname).push(entry); | ||
| } | ||
| for (const [hostname, entries] of hostnameMap) { | ||
| try { | ||
| const { address } = await lookup(hostname); | ||
| if (isPrivateIP(address)) { | ||
| for (const { ref, path } of entries) { | ||
| errors.push({ | ||
| instancePath: path, | ||
| schemaPath: "#/ssrf", | ||
| keyword: "ssrf", | ||
| params: { ref, hostname, resolvedIP: address }, | ||
| message: `$ref hostname "${hostname}" resolves to private IP ${address}`, | ||
| }); | ||
| } | ||
| } | ||
| } catch { | ||
| for (const { ref, path } of entries) { | ||
| errors.push({ | ||
| instancePath: path, | ||
| schemaPath: "#/ssrf", | ||
| keyword: "ssrf", | ||
| params: { ref, hostname }, | ||
| message: `$ref hostname "${hostname}" does not resolve`, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return errors; | ||
| }; | ||
| const resolveInstancePath = (obj, pointer) => { | ||
| if (typeof obj !== "object" || obj === null) return undefined; | ||
| if (!pointer) return obj; | ||
| const parts = pointer.split("/").slice(1); | ||
| let current = obj; | ||
| for (const part of parts) { | ||
| if (typeof current !== "object" || current === null) return undefined; | ||
| if (!Object.hasOwn(current, part)) return undefined; | ||
| current = current[part]; | ||
| } | ||
| return current; | ||
| }; | ||
| const stringify = (arr) => { | ||
| let str = "[\n"; | ||
| for (let i = 0, l = arr.length; i < l; i++) { | ||
| str += `${JSON.stringify(arr[i]) + (i < l - 1 ? "," : "")}\n`; | ||
| } | ||
| return `${str}]`; | ||
| }; |
| // Copyright 2026 will Farrell, and ajv-cmd contributors. | ||
| // SPDX-License-Identifier: MIT | ||
| import { readFile, stat, writeFile } from "node:fs/promises"; | ||
| import { writeFile } from "node:fs/promises"; | ||
| import transpile from "../transpile.js"; | ||
| import { assertFile, loadRefSchemas, readJson } from "./_utils.js"; | ||
| const fileExists = async (filepath) => { | ||
| const stats = await stat(filepath); | ||
| if (!stats.isFile()) { | ||
| throw new Error(`${filepath} is not a file`); | ||
| } | ||
| }; | ||
| export default async (input, options) => { | ||
| await fileExists(input); | ||
| await assertFile(input); | ||
| const jsonSchema = await readFile(input, { encoding: "utf8" }).then((res) => | ||
| JSON.parse(res), | ||
| ); | ||
| const jsonSchema = await readJson(input); | ||
| if (options?.refSchemaFiles) { | ||
| const refSchemas = []; | ||
| for (const schemaFilePath of options.refSchemaFiles) { | ||
| const refSchemaFile = await readFile(schemaFilePath, { | ||
| encoding: "utf8", | ||
| }).then((res) => JSON.parse(res)); | ||
| refSchemas.push(refSchemaFile); | ||
| } | ||
| options.schemas = refSchemas; | ||
| options.schemas = await loadRefSchemas(options.refSchemaFiles); | ||
| } | ||
@@ -30,0 +15,0 @@ |
+5
-28
| // Copyright 2026 will Farrell, and ajv-cmd contributors. | ||
| // SPDX-License-Identifier: MIT | ||
| import { readFile, stat } from "node:fs/promises"; | ||
| import validate from "../validate.js"; | ||
| import { assertFile, loadRefSchemas, readJson } from "./_utils.js"; | ||
| const fileExists = async (filepath) => { | ||
| const stats = await stat(filepath); | ||
| if (!stats.isFile()) { | ||
| throw new Error(`${filepath} is not a file`); | ||
| } | ||
| }; | ||
| export default async (input, options) => { | ||
| await fileExists(input); | ||
| await assertFile(input); | ||
| const jsonSchema = await readFile(input, { encoding: "utf8" }).then((res) => | ||
| JSON.parse(res), | ||
| ); | ||
| const jsonSchema = await readJson(input); | ||
| if (options?.refSchemaFiles) { | ||
| const refSchemas = []; | ||
| for (const schemaFilePath of options.refSchemaFiles) { | ||
| const refSchemaFile = await readFile(schemaFilePath, { | ||
| encoding: "utf8", | ||
| }).then((res) => JSON.parse(res)); | ||
| refSchemas.push(refSchemaFile); | ||
| } | ||
| options.schemas = refSchemas; | ||
| options.schemas = await loadRefSchemas(options.refSchemaFiles); | ||
| } | ||
| if (options?.testDataFiles) { | ||
| const testDataFiles = []; | ||
| for (const testDataFilePath of options.testDataFiles) { | ||
| const testDataFile = await readFile(testDataFilePath, { | ||
| encoding: "utf8", | ||
| }).then((res) => JSON.parse(res)); | ||
| testDataFiles.push(testDataFile); | ||
| } | ||
| options.testData = testDataFiles; | ||
| options.testData = await loadRefSchemas(options.testDataFiles); | ||
| } | ||
@@ -41,0 +18,0 @@ |
+5
-0
@@ -33,2 +33,3 @@ "use strict"; | ||
| default: () => index_default, | ||
| deref: () => deref, | ||
| ftl: () => ftl, | ||
@@ -41,2 +42,3 @@ instance: () => instance, | ||
| var import_compile = require("./compile.js"); | ||
| var import_deref = __toESM(require("./deref.js"), 1); | ||
| var import_ftl = __toESM(require("./ftl.js"), 1); | ||
@@ -47,2 +49,3 @@ var import_transpile = __toESM(require("./transpile.js"), 1); | ||
| const compile = import_compile.compile; | ||
| const deref = import_deref.default; | ||
| const transpile = import_transpile.default; | ||
@@ -54,2 +57,3 @@ const validate = import_validate.default; | ||
| compile, | ||
| deref, | ||
| ftl, | ||
@@ -62,2 +66,3 @@ transpile, | ||
| compile, | ||
| deref, | ||
| ftl, | ||
@@ -64,0 +69,0 @@ instance, |
+3
-0
@@ -7,2 +7,3 @@ // Copyright 2026 will Farrell, and ajv-cmd contributors. | ||
| } from "./compile.js"; | ||
| import derefImport from "./deref.js"; | ||
| import ftlImport from "./ftl.js"; | ||
@@ -14,2 +15,3 @@ import transpileImport from "./transpile.js"; | ||
| export const compile = compileImport; | ||
| export const deref = derefImport; | ||
| export const transpile = transpileImport; | ||
@@ -22,2 +24,3 @@ export const validate = validateImport; | ||
| compile, | ||
| deref, | ||
| ftl, | ||
@@ -24,0 +27,0 @@ transpile, |
+1
-1
| MIT License | ||
| Copyright (c) 2022 will Farrell | ||
| Copyright (c) 2026 will Farrell | ||
@@ -5,0 +5,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy |
+16
-16
| { | ||
| "name": "ajv-cmd", | ||
| "version": "0.11.0", | ||
| "version": "0.12.0", | ||
| "description": "Deref, Validate, Transpile, and Test JSON-Schema (.json) files using ajv", | ||
@@ -21,2 +21,4 @@ "type": "module", | ||
| "compile.js", | ||
| "deref.cjs", | ||
| "deref.js", | ||
| "ftl.cjs", | ||
@@ -28,3 +30,2 @@ "ftl.js", | ||
| "validate.js", | ||
| "sast.cjs", | ||
| "sast.js" | ||
@@ -69,5 +70,2 @@ ], | ||
| "default": "./sast.js" | ||
| }, | ||
| "require": { | ||
| "default": "./sast.cjs" | ||
| } | ||
@@ -93,15 +91,17 @@ }, | ||
| "scripts": { | ||
| "prepare": "husky", | ||
| "prepare": "husky || true", | ||
| "prepublishOnly": "npm run build", | ||
| "build": "npm run build:index && npm run build:compile && npm run build:ftl && npm run build:transpile && npm run build:validate && npm run build:sast", | ||
| "build": "npm run build:index && npm run build:compile && npm run build:deref && npm run build:ftl && npm run build:transpile && npm run build:validate", | ||
| "build:index": "./node_modules/.bin/esbuild index.js --platform=node --format=cjs --target=node18 --allow-overwrite --outfile=index.cjs", | ||
| "build:compile": "./node_modules/.bin/esbuild compile.js --platform=node --format=cjs --target=node18 --allow-overwrite --outfile=compile.cjs", | ||
| "build:deref": "./node_modules/.bin/esbuild deref.js --platform=node --format=cjs --target=node18 --allow-overwrite --outfile=deref.cjs", | ||
| "build:ftl": "./node_modules/.bin/esbuild ftl.js --platform=node --format=cjs --target=node18 --bundle --allow-overwrite --outfile=ftl.cjs", | ||
| "build:transpile": "./node_modules/.bin/esbuild transpile.js --platform=node --format=cjs --target=node18 --allow-overwrite --outfile=transpile.cjs", | ||
| "build:validate": "./node_modules/.bin/esbuild validate.js --platform=node --format=cjs --target=node18 --allow-overwrite --outfile=validate.cjs", | ||
| "build:sast": "./node_modules/.bin/esbuild sast.js --platform=node --format=cjs --target=node18 --allow-overwrite --outfile=sast.cjs", | ||
| "test": "npm run test:lint && npm run test:unit && npm run test:types && npm run test:sast && npm run test:perf && npm run test:dast", | ||
| "pretest": "npm run build", | ||
| "test": "npm run test:lint && npm run test:unit && npm run test:e2e && npm run test:types && npm run test:sast && npm run test:perf && npm run test:dast", | ||
| "test:unit": "node --test --experimental-test-coverage --test-coverage-lines=95 --test-coverage-branches=90 --test-coverage-functions=90 ./*.test.js ./commands/*.test.js", | ||
| "test:e2e": "./__test__/e2e.sh", | ||
| "test:sast": "npm run test:sast:license && npm run test:sast:lockfile && npm run test:sast:semgrep && npm run test:sast:trufflehog && npm run test:sast:trivy", | ||
| "test:sast:license": "license-check-and-add check -f license.json", | ||
| "test:sast:license": "license-check-and-add check -f .license.config.json", | ||
| "test:sast:lockfile": "lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https", | ||
@@ -118,9 +118,10 @@ "test:sast:semgrep": "semgrep scan --config auto", | ||
| "git:commit-msg": "commitlint --config commitlint.config.cjs --edit", | ||
| "git:lint-staged": "npm run test:lint", | ||
| "git:lint-staged": "npm run test:lint:staged", | ||
| "git:unit-staged": "node --test", | ||
| "git:test-staged": "npm run git:unit-staged", | ||
| "lint": "biome check --write --no-errors-on-unmatched", | ||
| "test:lint": "biome check --staged --no-errors-on-unmatched", | ||
| "release:license:add": "license-check-and-add add -f license.json", | ||
| "release:license:remove": "license-check-and-add remove -f license.json" | ||
| "test:lint": "biome ci --no-errors-on-unmatched", | ||
| "test:lint:staged": "biome check --staged --no-errors-on-unmatched", | ||
| "release:license:add": "license-check-and-add add -f .license.config.json", | ||
| "release:license:remove": "license-check-and-add remove -f .license.config.json" | ||
| }, | ||
@@ -156,4 +157,3 @@ "repository": { | ||
| "esbuild": "^0.28.0", | ||
| "redos-detector": "6.1.4", | ||
| "sast-json-schema": "^0.1.0" | ||
| "sast-json-schema": "0.2.3" | ||
| }, | ||
@@ -160,0 +160,0 @@ "devDependencies": { |
+9
-23
| // Copyright 2026 will Farrell, and ajv-cmd contributors. | ||
| // SPDX-License-Identifier: MIT | ||
| import { createRequire } from "node:module"; | ||
| import Ajv from "ajv/dist/2020.js"; | ||
| //import sastSchema from 'sast-json-schema/index.json' with { type: 'json' } | ||
| const defaultOptions = { | ||
| strictTypes: false, | ||
| allErrors: true, | ||
| }; | ||
| const sastSchema = createRequire(import.meta.url)( | ||
| "sast-json-schema/index.json", | ||
| ); | ||
| export const sast = (_schema, options = {}) => { | ||
| options = { ...defaultOptions, ...options }; | ||
| const ajv = new Ajv(options); | ||
| const validate = ajv.compile(sastSchema); | ||
| return validate; | ||
| }; | ||
| export default sast; | ||
| export { | ||
| analyze, | ||
| crawlSchema, | ||
| isPrivateIP, | ||
| MAX_DEPTH, | ||
| resolveSSRFRefs, | ||
| sast, | ||
| sast as default, | ||
| } from "sast-json-schema/cli"; |
+20
-16
@@ -93,19 +93,23 @@ "use strict"; | ||
| const cleanupFiles = [file]; | ||
| if (needsBridge) { | ||
| await (0, import_promises.writeFile)(bridgeFile, bridgeModuleContent, "utf8"); | ||
| cleanupFiles.push(bridgeFile); | ||
| if (needsBridge) cleanupFiles.push(bridgeFile); | ||
| try { | ||
| if (needsBridge) { | ||
| await (0, import_promises.writeFile)(bridgeFile, bridgeModuleContent, "utf8"); | ||
| } | ||
| await (0, import_promises.writeFile)(file, js, "utf8"); | ||
| await (0, import_esbuild.build)({ | ||
| entryPoints: [file], | ||
| platform: "node", | ||
| format: "esm", | ||
| bundle: true, | ||
| minify: true, | ||
| legalComments: "none", | ||
| allowOverwrite: true, | ||
| outfile: file | ||
| }); | ||
| js = await (0, import_promises.readFile)(file, { encoding: "utf8" }); | ||
| } finally { | ||
| await Promise.all(cleanupFiles.map((f) => (0, import_promises.unlink)(f).catch(() => { | ||
| }))); | ||
| } | ||
| await (0, import_promises.writeFile)(file, js, "utf8"); | ||
| await (0, import_esbuild.build)({ | ||
| entryPoints: [file], | ||
| platform: "node", | ||
| format: "esm", | ||
| bundle: true, | ||
| minify: true, | ||
| legalComments: "none", | ||
| allowOverwrite: true, | ||
| outfile: file | ||
| }); | ||
| js = await (0, import_promises.readFile)(file, { encoding: "utf8" }); | ||
| await Promise.all(cleanupFiles.map((f) => (0, import_promises.unlink)(f))); | ||
| return js; | ||
@@ -112,0 +116,0 @@ }; |
+20
-17
@@ -76,22 +76,25 @@ // Copyright 2026 will Farrell, and ajv-cmd contributors. | ||
| const cleanupFiles = [file]; | ||
| if (needsBridge) { | ||
| await writeFile(bridgeFile, bridgeModuleContent, "utf8"); | ||
| cleanupFiles.push(bridgeFile); | ||
| } | ||
| if (needsBridge) cleanupFiles.push(bridgeFile); | ||
| await writeFile(file, js, "utf8"); | ||
| try { | ||
| if (needsBridge) { | ||
| await writeFile(bridgeFile, bridgeModuleContent, "utf8"); | ||
| } | ||
| await writeFile(file, js, "utf8"); | ||
| await build({ | ||
| entryPoints: [file], | ||
| platform: "node", | ||
| format: "esm", | ||
| bundle: true, | ||
| minify: true, | ||
| legalComments: "none", | ||
| allowOverwrite: true, | ||
| outfile: file, | ||
| }); | ||
| await build({ | ||
| entryPoints: [file], | ||
| platform: "node", | ||
| format: "esm", | ||
| bundle: true, | ||
| minify: true, | ||
| legalComments: "none", | ||
| allowOverwrite: true, | ||
| outfile: file, | ||
| }); | ||
| js = await readFile(file, { encoding: "utf8" }); | ||
| await Promise.all(cleanupFiles.map((f) => unlink(f))); | ||
| js = await readFile(file, { encoding: "utf8" }); | ||
| } finally { | ||
| await Promise.all(cleanupFiles.map((f) => unlink(f).catch(() => {}))); | ||
| } | ||
@@ -98,0 +101,0 @@ return js; |
+9
-9
@@ -22,3 +22,3 @@ "use strict"; | ||
| default: () => validate_default, | ||
| test: () => test | ||
| validate: () => validate | ||
| }); | ||
@@ -31,16 +31,16 @@ module.exports = __toCommonJS(validate_exports); | ||
| }; | ||
| const test = async (schema, options = {}) => { | ||
| const validate = async (schema, options = {}) => { | ||
| options = { ...defaultOptions, ...options }; | ||
| let validate, valid; | ||
| let compiled; | ||
| try { | ||
| validate = (0, import_compile.compile)(schema, options); | ||
| compiled = (0, import_compile.compile)(schema, options); | ||
| } catch (e) { | ||
| console.error(e.message); | ||
| return valid; | ||
| return void 0; | ||
| } | ||
| let testSuccess = true; | ||
| for (const data of options?.testData ?? []) { | ||
| valid = validate(structuredClone(data)); | ||
| const valid = compiled(structuredClone(data)); | ||
| if (!valid) { | ||
| console.error(validate.errors); | ||
| console.error(compiled.errors); | ||
| testSuccess = false; | ||
@@ -51,6 +51,6 @@ } | ||
| }; | ||
| var validate_default = test; | ||
| var validate_default = validate; | ||
| // Annotate the CommonJS export names for ESM import in node: | ||
| 0 && (module.exports = { | ||
| test | ||
| validate | ||
| }); |
+7
-21
@@ -5,4 +5,2 @@ // Copyright 2026 will Farrell, and ajv-cmd contributors. | ||
| //import spec from 'ajv/dist/refs/json-schema-2020-12/schema.json' assert { type: 'json' } | ||
| const defaultOptions = { | ||
@@ -12,30 +10,18 @@ allErrors: true, // required for `errorMessage` | ||
| export const test = async (schema, options = {}) => { | ||
| export const validate = async (schema, options = {}) => { | ||
| options = { ...defaultOptions, ...options }; | ||
| let validate, valid; | ||
| // Spec check | ||
| // const ajv = new Ajv(options) | ||
| // validate = ajv.compile(spec) | ||
| // valid = validate(schema) | ||
| // ajv.removeSchema() | ||
| // if (!valid) { | ||
| // console.error('Schema is not spec compliant') | ||
| // return valid | ||
| // } | ||
| // Compile check | ||
| let compiled; | ||
| try { | ||
| validate = compile(schema, options); | ||
| compiled = compile(schema, options); | ||
| } catch (e) { | ||
| console.error(e.message); | ||
| return valid; | ||
| return undefined; | ||
| } | ||
| // Data Check | ||
| let testSuccess = true; | ||
| for (const data of options?.testData ?? []) { | ||
| valid = validate(structuredClone(data)); | ||
| const valid = compiled(structuredClone(data)); | ||
| if (!valid) { | ||
| console.error(validate.errors); | ||
| console.error(compiled.errors); | ||
| testSuccess = false; | ||
@@ -48,2 +34,2 @@ } | ||
| export default test; | ||
| export default validate; |
-29
| { | ||
| "license": "license.template", | ||
| "licenseFormats": { | ||
| "js|ts": { | ||
| "eachLine": { | ||
| "prepend": "// " | ||
| } | ||
| } | ||
| }, | ||
| "ignoreFile": ".gitignore", | ||
| "ignore": [ | ||
| ".github/**/*", | ||
| ".husky/**/*", | ||
| "fixtures/*", | ||
| "commitlint.config.cjs", | ||
| "LICENSE", | ||
| "license.template", | ||
| "**/.gitignore", | ||
| "**/*.fuzz.js", | ||
| "**/*.perf.js", | ||
| "**/*.test.js", | ||
| "**/*.cjs", | ||
| "**/*.md", | ||
| "**/*.yml", | ||
| "**/.DS_Store", | ||
| "**/*.sh", | ||
| "**/*.ftl" | ||
| ] | ||
| } |
Sorry, the diff of this file is not supported yet
-55
| "use strict"; | ||
| var __create = Object.create; | ||
| var __defProp = Object.defineProperty; | ||
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
| var __getOwnPropNames = Object.getOwnPropertyNames; | ||
| var __getProtoOf = Object.getPrototypeOf; | ||
| var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
| var __export = (target, all) => { | ||
| for (var name in all) | ||
| __defProp(target, name, { get: all[name], enumerable: true }); | ||
| }; | ||
| var __copyProps = (to, from, except, desc) => { | ||
| if (from && typeof from === "object" || typeof from === "function") { | ||
| for (let key of __getOwnPropNames(from)) | ||
| if (!__hasOwnProp.call(to, key) && key !== except) | ||
| __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
| } | ||
| return to; | ||
| }; | ||
| var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( | ||
| // If the importer is in node compatibility mode or this is not an ESM | ||
| // file that has been converted to a CommonJS file using a Babel- | ||
| // compatible transform (i.e. "__esModule" has not been set), then set | ||
| // "default" to the CommonJS "module.exports" for node compatibility. | ||
| isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, | ||
| mod | ||
| )); | ||
| var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
| var sast_exports = {}; | ||
| __export(sast_exports, { | ||
| default: () => sast_default, | ||
| sast: () => sast | ||
| }); | ||
| module.exports = __toCommonJS(sast_exports); | ||
| var import_node_module = require("node:module"); | ||
| var import__ = __toESM(require("ajv/dist/2020.js"), 1); | ||
| const import_meta = {}; | ||
| const defaultOptions = { | ||
| strictTypes: false, | ||
| allErrors: true | ||
| }; | ||
| const sastSchema = (0, import_node_module.createRequire)(import_meta.url)( | ||
| "sast-json-schema/index.json" | ||
| ); | ||
| const sast = (_schema, options = {}) => { | ||
| options = { ...defaultOptions, ...options }; | ||
| const ajv = new import__.default(options); | ||
| const validate = ajv.compile(sastSchema); | ||
| return validate; | ||
| }; | ||
| var sast_default = sast; | ||
| // Annotate the CommonJS export names for ESM import in node: | ||
| 0 && (module.exports = { | ||
| sast | ||
| }); |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
11
-8.33%112401
-8.29%3163
-9.53%4
100%+ Added
- Removed
- Removed
Updated