Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@exodus/schemasafe

Package Overview
Dependencies
Maintainers
99
Versions
30
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@exodus/schemasafe - npm Package Compare versions

Comparing version 1.0.0-rc.8 to 1.0.0-rc.9

2

package.json
{
"name": "@exodus/schemasafe",
"version": "1.0.0-rc.8",
"version": "1.0.0-rc.9",
"description": "JSON Safe Parser & Schema Validator",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -23,3 +23,3 @@ # `@exodus/schemasafe`

_E.g. it will detect mistakes like `{type: "array", "maxLength": 2}`._
* [Less than 2000 lines of code](./doc/Auditable.md), non-minified.
* [About 2000 lines of code](./doc/Auditable.md), non-minified.
* Uses [secure code generation](./doc/Secure-code-generation.md) approach to prevent data from schema from leaking into

@@ -26,0 +26,0 @@ the generated code without being JSON-wrapped.

@@ -87,3 +87,3 @@ 'use strict'

)
return `(function() {\n'use strict'\n${scopeDefs.join('\n')}\n${build()}})();`
return `(function() {\n'use strict'\n${scopeDefs.join('\n')}\n${build()}})()`
},

@@ -90,0 +90,0 @@

@@ -6,31 +6,52 @@ 'use strict'

const { compile } = require('./compile')
const functions = require('./scope-functions')
const { deepEqual } = require('./scope-functions')
const validator = (schema, { jsonCheck = false, isJSON = false, schemas, ...opts } = {}) => {
const jsonCheckWithErrors = (validate) =>
function validateIsJSON(data) {
if (!deepEqual(data, JSON.parse(JSON.stringify(data)))) {
validateIsJSON.errors = [{ instanceLocation: '#', error: 'not JSON compatible' }]
return false
}
const res = validate(data)
validateIsJSON.errors = validate.errors
return res
}
const jsonCheckWithoutErrors = (validate) => (data) =>
deepEqual(data, JSON.parse(JSON.stringify(data))) && validate(data)
const validator = (
schema,
{ parse = false, multi = false, jsonCheck = false, isJSON = false, schemas = [], ...opts } = {}
) => {
if (jsonCheck && isJSON) throw new Error('Can not specify both isJSON and jsonCheck options')
const options = { ...opts, schemas: buildSchemas(schemas || []), isJSON: isJSON || jsonCheck }
const { scope, refs } = compile([schema], options) // only a single ref
if (parse && (jsonCheck || isJSON))
throw new Error('jsonCheck and isJSON options are not applicable in parser mode')
const mode = parse ? 'strong' : 'default' // strong mode is default in parser, can be overriden
const willJSON = isJSON || jsonCheck || parse
const arg = multi ? schema : [schema]
const options = { mode, ...opts, schemas: buildSchemas(schemas, arg), isJSON: willJSON }
const { scope, refs } = compile(arg, options) // only a single ref
if (opts.dryRun) return
const fun = genfun()
if (!jsonCheck) {
fun.write('%s', refs[0])
if (parse) {
scope.parseWrap = opts.includeErrors ? parseWithErrors : parseWithoutErrors
} else if (jsonCheck) {
scope.deepEqual = deepEqual
scope.jsonCheckWrap = opts.includeErrors ? jsonCheckWithErrors : jsonCheckWithoutErrors
}
if (multi) {
fun.write('[')
for (const ref of refs.slice(0, -1)) fun.write('%s,', ref)
if (refs.length > 0) fun.write('%s', refs[refs.length - 1])
fun.write(']')
if (parse) fun.write('.map(parseWrap)')
else if (jsonCheck) fun.write('.map(jsonCheckWrap)')
} else {
// jsonCheck wrapper implementation below
scope.deepEqual = functions.deepEqual
fun.write('function validateIsJSON(data) {')
if (opts.includeErrors) {
fun.write('if (!deepEqual(data, JSON.parse(JSON.stringify(data)))) {')
fun.write('validateIsJSON.errors = [{instanceLocation:"#",error:"not JSON compatible"}]')
fun.write('return false')
fun.write('}')
fun.write('const res = %s(data)', refs[0])
fun.write('validateIsJSON.errors = actualValidate.errors')
fun.write('return res')
} else {
fun.write('return deepEqual(data, JSON.parse(JSON.stringify(data))) && %s(data)', refs[0])
}
fun.write('}')
if (parse) fun.write('parseWrap(%s)', refs[0])
else if (jsonCheck) fun.write('jsonCheckWrap(%s)', refs[0])
else fun.write('%s', refs[0])
}
const validate = fun.makeFunction(scope)
validate.toModule = () => fun.makeModule(scope)
validate.toModule = ({ semi = true } = {}) => fun.makeModule(scope) + (semi ? ';' : '')
validate.toJSON = () => schema

@@ -40,45 +61,34 @@ return validate

const parser = function(schema, opts = {}) {
// strong mode is default in parser
if (functions.hasOwn(opts, 'jsonCheck') || functions.hasOwn(opts, 'isJSON'))
throw new Error('jsonCheck and isJSON options are not applicable in parser mode')
const validate = validator(schema, { mode: 'strong', ...opts, jsonCheck: false, isJSON: true })
const parse = opts.includeErrors
? (src) => {
if (typeof src !== 'string') return { valid: false, error: 'Input is not a string' }
try {
const value = JSON.parse(src)
if (!validate(value)) {
const { keywordLocation, instanceLocation } = validate.errors[0]
const keyword = keywordLocation.slice(keywordLocation.lastIndexOf('/') + 1)
const error = `JSON validation failed for ${keyword} at ${instanceLocation}`
return { valid: false, error, errors: validate.errors }
}
return { valid: true, value }
} catch ({ message }) {
return { valid: false, error: message }
}
}
: (src) => {
if (typeof src !== 'string') return { valid: false }
try {
const value = JSON.parse(src)
if (!validate(value)) return { valid: false }
return { valid: true, value }
} catch (e) {
return { valid: false }
}
}
parse.toModule = () =>
[
'(function(src) {',
`const validate = ${validate.toModule()}`,
`const parse = ${parse}\n`,
'return parse(src)',
'});',
].join('\n')
parse.toJSON = () => schema
return parse
const parseWithErrors = (validate) => (src) => {
if (typeof src !== 'string') return { valid: false, error: 'Input is not a string' }
try {
const value = JSON.parse(src)
if (!validate(value)) {
const { keywordLocation, instanceLocation } = validate.errors[0]
const keyword = keywordLocation.slice(keywordLocation.lastIndexOf('/') + 1)
const error = `JSON validation failed for ${keyword} at ${instanceLocation}`
return { valid: false, error, errors: validate.errors }
}
return { valid: true, value }
} catch ({ message }) {
return { valid: false, error: message }
}
}
const parseWithoutErrors = (validate) => (src) => {
if (typeof src !== 'string') return { valid: false }
try {
const value = JSON.parse(src)
if (!validate(value)) return { valid: false }
return { valid: true, value }
} catch (e) {
return { valid: false }
}
}
const parser = function(schema, { parse = true, ...opts } = {}) {
if (!parse) throw new Error('can not disable parse in parser')
return validator(schema, { parse, ...opts })
}
module.exports = { validator, parser }

@@ -5,2 +5,7 @@ 'use strict'

function safeSet(map, key, value, comment = 'keys') {
if (!map.has(key)) return map.set(key, value)
if (map.get(key) !== value) throw new Error(`Conflicting duplicate ${comment}: ${key}`)
}
function untilde(string) {

@@ -58,11 +63,25 @@ if (!string.includes('~')) return string

const withSpecialChilds = ['properties', 'patternProperties', '$defs', 'definitions']
const skipChilds = ['const', 'enum', 'examples', 'example', 'comment']
const sSkip = Symbol('skip')
function traverse(schema, work) {
const visit = (sub, specialChilds = false) => {
if (!sub || typeof sub !== 'object') return
const res = work(sub)
if (res !== undefined) return res
if (res === sSkip) return
for (const k of Object.keys(sub)) {
if (!specialChilds && !Array.isArray(sub) && !knownKeywords.includes(k)) continue
if (!specialChilds && skipChilds.includes(k)) continue
const kres = visit(sub[k], !specialChilds && withSpecialChilds.includes(k))
if (kres !== undefined) return kres
}
}
return visit(schema)
}
// Returns a list of resolved entries, in a form: [schema, root, basePath]
// basePath doesn't contain the target object $id itself
function resolveReference(root, additionalSchemas, ref, base = '') {
function resolveReference(root, schemas, ref, base = '') {
const ptr = joinPath(base, ref)
const schemas = new Map(additionalSchemas)
const self = (base || '').split('#')[0]
if (self) schemas.set(self, root)
const results = []

@@ -76,2 +95,3 @@

if (!sub || typeof sub !== 'object') return
const id = sub.$id || sub.id

@@ -96,5 +116,6 @@ let path = oldPath

}
for (const k of Object.keys(sub)) {
if (!specialChilds && !Array.isArray(sub) && !knownKeywords.includes(k)) continue
if (!specialChilds && ['const', 'enum', 'examples', 'comment'].includes(k)) continue
if (!specialChilds && skipChilds.includes(k)) continue
visit(sub[k], path, !specialChilds && withSpecialChilds.includes(k))

@@ -115,3 +136,3 @@ }

if (schemas.has(main)) {
const additional = resolveReference(schemas.get(main), additionalSchemas, `#${hash}`)
const additional = resolveReference(schemas.get(main), schemas, `#${hash}`)
results.push(...additional.map(([res, rRoot, rPath]) => [res, rRoot, joinPath(main, rPath)]))

@@ -128,5 +149,4 @@ }

const results = new Map()
const visit = (sub, specialChilds = false) => {
if (!sub || typeof sub !== 'object') return
if (sub !== schema && (sub.$id || sub.id)) return // base changed, no longer in the same resource
traverse(schema, (sub) => {
if (sub !== schema && (sub.$id || sub.id)) return sSkip // base changed, no longer in the same resource
const anchor = sub.$dynamicAnchor

@@ -136,30 +156,30 @@ if (anchor && typeof anchor === 'string') {

if (!/^[a-zA-Z0-9_-]+$/.test(anchor)) throw new Error(`Unsupported $dynamicAnchor: ${anchor}`)
if (results.has(anchor)) throw new Error(`duplicate $dynamicAnchor: ${anchor}`)
results.set(anchor, sub)
safeSet(results, anchor, sub, '$dynamicAnchor')
}
for (const k of Object.keys(sub)) {
if (!specialChilds && !Array.isArray(sub) && !knownKeywords.includes(k)) continue
if (!specialChilds && ['const', 'enum', 'examples', 'comment'].includes(k)) continue
visit(sub[k], !specialChilds && withSpecialChilds.includes(k))
}
}
visit(schema)
})
return results
}
function hasKeywords(schema, keywords) {
const visit = (sub, specialChilds = false) => {
if (!sub || typeof sub !== 'object') return false
for (const k of Object.keys(sub)) {
if (keywords.includes(k)) return true
if (!specialChilds && !Array.isArray(sub) && !knownKeywords.includes(k)) continue
if (!specialChilds && ['const', 'enum', 'examples', 'comment'].includes(k)) continue
if (visit(sub[k], !specialChilds && withSpecialChilds.includes(k))) return true
}
return false
const hasKeywords = (schema, keywords) =>
traverse(schema, (s) => Object.keys(s).some((k) => keywords.includes(k)) || undefined) || false
const addSchemasArrayToMap = (schemas, input, optional = false) => {
if (!Array.isArray(input)) throw new Error('Expected an array of schemas')
// schema ids are extracted from the schemas themselves
for (const schema of input) {
traverse(schema, (sub) => {
const idRaw = sub.$id || sub.id
const id = idRaw && typeof idRaw === 'string' ? idRaw.replace(/#$/, '') : null // # is allowed only as the last symbol here
if (id && id.includes('://') && !id.includes('#')) {
safeSet(schemas, id, sub, "schema $id in 'schemas'")
} else if (sub === schema && !optional) {
throw new Error("Schema with missing or invalid $id in 'schemas'")
}
})
}
return visit(schema)
return schemas
}
const buildSchemas = (input) => {
const buildSchemas = (input, extra) => {
if (extra) return addSchemasArrayToMap(buildSchemas(input), extra, true)
if (input) {

@@ -172,22 +192,3 @@ switch (Object.getPrototypeOf(input)) {

case Array.prototype: {
// In this case, schema ids are extracted from the schemas themselves
const schemas = new Map()
const cleanId = (id) =>
// # is allowed only as the last symbol here
id && typeof id === 'string' && !/#./.test(id) ? id.replace(/#$/, '') : null
for (const schema of input) {
const visit = (sub) => {
if (!sub || typeof sub !== 'object') return
const id = cleanId(sub.$id || sub.id)
if (id && id.includes('://')) {
if (schemas.has(id)) throw new Error("Duplicate schema $id in 'schemas'")
schemas.set(id, sub)
} else if (sub === schema) {
throw new Error("Schema with missing or invalid $id in 'schemas'")
}
for (const k of Object.keys(sub)) visit(sub[k])
}
visit(schema)
}
return schemas
return addSchemasArrayToMap(new Map(), input)
}

@@ -194,0 +195,0 @@ }

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc