-
Notifications
You must be signed in to change notification settings - Fork 129
Expand file tree
/
Copy pathautomation-utils.js
More file actions
297 lines (267 loc) · 9.22 KB
/
automation-utils.js
File metadata and controls
297 lines (267 loc) · 9.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
import fs from 'fs';
import path from 'path';
import { immutableJSONPatch } from 'immutable-json-patch';
/**
* Auto-approvable features configuration
* Defines which features can be auto-approved and their allowed paths
*/
export const AUTO_APPROVABLE_FEATURES = {
'/features/elementHiding': [
'/settings/domains',
'/exceptions',
],
'/features/fingerprintingTemporaryStorage': [
'/exceptions',
],
'/features/fingerprintingAudio': [
'/exceptions',
],
'/features/fingerprintingBattery': [
'/exceptions',
],
'/features/fingerprintingCanvas': [
'/exceptions',
],
'/features/fingerprintingHardware': [
'/exceptions',
],
'/features/fingerprintingScreenSize': [
'/exceptions',
],
'/features/trackerAllowlist': [
'/settings/allowlistedTrackers',
],
'/features/gpc': [
'/exceptions',
],
'/features/webCompat': [
'/exceptions',
],
'/features/clickToLoad': [
'/exceptions',
],
'/features/eme': [
'/exceptions',
],
'/features/autoconsent': [
'/exceptions',
'/settings/disabledCMPs',
],
'/features/customUserAgent': [
'/exceptions',
'/settings/ddgFixedSites',
'/settings/omitApplicationSites',
'/settings/defaultSites',
],
'/features/mediaPlaybackRequiresUserGesture': [
'/exceptions',
],
};
/**
* List of auto-approvable feature paths for summary generation
*/
export const AUTO_APPROVABLE_FEATURE_PATHS = Object.keys(AUTO_APPROVABLE_FEATURES);
/**
* Checks if a patch path is allowed for auto-approval
* @param {string} patchPath - The patch path to check
* @param {string} featurePath - The feature path this patch belongs to
* @returns {boolean} True if the path is allowed for auto-approval
*/
export function isPathAllowedForFeature(patchPath, featurePath) {
const allowedPaths = AUTO_APPROVABLE_FEATURES[featurePath];
if (!allowedPaths) {
return false;
}
// Use exact path matching or path starts with allowed path
return allowedPaths.some((allowedPath) => {
const fullAllowedPath = featurePath + allowedPath;
if (patchPath === fullAllowedPath || patchPath.startsWith(fullAllowedPath + '/')) {
return true;
}
return false;
});
}
/**
* Reads all files in a directory recursively and returns them as an object
* @param {string} directory - The directory path to read
* @returns {Object} Object with file paths as keys and file contents as values
*/
export function readFilesRecursively(directory) {
const filenames = fs.readdirSync(directory);
const files = {};
filenames.forEach((filename) => {
const filePath = path.join(directory, filename);
const fileStats = fs.statSync(filePath);
if (fileStats.isDirectory()) {
const nestedFiles = readFilesRecursively(filePath);
for (const [
nestedFilePath,
nestedFileContent,
] of Object.entries(nestedFiles)) {
files[path.join(filename, nestedFilePath)] = nestedFileContent;
}
} else {
files[filename] = fs.readFileSync(filePath, 'utf-8');
}
});
return files;
}
/**
* Removes superfluous info from the file contents to improve diff readability
* @param {string} fileContent - The raw file content
* @param {string} filePath - The file path (used to determine file type)
* @returns {string} The cleaned file content
*/
export function mungeFileContents(fileContent, filePath) {
if (filePath.endsWith('.json')) {
const fileJSON = JSON.parse(fileContent);
delete fileJSON.version;
if ('features' in fileJSON) {
for (const key of Object.keys(fileJSON.features)) {
if ('hash' in fileJSON.features[key]) {
delete fileJSON.features[key].hash;
}
}
}
return JSON.stringify(fileJSON, null, 4);
}
return fileContent;
}
/**
* Checks if changes are only to allowed paths in auto-approvable features
* @param {Array} patches - Array of JSON patches from fast-json-patch
* @returns {boolean} True if changes are only to allowed paths
*/
export function isAllowedChangesOnly(patches) {
// Check if all patches are for auto-approvable features and allowed paths
return patches.every((patch) => {
// Find which auto-approvable feature this patch belongs to
const featurePath = AUTO_APPROVABLE_FEATURE_PATHS.find((feature) => patch.path.startsWith(feature));
if (!featurePath) {
return false; // Not an auto-approvable feature
}
// Check if the path is in the allowed list for this feature
return isPathAllowedForFeature(patch.path, featurePath);
});
}
/**
* Analyzes patches to determine if they should be auto-approved
* @param {Array} patches - Array of JSON patches from fast-json-patch
* @returns {Object} Analysis result with approval status and reasoning
*/
export function analyzePatchesForApproval(patches) {
if (patches.length === 0) {
return {
shouldApprove: false,
reason: 'No changes detected',
};
}
// Check if changes are only to auto-approvable allowed paths
if (isAllowedChangesOnly(patches)) {
return {
shouldApprove: true,
reason: 'Auto-approved: Changes only to auto-approvable feature domains/exceptions',
};
}
// Check if any changes are outside allowed paths
const disallowedPatches = [];
for (const patch of patches) {
const featurePath = AUTO_APPROVABLE_FEATURE_PATHS.find((feature) => patch.path.startsWith(feature));
const isDisallowed = featurePath ? !isPathAllowedForFeature(patch.path, featurePath) : true;
if (isDisallowed) {
disallowedPatches.push(patch);
}
}
// This case covers changes to non-auto-approvable features
return {
shouldApprove: false,
reason: 'Manual review required: Changes to disallowed paths',
disallowedPatches,
};
}
/**
* Generates a summary of changes for reporting
* @param {Array} patches - Array of JSON patches from fast-json-patch
* @returns {Object} Summary of changes by operation type and path
*/
export function generateChangeSummary(patches) {
const summary = {
total: patches.length,
byOperation: {},
byPath: {},
autoApprovableChanges: 0,
otherChanges: 0,
};
patches.forEach((patch) => {
// Count by operation
summary.byOperation[patch.op] = (summary.byOperation[patch.op] || 0) + 1;
// Count by path
const pathKey = patch.path.split('/').slice(0, 3).join('/'); // Top 3 levels
summary.byPath[pathKey] = (summary.byPath[pathKey] || 0) + 1;
// Count auto-approvable vs other changes
const featurePath = AUTO_APPROVABLE_FEATURE_PATHS.find((feature) => patch.path.startsWith(feature));
if (featurePath && isPathAllowedForFeature(patch.path, featurePath)) {
summary.autoApprovableChanges++;
} else {
summary.otherChanges++;
}
});
return summary;
}
/**
* Checks if a feature has conditionalChanges
* @param {Object} feature - The feature object to check
* @returns {boolean} True if the feature has conditionalChanges
*/
export function hasConditionalChanges(feature) {
return !!feature?.settings?.conditionalChanges;
}
/**
* Applies conditionalChanges patches to feature settings
* @param {Object} feature - The feature object containing settings and conditionalChanges
* @returns {Object|false} The feature settings after applying all conditionalChanges patches, or false on error
*/
export function applyConditionalChanges(feature) {
if (!hasConditionalChanges(feature)) {
return feature.settings;
}
let patchedSettings = feature.settings;
for (const change of feature.settings.conditionalChanges) {
if (change.patchSettings) {
try {
patchedSettings = immutableJSONPatch(patchedSettings, change.patchSettings);
} catch (error) {
console.warn(`Failed to apply conditionalChanges patch: ${error.message}`);
return false;
}
}
}
return patchedSettings;
}
/**
* Applies conditionalChanges patches to all features in a config object
* @param {Object} config - The config object containing features
* @returns {Object|false} The config object with all conditionalChanges patches applied, or false on error
*/
export function applyConditionalChangesToConfig(config) {
if (!config?.features) {
return config;
}
const patchedConfig = JSON.parse(JSON.stringify(config));
for (const [
featureName,
feature,
] of Object.entries(patchedConfig.features)) {
if (hasConditionalChanges(feature)) {
const patchedSettings = applyConditionalChanges(feature);
if (patchedSettings === false) {
return false;
}
patchedConfig.features[featureName] = {
...feature,
settings: patchedSettings,
};
}
}
return patchedConfig;
}