@percy/config 1.31.0-alpha.3 → 1.31.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/dist/defaults.js +6 -2
- package/dist/utils/normalize.js +26 -0
- package/dist/validate.js +79 -5
- package/package.json +4 -4
package/dist/defaults.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { merge } from './utils/index.js';
|
|
1
|
+
import { merge, sanitizeObject } from './utils/index.js';
|
|
2
2
|
import { getSchema } from './validate.js';
|
|
3
3
|
const {
|
|
4
4
|
isArray
|
|
@@ -30,7 +30,11 @@ function getDefaultsFromSchema(schema) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
export function getDefaults(overrides = {}) {
|
|
33
|
-
|
|
33
|
+
// We are sanitizing the overrides object to prevent prototype pollution.
|
|
34
|
+
// This ensures protection against attacks where a payload having Object.prototype setters
|
|
35
|
+
// to add or modify properties on the global prototype chain, which could lead to issues like denial of service (DoS) at a minimum.
|
|
36
|
+
const sanitizedOverrides = sanitizeObject(overrides);
|
|
37
|
+
return merge([getDefaultsFromSchema(), sanitizedOverrides], (path, prev, next) => {
|
|
34
38
|
// override default array instead of merging
|
|
35
39
|
return isArray(next) && [path, next];
|
|
36
40
|
});
|
package/dist/utils/normalize.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import merge from './merge.js';
|
|
2
2
|
import { getSchema } from '../validate.js';
|
|
3
|
+
const {
|
|
4
|
+
isArray
|
|
5
|
+
} = Array;
|
|
3
6
|
|
|
4
7
|
// Edge case camelizations
|
|
5
8
|
const CAMELCASE_MAP = new Map([['css', 'CSS'], ['javascript', 'JavaScript'], ['dom', 'DOM']]);
|
|
@@ -7,6 +10,9 @@ const CAMELCASE_MAP = new Map([['css', 'CSS'], ['javascript', 'JavaScript'], ['d
|
|
|
7
10
|
// Regular expression that matches words from boundaries or consecutive casing
|
|
8
11
|
const WORD_REG = /[a-z]{2,}|[A-Z]{2,}|[0-9]{2,}|[^-_\s]+?(?=[A-Z0-9-_\s]|$)/g;
|
|
9
12
|
|
|
13
|
+
// Unsafe keys list
|
|
14
|
+
const UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype', 'toString', 'valueOf', '__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__'];
|
|
15
|
+
|
|
10
16
|
// Converts kebab-cased and snake_cased strings to camelCase.
|
|
11
17
|
export function camelcase(str) {
|
|
12
18
|
if (typeof str !== 'string') return str;
|
|
@@ -53,4 +59,24 @@ export function normalize(object, options) {
|
|
|
53
59
|
return [mapped];
|
|
54
60
|
});
|
|
55
61
|
}
|
|
62
|
+
|
|
63
|
+
// Utility function to prevent prototype pollution
|
|
64
|
+
export function isSafeKey(key) {
|
|
65
|
+
return !UNSAFE_KEYS.includes(key);
|
|
66
|
+
}
|
|
67
|
+
export function sanitizeObject(obj) {
|
|
68
|
+
if (!obj || typeof obj !== 'object' || isArray(obj)) {
|
|
69
|
+
return obj;
|
|
70
|
+
}
|
|
71
|
+
if (obj instanceof RegExp) {
|
|
72
|
+
return obj;
|
|
73
|
+
}
|
|
74
|
+
const sanitized = {};
|
|
75
|
+
for (const key in obj) {
|
|
76
|
+
if (isSafeKey(key)) {
|
|
77
|
+
sanitized[key] = sanitizeObject(obj[key]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return sanitized;
|
|
81
|
+
}
|
|
56
82
|
export default normalize;
|
package/dist/validate.js
CHANGED
|
@@ -65,6 +65,20 @@ const ajv = new AJV({
|
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
}, {
|
|
69
|
+
keyword: 'onlyWeb',
|
|
70
|
+
error: {
|
|
71
|
+
message: 'property only valid with Web integration.'
|
|
72
|
+
},
|
|
73
|
+
code: cxt => {
|
|
74
|
+
const tokenPrefix = (process.env.PERCY_TOKEN || '').split('_')[0];
|
|
75
|
+
const isNotWebProjectToken = tokenPrefix === 'auto' || tokenPrefix === 'app';
|
|
76
|
+
|
|
77
|
+
// we do validation only when token is passed
|
|
78
|
+
if (!!process.env.PERCY_TOKEN && isNotWebProjectToken) {
|
|
79
|
+
cxt.error();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
68
82
|
}]
|
|
69
83
|
});
|
|
70
84
|
|
|
@@ -184,8 +198,8 @@ function shouldHideError(key, path, error) {
|
|
|
184
198
|
|
|
185
199
|
// Validates data according to the associated schema and returns a list of errors, if any.
|
|
186
200
|
export function validate(data, key = '/config') {
|
|
201
|
+
let errors = new Map();
|
|
187
202
|
if (!ajv.validate(key, data)) {
|
|
188
|
-
let errors = new Map();
|
|
189
203
|
for (let error of ajv.errors) {
|
|
190
204
|
var _parentSchema$errors2;
|
|
191
205
|
let {
|
|
@@ -242,12 +256,72 @@ export function validate(data, key = '/config') {
|
|
|
242
256
|
message
|
|
243
257
|
});
|
|
244
258
|
}
|
|
245
|
-
|
|
246
259
|
// filter empty values as a result of scrubbing
|
|
247
260
|
filterEmpty(data);
|
|
248
|
-
|
|
249
|
-
// return an array of errors
|
|
250
|
-
return Array.from(errors.values());
|
|
251
261
|
}
|
|
262
|
+
if (data.regions && Array.isArray(data.regions)) {
|
|
263
|
+
data.regions.forEach((region, index) => {
|
|
264
|
+
/* istanbul ignore next */
|
|
265
|
+
if (region.elementSelector) {
|
|
266
|
+
const selectorKeys = ['elementCSS', 'elementXpath', 'boundingBox'];
|
|
267
|
+
const providedKeys = selectorKeys.filter(key => region.elementSelector[key] !== undefined);
|
|
268
|
+
if (providedKeys.length !== 1) {
|
|
269
|
+
const pathStr = `regions[${index}].elementSelector`;
|
|
270
|
+
errors.set(pathStr, {
|
|
271
|
+
path: pathStr,
|
|
272
|
+
message: "Exactly one of 'elementCSS', 'elementXpath', or 'boundingBox' must be provided."
|
|
273
|
+
});
|
|
274
|
+
delete data.regions[index];
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
if (region.algorithm === 'ignore') {
|
|
278
|
+
const pathStr = `regions[${index}].elementSelector`;
|
|
279
|
+
errors.set(pathStr, {
|
|
280
|
+
path: pathStr,
|
|
281
|
+
message: "'elementSelector' is required when algorithm is 'ignore'."
|
|
282
|
+
});
|
|
283
|
+
delete data.regions[index];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const algorithmType = region.algorithm;
|
|
287
|
+
const hasConfiguration = region.configuration !== undefined;
|
|
288
|
+
if (algorithmType === 'layout' || algorithmType === 'ignore') {
|
|
289
|
+
if (hasConfiguration) {
|
|
290
|
+
const pathStr = `regions[${index}].configuration`;
|
|
291
|
+
errors.set(pathStr, {
|
|
292
|
+
path: pathStr,
|
|
293
|
+
message: `Configuration is not applicable for '${algorithmType}' algorithm`
|
|
294
|
+
});
|
|
295
|
+
delete data.regions[index];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if ((algorithmType === 'standard' || algorithmType === 'intelliignore') && !hasConfiguration) {
|
|
299
|
+
const pathStr = `regions[${index}]`;
|
|
300
|
+
errors.set(pathStr, {
|
|
301
|
+
path: pathStr,
|
|
302
|
+
message: `Configuration is recommended for '${algorithmType}' algorithm`
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (data.algorithmConfiguration) {
|
|
308
|
+
const pathStr = 'algorithmConfiguration';
|
|
309
|
+
const algorithmType = data.algorithm;
|
|
310
|
+
if (!algorithmType) {
|
|
311
|
+
errors.set(pathStr, {
|
|
312
|
+
path: pathStr,
|
|
313
|
+
message: 'algorithmConfiguration needs algorigthm to be passed'
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const nonAlgoConfigTypes = ['layout'];
|
|
317
|
+
if (nonAlgoConfigTypes.includes(algorithmType)) {
|
|
318
|
+
errors.set(pathStr, {
|
|
319
|
+
path: pathStr,
|
|
320
|
+
message: `algorithmConfiguration is not applicable for '${algorithmType}' algorithm`
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const errorArray = Array.from(errors.values());
|
|
325
|
+
return errorArray.length > 0 ? errorArray : undefined;
|
|
252
326
|
}
|
|
253
327
|
export default validate;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/config",
|
|
3
|
-
"version": "1.31.0
|
|
3
|
+
"version": "1.31.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public",
|
|
12
|
-
"tag": "
|
|
12
|
+
"tag": "latest"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|
|
15
15
|
"node": ">=14"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"test:types": "tsd"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@percy/logger": "1.31.0
|
|
41
|
+
"@percy/logger": "1.31.0",
|
|
42
42
|
"ajv": "^8.6.2",
|
|
43
43
|
"cosmiconfig": "^8.0.0",
|
|
44
44
|
"yaml": "^2.0.0"
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"json-schema-typed": "^7.0.3"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "49895470c0dfa7242881db43e293317d1fb8f8b6"
|
|
50
50
|
}
|