@bedrock/validation 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,250 @@
1
+ /*!
2
+ * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+ import Ajv from 'ajv';
6
+ import {Cache} from './Cache.js';
7
+ import {promises as fs} from 'fs';
8
+ import {logger} from './logger.js';
9
+ import PATH from 'path';
10
+
11
+ const ajv = new Ajv({cache: new Cache(), serialize: false, verbose: true});
12
+ const {util: {BedrockError}} = bedrock;
13
+
14
+ // load config defaults
15
+ import './config.js';
16
+
17
+ // available schemas
18
+ export const schemas = {};
19
+
20
+ bedrock.events.on('bedrock.init', init);
21
+
22
+ /**
23
+ * Initializes the validation system: loads all schemas, etc.
24
+ */
25
+ async function init() {
26
+ // schemas to skip loading
27
+ const skip = bedrock.config.validation.schema.skip.slice();
28
+
29
+ // load all schemas in directory order
30
+ const schemaDirs = bedrock.config.validation.schema.paths;
31
+ const jsExt = '.js';
32
+ for(let schemaDir of schemaDirs) {
33
+ schemaDir = PATH.resolve(schemaDir);
34
+ logger.debug('loading schemas from: ' + schemaDir);
35
+ const files = (await fs.readdir(schemaDir)).filter(file => {
36
+ const js = PATH.extname(file) === jsExt;
37
+ const use = skip.indexOf(file) === -1;
38
+ return js && use;
39
+ });
40
+ // load files in parallel
41
+ await Promise.all(files.map(async file => {
42
+ const name = PATH.basename(file, PATH.extname(file));
43
+ const module = await import(PATH.join(schemaDir, file));
44
+ const api = module.default || module;
45
+ if(typeof api === 'function') {
46
+ if(name in schemas) {
47
+ logger.debug(
48
+ 'overwriting schema "' + name + '" with ' +
49
+ PATH.resolve(schemaDir, file));
50
+ }
51
+ schemas[name] = api;
52
+ schemas[name].instance = api();
53
+ logger.debug('loaded schema: ' + name);
54
+ } else {
55
+ for(const key in api) {
56
+ const tmp = name + '.' + key;
57
+ if(tmp in schemas) {
58
+ logger.debug('overwriting schema "' + tmp + '" with ' + file);
59
+ }
60
+ schemas[tmp] = api[key];
61
+ schemas[tmp].instance = schemas[tmp]();
62
+ logger.debug('loaded schema: ' + tmp);
63
+ }
64
+ }
65
+ }));
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Compiles the given schema, returning a validation function that takes one
71
+ * parameter: the data to be validated. It returns the same value that
72
+ * `validateInstance` returns.
73
+ *
74
+ * @param {object} options - The options to use.
75
+ * @param {object} options.schema - The JSON schema to compile.
76
+ *
77
+ * @returns {Function} The a validate function to call on instance data.
78
+ */
79
+ export function compile({schema} = {}) {
80
+ const fn = ajv.compile(schema);
81
+ fn.title = schema.title;
82
+ return function validate(instance) {
83
+ if(fn(instance)) {
84
+ return {valid: true};
85
+ }
86
+ return {
87
+ valid: false,
88
+ error: _createError({schema: fn, instance})
89
+ };
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Retrieves a validation schema given a name for the schema.
95
+ *
96
+ * @param {object} options - The options to use.
97
+ * @param {string} options.name - The name of the schema to retrieve.
98
+ *
99
+ * @returns {object|null} The object for the schema, or `null` if the schema
100
+ * doesn't exist.
101
+ */
102
+ export function getSchema({name} = {}) {
103
+ let schema = null;
104
+ if(name in schemas) {
105
+ schema = schemas[name].instance;
106
+ }
107
+ return schema;
108
+ }
109
+
110
+ /**
111
+ * Validates an instance against a schema.
112
+ *
113
+ * @param {object} options - The options to use.
114
+ * @param {*} options.instance - The instance to validate.
115
+ * @param {object|Array|Function|string} [options.schema] - The JSON schema,
116
+ * compiled schema function, or name of schema to use.
117
+ *
118
+ * @returns {object} The validation result.
119
+ */
120
+ export function validateInstance({instance, schema} = {}) {
121
+ const schemaIsFunction = typeof schema === 'function';
122
+
123
+ // do validation
124
+ let valid;
125
+ if(schemaIsFunction) {
126
+ valid = schema(instance);
127
+ } else {
128
+ if(typeof schema === 'string') {
129
+ const name = schema;
130
+ schema = getSchema({name});
131
+ if(!schema) {
132
+ throw new BedrockError(
133
+ `Could not validate data; unknown schema name (${name}).`,
134
+ 'NotFoundError', {schemaName: name});
135
+ }
136
+ }
137
+ valid = ajv.validate(schema, instance);
138
+ }
139
+ if(valid) {
140
+ return {valid};
141
+ }
142
+
143
+ const result = {
144
+ valid: false,
145
+ error: _createError({schema, instance})
146
+ };
147
+
148
+ return result;
149
+ }
150
+
151
+ /**
152
+ * Creates middleware that will validate request body and URL query parameters.
153
+ *
154
+ * Use this method over the deprecated `validate` to create a middleware.
155
+ *
156
+ * @param {object} options - The options to use.
157
+ * @param {object} [options.bodySchema] - The JSON schema to use to validate
158
+ * the request body.
159
+ * @param {object} [options.querySchema] - The JSON schema to use to validate
160
+ * the request URL query parameters.
161
+ *
162
+ * @returns {Function} An express-style middleware.
163
+ */
164
+ export function createValidateMiddleware({bodySchema, querySchema} = {}) {
165
+ if(!(bodySchema || querySchema)) {
166
+ throw new TypeError(
167
+ 'One of the following parameters is required: ' +
168
+ '"bodySchema", "querySchema".');
169
+ }
170
+ // pre-compile schemas
171
+ let validateBodySchema;
172
+ if(bodySchema) {
173
+ validateBodySchema = compile({schema: bodySchema});
174
+ }
175
+ let validateQuerySchema;
176
+ if(querySchema) {
177
+ validateQuerySchema = compile({schema: querySchema});
178
+ }
179
+ return function validate(req, res, next) {
180
+ if(validateBodySchema) {
181
+ const result = validateBodySchema(req.body);
182
+ if(!result.valid) {
183
+ return next(result.error);
184
+ }
185
+ }
186
+ if(validateQuerySchema) {
187
+ const result = validateQuerySchema(req.query);
188
+ if(!result.valid) {
189
+ return next(result.error);
190
+ }
191
+ }
192
+ next();
193
+ };
194
+ }
195
+
196
+ function _createError({schema, instance}) {
197
+ // create public error messages
198
+ const schemaIsFunction = typeof schema === 'function';
199
+ const validationErrors = schemaIsFunction ? schema.errors : ajv.errors;
200
+ const errors = [];
201
+ for(const error of validationErrors) {
202
+ // create custom error details
203
+ const details = {
204
+ instance,
205
+ params: error.params,
206
+ path: error.dataPath,
207
+ public: true,
208
+ schemaPath: error.schemaPath,
209
+ };
210
+ let title;
211
+ if(Array.isArray(error.schema)) {
212
+ [title] = error.schema;
213
+ }
214
+ title = title || error.parentSchema.title || '',
215
+ details.schema = {
216
+ description: error.parentSchema.description || '',
217
+ title,
218
+ };
219
+ // include custom errors or use default
220
+ // FIXME: enable if ajv supports this parentSchema.errors property
221
+ // it appears that this is not the case
222
+ // details.errors = error.parentSchema.errors || {
223
+ // invalid: 'Invalid input.',
224
+ // missing: 'Missing input.'
225
+ // };
226
+ if(error.data) {
227
+ if(error.parentSchema.errors && 'mask' in error.parentSchema.errors) {
228
+ const mask = error.parentSchema.errors.mask;
229
+ if(mask === true) {
230
+ details.value = '***MASKED***';
231
+ } else {
232
+ details.value = mask;
233
+ }
234
+ } else {
235
+ details.value = error.data;
236
+ }
237
+ }
238
+
239
+ // add bedrock validation error
240
+ errors.push(new BedrockError(error.message, 'ValidationError', details));
241
+ }
242
+
243
+ const msg = schema.title ?
244
+ 'A validation error occured in the \'' + schema.title + '\' validator.' :
245
+ 'A validation error occured in an unnamed validator.';
246
+ const error = new BedrockError(
247
+ msg, 'ValidationError', {public: true, errors, httpStatusCode: 400});
248
+
249
+ return error;
250
+ }
package/lib/logger.js ADDED
@@ -0,0 +1,5 @@
1
+ /*!
2
+ * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import {loggers} from '@bedrock/core';
5
+ export const logger = loggers.get('app').child('bedrock-validation');
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@bedrock/validation",
3
+ "version": "6.0.0",
4
+ "type": "module",
5
+ "description": "Bedrock validation",
6
+ "main": "./lib/index.js",
7
+ "scripts": {
8
+ "lint": "eslint ."
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/digitalbazaar/bedrock-validation"
13
+ },
14
+ "keywords": [
15
+ "bedrock"
16
+ ],
17
+ "author": {
18
+ "name": "Digital Bazaar, Inc.",
19
+ "email": "support@digitalbazaar.com",
20
+ "url": "http://digitalbazaar.com"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/digitalbazaar/bedrock-validation/issues"
24
+ },
25
+ "homepage": "https://github.com/digitalbazaar/bedrock-validation",
26
+ "dependencies": {
27
+ "ajv": "^6.12.0"
28
+ },
29
+ "peerDependencies": {
30
+ "@bedrock/core": "^5.0.0"
31
+ },
32
+ "directories": {
33
+ "lib": "./lib"
34
+ },
35
+ "devDependencies": {
36
+ "eslint": "^7.32.0",
37
+ "eslint-config-digitalbazaar": "^2.8.0",
38
+ "eslint-plugin-jsdoc": "^37.9.7",
39
+ "jsdoc-to-markdown": "^6.0.1"
40
+ }
41
+ }
@@ -0,0 +1,24 @@
1
+ /*!
2
+ * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+
6
+ const schema = {
7
+ title: 'Comment',
8
+ description: 'A short comment.',
9
+ type: 'string',
10
+ minLength: 1,
11
+ maxLength: 5000,
12
+ errors: {
13
+ invalid: 'The comment contains invalid characters or is more than ' +
14
+ '5000 characters in length.',
15
+ missing: 'Please enter a comment.'
16
+ }
17
+ };
18
+
19
+ export default function(extend) {
20
+ if(extend) {
21
+ return bedrock.util.extend(true, bedrock.util.clone(schema), extend);
22
+ }
23
+ return schema;
24
+ }
@@ -0,0 +1,37 @@
1
+ /*!
2
+ * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+ import identifier from './identifier.js';
6
+ import jsonldContext from './jsonldContext.js';
7
+ import w3cDateTime from './w3cDateTime.js';
8
+
9
+ // TODO: Improve this schema
10
+ const schema = {
11
+ type: 'object',
12
+ title: 'Credential',
13
+ properties: {
14
+ '@context': jsonldContext(),
15
+ // FIXME: improve credential context check
16
+ //'@context': schemas.jsonldContext([
17
+ // constants.IDENTITY_CONTEXT_V1_URL,
18
+ // constants.CREDENTIALS_CONTEXT_V1_URL
19
+ //]),
20
+ issuer: identifier(),
21
+ issued: w3cDateTime(),
22
+ claim: {
23
+ required: ['id'],
24
+ properties: {
25
+ id: identifier()
26
+ },
27
+ }
28
+ },
29
+ required: ['issuer', 'issued', 'claim']
30
+ };
31
+
32
+ export default function(extend) {
33
+ if(extend) {
34
+ return bedrock.util.extend(true, bedrock.util.clone(schema), extend);
35
+ }
36
+ return schema;
37
+ }
@@ -0,0 +1,24 @@
1
+ /*!
2
+ * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+
6
+ const schema = {
7
+ title: 'Description',
8
+ description: 'A description.',
9
+ type: 'string',
10
+ minLength: 0,
11
+ maxLength: 5000,
12
+ errors: {
13
+ invalid: 'The description contains invalid characters or is more than ' +
14
+ '5000 characters in length.',
15
+ missing: 'Please enter a description.'
16
+ }
17
+ };
18
+
19
+ export default function(extend) {
20
+ if(extend) {
21
+ return bedrock.util.extend(true, bedrock.util.clone(schema), extend);
22
+ }
23
+ return schema;
24
+ }
@@ -0,0 +1,34 @@
1
+ /*!
2
+ * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+
6
+ // RFC 1034 - All labels have a max length of 63 octets.
7
+ // https://tools.ietf.org/html/rfc1034#section-3.1
8
+ const schema = {
9
+ title: 'Email',
10
+ description: 'An email address.',
11
+ type: 'string',
12
+ // eslint-disable-next-line max-len
13
+ pattern: '^[-a-z0-9~!$%^&*_=+}{\\\'?]+(\\.[-a-z0-9~!$%^&*_=+}{\\\'?]+)*@(((([a-z0-9]{1}[a-z0-9\\-]{0,63}[a-z0-9]{1})|[a-z])\\.)+[a-z]{2,63})$',
14
+ minLength: 1,
15
+ maxLength: 100,
16
+ errors: {
17
+ invalid: 'The email address is invalid.',
18
+ missing: 'Please enter an email address.'
19
+ }
20
+ };
21
+
22
+ export default function(extend, options) {
23
+ if(options && options.lowerCaseOnly) {
24
+ extend = extend || {};
25
+ if(!('pattern' in extend)) {
26
+ // eslint-disable-next-line max-len
27
+ extend.pattern = '^[-a-z0-9~!$%^&*_=+}{\\\'?]+(\\.[-a-z0-9~!$%^&*_=+}{\\\'?]+)*@(((([a-z0-9]{1}[a-z0-9\\-]{0,63}[a-z0-9]{1})|[a-z])\\.)+[a-z]{2,63})$';
28
+ }
29
+ }
30
+ if(extend) {
31
+ return bedrock.util.extend(true, bedrock.util.clone(schema), extend);
32
+ }
33
+ return schema;
34
+ }
@@ -0,0 +1,22 @@
1
+ /*!
2
+ * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+
6
+ const schema = {
7
+ title: 'ID',
8
+ description: 'A unique identifier.',
9
+ type: 'string',
10
+ minLength: 1,
11
+ disallow: {
12
+ type: 'string',
13
+ enum: ['0']
14
+ }
15
+ };
16
+
17
+ export default function(extend) {
18
+ if(extend) {
19
+ return bedrock.util.extend(true, bedrock.util.clone(schema), extend);
20
+ }
21
+ return schema;
22
+ }
@@ -0,0 +1,38 @@
1
+ /*!
2
+ * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+
6
+ const schema = {
7
+ title: 'JSON Patch',
8
+ type: 'array',
9
+ minItems: 1,
10
+ items: {
11
+ type: 'object',
12
+ required: ['op', 'path'],
13
+ // FIXME: more strictly validate properties based on value of `op`
14
+ properties: {
15
+ op: {
16
+ type: 'string',
17
+ enum: ['add', 'copy', 'move', 'remove', 'replace', 'test']
18
+ },
19
+ from: {
20
+ type: 'string',
21
+ },
22
+ path: {
23
+ type: 'string',
24
+ },
25
+ value: {
26
+ //type: ['number', 'string', 'boolean', 'object', 'array'],
27
+ }
28
+ },
29
+ additionalProperties: false
30
+ }
31
+ };
32
+
33
+ export default function(extend) {
34
+ if(extend) {
35
+ return bedrock.util.extend(true, bedrock.util.clone(schema), extend);
36
+ }
37
+ return schema;
38
+ }
@@ -0,0 +1,54 @@
1
+ /*!
2
+ * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+
6
+ export default function(context, extend) {
7
+ const schema = {
8
+ title: 'JSON-LD context',
9
+ description: 'A JSON-LD Context'
10
+ };
11
+ if(!Array.isArray(context)) {
12
+ schema.anyOf = [{
13
+ type: 'string'
14
+ // enum added below if context param truthy
15
+ }, {
16
+ type: 'object'
17
+ // FIXME: improve context object validator
18
+ }, {
19
+ type: 'array',
20
+ // items added below if context param truthy
21
+ }];
22
+ if(context) {
23
+ schema.anyOf[0].enum = [context];
24
+ schema.anyOf[2].items = [{const: context}];
25
+ schema.anyOf[2].additionalItems = false;
26
+ }
27
+ } else {
28
+ Object.assign(schema, {
29
+ type: 'array',
30
+ minItems: context.length,
31
+ uniqueItems: true,
32
+ items: [],
33
+ errors: {
34
+ invalid: 'The JSON-LD context information is invalid.',
35
+ missing: 'The JSON-LD context information is missing.'
36
+ }
37
+ });
38
+ for(let i = 0; i < context.length; ++i) {
39
+ if(typeof context[i] === 'string') {
40
+ schema.items.push({
41
+ type: 'string',
42
+ enum: [context[i]]
43
+ });
44
+ } else {
45
+ // FIXME: improve context object validator
46
+ schema.items.push({type: 'object'});
47
+ }
48
+ }
49
+ }
50
+ if(extend) {
51
+ return bedrock.util.extend(true, bedrock.util.clone(schema), extend);
52
+ }
53
+ return schema;
54
+ }
@@ -0,0 +1,72 @@
1
+ /*!
2
+ * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ export default function(types, alternates) {
5
+ const schema = {
6
+ title: 'Object Type',
7
+ description: 'A set of terms, CURIEs, or URLs specifying the type of ' +
8
+ 'the object.',
9
+ anyOf: []
10
+ };
11
+
12
+ types = Array.isArray(types) ? types : [types];
13
+
14
+ // allow single object
15
+ if(types.length === 1) {
16
+ schema.anyOf.push({
17
+ type: 'string',
18
+ enum: types,
19
+ errors: {
20
+ invalid: 'The JSON-LD type information is invalid.',
21
+ missing: 'The JSON-LD type information is missing.'
22
+ }
23
+ });
24
+ }
25
+
26
+ // allow array combination of all types
27
+ schema.anyOf.push({
28
+ type: 'array',
29
+ // minItems: types.length,
30
+ uniqueItems: true,
31
+ items: {
32
+ type: 'string',
33
+ enum: types
34
+ },
35
+ errors: {
36
+ invalid: 'The JSON-LD type information is invalid.',
37
+ missing: 'The JSON-LD type information is missing.'
38
+ }
39
+ });
40
+
41
+ // HACK: madness to support given types *must* exist, while allowing
42
+ // up to <alternates> other custom types
43
+ if(alternates !== undefined) {
44
+ for(let before = 0; before <= alternates; ++before) {
45
+ const s = {
46
+ type: 'array',
47
+ minItems: types.length,
48
+ uniqueItems: true,
49
+ items: [],
50
+ additionalItems: {
51
+ type: 'string'
52
+ },
53
+ errors: {
54
+ invalid: 'The JSON-LD type information is invalid.',
55
+ missing: 'The JSON-LD type information is missing.'
56
+ }
57
+ };
58
+ for(let i = 0; i < before; ++i) {
59
+ s.items.push({type: 'string'});
60
+ }
61
+ for(let i = 0; i < types.length; ++i) {
62
+ s.items.push({
63
+ type: 'string',
64
+ enum: types
65
+ });
66
+ }
67
+ schema.anyOf.push(s);
68
+ }
69
+ }
70
+
71
+ return schema;
72
+ }
@@ -0,0 +1,25 @@
1
+ /*!
2
+ * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+
6
+ const schema = {
7
+ title: 'Label',
8
+ description: 'A short, descriptive label.',
9
+ type: 'string',
10
+ pattern: '^[-a-zA-Z0-9~`!@#$%^&*\\(\\)\\[\\]{}<>_=+\\\\|:;\'"\\.,/? ]*$',
11
+ minLength: 1,
12
+ maxLength: 200,
13
+ errors: {
14
+ invalid: 'The label contains invalid characters or is not between ' +
15
+ '1 and 200 characters in length.',
16
+ missing: 'Please enter a label.'
17
+ }
18
+ };
19
+
20
+ export default function(extend) {
21
+ if(extend) {
22
+ return bedrock.util.extend(true, bedrock.util.clone(schema), extend);
23
+ }
24
+ return schema;
25
+ }
@@ -0,0 +1,46 @@
1
+ /*!
2
+ * Copyright (c) 2012-2022 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import * as bedrock from '@bedrock/core';
5
+ import identifier from './identifier.js';
6
+ import w3cDateTime from './w3cDateTime.js';
7
+
8
+ const signature = {
9
+ title: 'Linked Data Signature',
10
+ description: 'A Linked Data digital signature.',
11
+ type: 'object',
12
+ properties: {
13
+ id: identifier(),
14
+ type: {
15
+ title: 'Linked Data Signature Type',
16
+ type: 'string',
17
+ enum: ['LinkedDataSignature2015', 'LinkedDataSignature2016']
18
+ },
19
+ creator: identifier(),
20
+ created: w3cDateTime(),
21
+ signatureValue: {
22
+ title: 'Digital Signature Value',
23
+ description: 'The Base64 encoding of the result of the signature ' +
24
+ 'algorithm.',
25
+ type: 'string'
26
+ },
27
+ },
28
+ // NOTE: id is not required
29
+ required: ['type', 'creator', 'created', 'signatureValue']
30
+ };
31
+
32
+ const schema = {
33
+ title: 'Linked Data Signatures',
34
+ anyOf: [{
35
+ type: 'array',
36
+ items: signature,
37
+ minItems: 1,
38
+ }, signature]
39
+ };
40
+
41
+ export default function(extend) {
42
+ if(extend) {
43
+ return bedrock.util.extend(true, bedrock.util.clone(schema), extend);
44
+ }
45
+ return schema;
46
+ }