@percy/core 1.32.0-beta.3 → 1.32.0-beta.4

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/api.js CHANGED
@@ -16,6 +16,7 @@ import { Readable } from 'stream';
16
16
  // This change ensures better compatibility and avoids relying on Node.js-specific APIs that might cause issues in ESM environments.
17
17
  import { fileURLToPath } from 'url';
18
18
  import { createRequire } from 'module';
19
+ import { configSchema } from './config.js';
19
20
  export const getPercyDomPath = url => {
20
21
  try {
21
22
  return createRequire(url).resolve('@percy/dom');
@@ -35,6 +36,57 @@ function encodeURLSearchParams(subj, prefix) {
35
36
  return typeof subj === 'object' ? Object.entries(subj).map(([key, value]) => encodeURLSearchParams(value, prefix ? `${prefix}[${key}]` : key)).join('&') : `${prefix}=${encodeURIComponent(subj)}`;
36
37
  }
37
38
 
39
+ // Walks the config schema and collects dot-paths of any fields marked `httpReadOnly: true`
40
+ // that are present in `body`. Driving this from the schema means new HTTP-blocked fields
41
+ // only need a one-line annotation next to their definition — no list to keep in sync here.
42
+ function findHttpReadOnlyPaths(body, schema, path = '') {
43
+ if (!body || typeof body !== 'object' || !(schema !== null && schema !== void 0 && schema.properties)) return [];
44
+ let paths = [];
45
+ for (let [key, propSchema] of Object.entries(schema.properties)) {
46
+ if (!Object.prototype.hasOwnProperty.call(body, key)) continue;
47
+ let childPath = path ? `${path}.${key}` : key;
48
+ if (propSchema !== null && propSchema !== void 0 && propSchema.httpReadOnly) {
49
+ paths.push(childPath);
50
+ } else {
51
+ paths.push(...findHttpReadOnlyPaths(body[key], propSchema, childPath));
52
+ }
53
+ }
54
+ return paths;
55
+ }
56
+
57
+ // Top-level configSchema is a map of subschemas keyed by top-level config namespace
58
+ // (`discovery`, `snapshot`, …). Wrap it as a single object schema so the walker can recurse
59
+ // uniformly from the root.
60
+ const ROOT_CONFIG_SCHEMA = {
61
+ type: 'object',
62
+ properties: configSchema
63
+ };
64
+
65
+ // Removes each dot-path's leaf from a deep clone of `body` and logs a warning per path.
66
+ // Returns the original `body` unchanged when `paths` is empty so we don't pay for a clone
67
+ // on every config request. Exported for unit testing: the `?.` chain in the reduce is a
68
+ // defensive guard for paths whose ancestor is absent from `body`. Through the production
69
+ // caller (stripBlockedConfigFields → findHttpReadOnlyPaths) every intermediate is verified
70
+ // present, so the guard is unreachable in normal use — but the explicit paths parameter
71
+ // lets a unit test exercise it without contorting the schema.
72
+ export function _applyHttpReadOnlyStripping(body, paths, log) {
73
+ if (!paths.length) return body;
74
+ let stripped = JSON.parse(JSON.stringify(body));
75
+ for (let p of paths) {
76
+ let parts = p.split('.');
77
+ let leaf = parts.pop();
78
+ let parent = parts.reduce((o, k) => o === null || o === void 0 ? void 0 : o[k], stripped);
79
+ if (parent && typeof parent === 'object') delete parent[leaf];
80
+ log.warn(`Ignoring \`${p}\` from /percy/config request: this field can only be set via the config file or CLI at startup.`);
81
+ }
82
+ return stripped;
83
+ }
84
+
85
+ // Returns a body with `httpReadOnly` fields removed. Caller guarantees `body` is truthy.
86
+ function stripBlockedConfigFields(body, log) {
87
+ return _applyHttpReadOnlyStripping(body, findHttpReadOnlyPaths(body, ROOT_CONFIG_SCHEMA), log);
88
+ }
89
+
38
90
  // Parse PNG IHDR chunk for the screenshot's actual rendered dimensions.
39
91
  // Returns { width, height } when the buffer is a valid PNG with non-zero
40
92
  // dimensions, or null otherwise (non-PNG signature, truncated file, zero
@@ -297,10 +349,13 @@ export function createPercyServer(percy, port) {
297
349
  });
298
350
  })
299
351
  // get or set config options
300
- .route(['get', 'post'], '/percy/config', async (req, res) => res.json(200, {
301
- config: req.body ? percy.set(req.body) : percy.config,
302
- success: true
303
- }))
352
+ .route(['get', 'post'], '/percy/config', async (req, res) => {
353
+ let body = req.body && stripBlockedConfigFields(req.body, logger('core:server'));
354
+ return res.json(200, {
355
+ config: body ? percy.set(body) : percy.config,
356
+ success: true
357
+ });
358
+ })
304
359
  // responds once idle (may take a long time)
305
360
  .route('get', '/percy/idle', async (req, res) => res.json(200, {
306
361
  success: await percy.idle().then(() => true)
package/dist/config.js CHANGED
@@ -597,8 +597,12 @@ export const configSchema = {
597
597
  type: 'object',
598
598
  additionalProperties: false,
599
599
  properties: {
600
+ // httpReadOnly: never settable over /percy/config — accepting these would let any
601
+ // local process execute arbitrary code as the Percy user via flags like
602
+ // --renderer-cmd-prefix / --gpu-launcher. Must be set via static config or CLI.
600
603
  executable: {
601
- type: 'string'
604
+ type: 'string',
605
+ httpReadOnly: true
602
606
  },
603
607
  timeout: {
604
608
  type: 'integer'
@@ -607,7 +611,8 @@ export const configSchema = {
607
611
  type: 'array',
608
612
  items: {
609
613
  type: 'string'
610
- }
614
+ },
615
+ httpReadOnly: true
611
616
  },
612
617
  headless: {
613
618
  type: 'boolean'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/core",
3
- "version": "1.32.0-beta.3",
3
+ "version": "1.32.0-beta.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,12 +46,12 @@
46
46
  "dependencies": {
47
47
  "@grpc/grpc-js": "^1.14.3",
48
48
  "@grpc/proto-loader": "^0.8.0",
49
- "@percy/client": "1.32.0-beta.3",
50
- "@percy/config": "1.32.0-beta.3",
51
- "@percy/dom": "1.32.0-beta.3",
52
- "@percy/logger": "1.32.0-beta.3",
53
- "@percy/monitoring": "1.32.0-beta.3",
54
- "@percy/webdriver-utils": "1.32.0-beta.3",
49
+ "@percy/client": "1.32.0-beta.4",
50
+ "@percy/config": "1.32.0-beta.4",
51
+ "@percy/dom": "1.32.0-beta.4",
52
+ "@percy/logger": "1.32.0-beta.4",
53
+ "@percy/monitoring": "1.32.0-beta.4",
54
+ "@percy/webdriver-utils": "1.32.0-beta.4",
55
55
  "busboy": "^1.6.0",
56
56
  "content-disposition": "^0.5.4",
57
57
  "cross-spawn": "^7.0.3",
@@ -67,7 +67,7 @@
67
67
  "yaml": "^2.4.1"
68
68
  },
69
69
  "optionalDependencies": {
70
- "@percy/cli-doctor": "1.32.0-beta.3"
70
+ "@percy/cli-doctor": "1.32.0-beta.4"
71
71
  },
72
- "gitHead": "7244de79269a8dddc4bd7883208bfbe4058fb382"
72
+ "gitHead": "2e5b1654806b609f961890f1a49fbc4154da1de2"
73
73
  }