@mnemonik/shared 7.143.0 → 7.144.1
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/ast/astChunker.d.ts.map +1 -1
- package/dist/ast/astChunker.js +6 -1
- package/dist/ast/astChunker.js.map +1 -1
- package/dist/ast/grammars.d.ts +4 -2
- package/dist/ast/grammars.d.ts.map +1 -1
- package/dist/ast/grammars.js +37 -0
- package/dist/ast/grammars.js.map +1 -1
- package/dist/codeScanner.d.ts +3 -0
- package/dist/codeScanner.d.ts.map +1 -1
- package/dist/codeScanner.js +7 -0
- package/dist/codeScanner.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/sourceRedaction.d.ts.map +1 -1
- package/dist/sourceRedaction.js +667 -37
- package/dist/sourceRedaction.js.map +1 -1
- package/dist/vueSource.d.ts +17 -0
- package/dist/vueSource.d.ts.map +1 -0
- package/dist/vueSource.js +63 -0
- package/dist/vueSource.js.map +1 -0
- package/package.json +3 -1
- package/queries/cmake.tags.scm +6 -0
- package/queries/nix.tags.scm +4 -0
- package/queries/objcpp.tags.scm +4 -0
- package/queries/terraform.tags.scm +8 -0
- package/scripts/vendor-grammars.ts +468 -40
- package/src/ast/astChunker.ts +10 -1
- package/src/ast/grammars.ts +34 -1
- package/src/codeScanner.ts +6 -0
- package/src/index.ts +5 -0
- package/src/sourceRedaction.ts +726 -37
- package/src/vueSource.ts +77 -0
- package/wasm/bash.wasm +0 -0
- package/wasm/cmake.LICENSE +21 -0
- package/wasm/cmake.tags.scm +6 -0
- package/wasm/cmake.wasm +0 -0
- package/wasm/cpp.LICENSE +21 -0
- package/wasm/cpp.wasm +0 -0
- package/wasm/groovy.LICENSE +25 -0
- package/wasm/groovy.wasm +0 -0
- package/wasm/nix.LICENSE +21 -0
- package/wasm/nix.tags.scm +4 -0
- package/wasm/nix.wasm +0 -0
- package/wasm/objcpp.LICENSE +28 -0
- package/wasm/objcpp.tags.scm +4 -0
- package/wasm/objcpp.wasm +0 -0
- package/wasm/sql.wasm +0 -0
- package/wasm/terraform.LICENSE +201 -0
- package/wasm/terraform.tags.scm +8 -0
- package/wasm/terraform.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- package/wasm/typescript.wasm +0 -0
package/src/sourceRedaction.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseVueSource, walkVueTemplate, VUE_FIDELITY_MARKER } from './vueSource.js';
|
|
2
|
+
import { basename, extname } from 'node:path';
|
|
2
3
|
import { Parser, type Node, type Tree } from 'web-tree-sitter';
|
|
3
|
-
import { loadGrammar } from './ast/grammars.js';
|
|
4
|
+
import { loadGrammar, objcppSyntaxIssue } from './ast/grammars.js';
|
|
4
5
|
import { MAX_AST_PARSE_BYTES } from './ast/astChunker.js';
|
|
5
6
|
import { commandLineCredentialRanges, redactCredentials } from './redactCredentials.js';
|
|
6
7
|
import {
|
|
@@ -17,60 +18,586 @@ function scrubPayload(text: string): string {
|
|
|
17
18
|
/** Source syntax stays executable; unsupported or incomplete input uses the conservative scrubber. */
|
|
18
19
|
export async function scrubSourceSecrets(text: string, filePath: string): Promise<string> {
|
|
19
20
|
const extension = extname(filePath).toLowerCase();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
? '
|
|
24
|
-
: ['.
|
|
25
|
-
? '
|
|
26
|
-
:
|
|
27
|
-
? '
|
|
28
|
-
: ['.
|
|
29
|
-
? '
|
|
30
|
-
:
|
|
31
|
-
? '
|
|
32
|
-
:
|
|
33
|
-
? '
|
|
34
|
-
: extension === '.
|
|
35
|
-
? '
|
|
36
|
-
: extension === '.
|
|
37
|
-
? '
|
|
38
|
-
:
|
|
21
|
+
if (extension === '.vue') return scrubVueSource(text);
|
|
22
|
+
const language =
|
|
23
|
+
extension === '.cmake' || basename(filePath) === 'CMakeLists.txt'
|
|
24
|
+
? 'cmake'
|
|
25
|
+
: ['.ts', '.mts', '.cts'].includes(extension)
|
|
26
|
+
? 'typescript'
|
|
27
|
+
: extension === '.tsx'
|
|
28
|
+
? 'tsx'
|
|
29
|
+
: ['.js', '.jsx', '.mjs', '.cjs'].includes(extension)
|
|
30
|
+
? 'javascript'
|
|
31
|
+
: ['.py', '.pyi'].includes(extension)
|
|
32
|
+
? 'python'
|
|
33
|
+
: ['.sh', '.bash', '.zsh'].includes(extension)
|
|
34
|
+
? 'bash'
|
|
35
|
+
: extension === '.swift'
|
|
36
|
+
? 'swift'
|
|
37
|
+
: extension === '.sql'
|
|
38
|
+
? 'sql'
|
|
39
|
+
: extension === '.lua'
|
|
40
|
+
? 'lua'
|
|
41
|
+
: extension === '.rs'
|
|
42
|
+
? 'rust'
|
|
43
|
+
: extension === '.nix'
|
|
44
|
+
? 'nix'
|
|
45
|
+
: extension === '.tf'
|
|
46
|
+
? 'terraform'
|
|
47
|
+
: ['.m', '.mm'].includes(extension)
|
|
48
|
+
? 'objcpp'
|
|
49
|
+
: extension === '.c'
|
|
50
|
+
? 'c'
|
|
51
|
+
: ['.h', '.cpp', '.cc', '.cxx', '.hpp', '.hh', '.hxx'].includes(
|
|
52
|
+
extension
|
|
53
|
+
)
|
|
54
|
+
? 'cpp'
|
|
55
|
+
: ['.groovy', '.gradle'].includes(extension)
|
|
56
|
+
? 'groovy'
|
|
57
|
+
: ['.ps1', '.psm1'].includes(extension)
|
|
58
|
+
? 'powershell'
|
|
59
|
+
: null;
|
|
60
|
+
const fallback = (): string => {
|
|
61
|
+
const clean = scrubPayload(text);
|
|
62
|
+
if (!text) return clean;
|
|
63
|
+
if (language === 'powershell') {
|
|
64
|
+
const marker = '# mnemonik:powershell-source-fidelity-unavailable\n';
|
|
65
|
+
return clean.startsWith(marker) ? clean : marker + clean;
|
|
66
|
+
}
|
|
67
|
+
// A fallback may repair syntax accidentally; these graph extractors must never trust it.
|
|
68
|
+
if (language === 'cmake' || language === 'groovy') {
|
|
69
|
+
const marker = `// mnemonik:${language}-source-fidelity-unavailable`;
|
|
70
|
+
const comment = language === 'cmake' ? marker.replace('//', '#') : marker;
|
|
71
|
+
return clean.startsWith(comment) ? clean : comment + '\n' + clean;
|
|
72
|
+
}
|
|
73
|
+
const marker =
|
|
74
|
+
language === 'nix'
|
|
75
|
+
? '/* mnemonik:nix-source-fidelity-unavailable */'
|
|
76
|
+
: '/* mnemonik:terraform-source-fidelity-unavailable */';
|
|
77
|
+
return (language === 'terraform' || language === 'nix') && !clean.startsWith(marker)
|
|
78
|
+
? marker + clean
|
|
79
|
+
: clean;
|
|
80
|
+
};
|
|
39
81
|
if (!text || !language || Buffer.byteLength(text, 'utf8') > MAX_AST_PARSE_BYTES) {
|
|
40
|
-
return
|
|
82
|
+
return fallback();
|
|
41
83
|
}
|
|
42
84
|
let parser: Parser | undefined;
|
|
43
85
|
let tree: Tree | null = null;
|
|
44
86
|
try {
|
|
45
87
|
const grammar = await loadGrammar(language);
|
|
46
|
-
if ('unavailable' in grammar) return
|
|
88
|
+
if ('unavailable' in grammar) return fallback();
|
|
47
89
|
parser = new Parser();
|
|
48
90
|
parser.setLanguage(grammar.ok.language);
|
|
49
91
|
tree = parser.parse(text);
|
|
50
|
-
if (!tree || tree.rootNode.hasError) return
|
|
92
|
+
if (!tree || tree.rootNode.hasError) return fallback();
|
|
51
93
|
const root = tree.rootNode;
|
|
94
|
+
if (language === 'objcpp' && objcppSyntaxIssue(root)) return fallback();
|
|
52
95
|
|
|
53
96
|
const redacted =
|
|
54
|
-
language === '
|
|
55
|
-
?
|
|
56
|
-
: language === '
|
|
57
|
-
?
|
|
58
|
-
: language === '
|
|
59
|
-
?
|
|
60
|
-
: language === '
|
|
61
|
-
?
|
|
62
|
-
: language === '
|
|
63
|
-
?
|
|
64
|
-
:
|
|
97
|
+
language === 'cmake'
|
|
98
|
+
? redactCmakeNode(root)
|
|
99
|
+
: language === 'bash'
|
|
100
|
+
? redactShellNode(root, commandLineCredentialRanges(text))
|
|
101
|
+
: language === 'swift'
|
|
102
|
+
? redactSwiftNode(root)
|
|
103
|
+
: language === 'sql'
|
|
104
|
+
? redactSqlNode(root)
|
|
105
|
+
: language === 'lua'
|
|
106
|
+
? redactLuaNode(root)
|
|
107
|
+
: language === 'rust'
|
|
108
|
+
? redactRustNode(root)
|
|
109
|
+
: language === 'nix'
|
|
110
|
+
? redactNixNode(root)
|
|
111
|
+
: language === 'terraform'
|
|
112
|
+
? redactTerraformNode(root)
|
|
113
|
+
: ['objcpp', 'c', 'cpp'].includes(language)
|
|
114
|
+
? redactCFamilyNode(root)
|
|
115
|
+
: language === 'groovy'
|
|
116
|
+
? redactGroovyNode(root)
|
|
117
|
+
: language === 'powershell'
|
|
118
|
+
? redactPowershellNode(root)
|
|
119
|
+
: redactNode(root);
|
|
65
120
|
return text.slice(0, root.startIndex) + redacted + text.slice(root.endIndex);
|
|
66
121
|
} catch {
|
|
67
|
-
return
|
|
122
|
+
return fallback();
|
|
68
123
|
} finally {
|
|
69
124
|
tree?.delete();
|
|
70
125
|
parser?.delete();
|
|
71
126
|
}
|
|
72
127
|
}
|
|
73
128
|
|
|
129
|
+
/** CMake substitutions remain source expressions; only AST argument/comment payloads are scrubbed. */
|
|
130
|
+
function redactCmakeNode(node: Node, sensitive = false): string {
|
|
131
|
+
const original = node.text;
|
|
132
|
+
const type = node.type;
|
|
133
|
+
const payload = (value: string): string =>
|
|
134
|
+
sensitive && value.trim() ? redactSecretValue(value) : literalPayload(value, false);
|
|
135
|
+
if (type === 'line_comment') return '#' + scrubPayload(original.slice(1));
|
|
136
|
+
if (type === 'bracket_comment' || type === 'bracket_argument') {
|
|
137
|
+
const payload = node.namedChildren.find((n) => n.type.endsWith('_content'));
|
|
138
|
+
if (!payload) return original;
|
|
139
|
+
const clean =
|
|
140
|
+
type === 'bracket_comment'
|
|
141
|
+
? scrubPayload(payload.text)
|
|
142
|
+
: sensitive
|
|
143
|
+
? redactSecretValue(payload.text)
|
|
144
|
+
: literalPayload(payload.text, false);
|
|
145
|
+
return (
|
|
146
|
+
original.slice(0, payload.startIndex - node.startIndex) +
|
|
147
|
+
clean +
|
|
148
|
+
original.slice(payload.endIndex - node.startIndex)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (type === 'variable_ref') return original;
|
|
152
|
+
if (type === 'escape_sequence') return sensitive ? redactSecretValue(original) : original;
|
|
153
|
+
if (type === 'quoted_element' || type === 'unquoted_argument') {
|
|
154
|
+
// Generator expressions have no inner AST in this grammar; withhold fidelity instead of rewriting them.
|
|
155
|
+
if (original.includes('$<')) throw new Error('Opaque CMake generator expression');
|
|
156
|
+
let result = '';
|
|
157
|
+
let offset = node.startIndex;
|
|
158
|
+
for (const child of node.namedChildren) {
|
|
159
|
+
result += payload(
|
|
160
|
+
original.slice(offset - node.startIndex, child.startIndex - node.startIndex)
|
|
161
|
+
);
|
|
162
|
+
result += redactCmakeNode(child, sensitive);
|
|
163
|
+
offset = child.endIndex;
|
|
164
|
+
}
|
|
165
|
+
result += payload(original.slice(offset - node.startIndex));
|
|
166
|
+
return type === 'unquoted_argument' && result !== original ? '"' + result + '"' : result;
|
|
167
|
+
}
|
|
168
|
+
const args =
|
|
169
|
+
type === 'argument_list' ? node.namedChildren.filter((n) => n.type === 'argument') : [];
|
|
170
|
+
const set =
|
|
171
|
+
node.parent?.namedChildren.find((n) => n.type === 'identifier')?.text.toLowerCase() === 'set';
|
|
172
|
+
const key = set ? args[0] : null;
|
|
173
|
+
const label = key?.firstNamedChild;
|
|
174
|
+
const literalKey =
|
|
175
|
+
label?.type === 'unquoted_argument' && !label.namedChildCount
|
|
176
|
+
? label.text
|
|
177
|
+
: label?.type === 'quoted_argument' && !label.firstNamedChild?.namedChildCount
|
|
178
|
+
? label.text.slice(1, -1)
|
|
179
|
+
: null;
|
|
180
|
+
const name = literalKey?.match(/^ENV\{([^{}]+)\}$/)?.[1] ?? literalKey;
|
|
181
|
+
const labeled = name !== null && SECRET_LABEL_RE.test(name);
|
|
182
|
+
let result = '';
|
|
183
|
+
let offset = node.startIndex;
|
|
184
|
+
for (const child of node.namedChildren) {
|
|
185
|
+
result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
|
|
186
|
+
result +=
|
|
187
|
+
child.id === key?.id || (type === 'normal_command' && child.type === 'identifier')
|
|
188
|
+
? child.text
|
|
189
|
+
: redactCmakeNode(child, sensitive || (labeled && args.some((a) => a.id === child.id)));
|
|
190
|
+
offset = child.endIndex;
|
|
191
|
+
}
|
|
192
|
+
return result + original.slice(offset - node.startIndex);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** PowerShell's AST separates command syntax from literal strings and interpolation nodes. */
|
|
196
|
+
function redactPowershellNode(node: Node, sensitive = false): string {
|
|
197
|
+
const original = node.text;
|
|
198
|
+
const type = node.type;
|
|
199
|
+
if (type === 'comment')
|
|
200
|
+
return original.startsWith('<#')
|
|
201
|
+
? '<#' + scrubPayload(original.slice(2, -2)) + '#>'
|
|
202
|
+
: '#' + scrubPayload(original.slice(1));
|
|
203
|
+
if (type === 'string_literal') {
|
|
204
|
+
const here = original.startsWith('@');
|
|
205
|
+
const prefix = here ? 2 : 1;
|
|
206
|
+
const expandable = node.firstNamedChild;
|
|
207
|
+
const substitutions = expandable?.type.startsWith('expandable_')
|
|
208
|
+
? expandable.namedChildren
|
|
209
|
+
: [];
|
|
210
|
+
const payload = (text: string): string =>
|
|
211
|
+
text
|
|
212
|
+
// Retain quote/dollar/control escapes and physical continuation boundaries verbatim.
|
|
213
|
+
.split(/(`(?:\r\n|[0abefnrtv`"'$\r\n])|''|\r\n|[\r\n])/g)
|
|
214
|
+
.map((part, index) =>
|
|
215
|
+
index % 2
|
|
216
|
+
? part
|
|
217
|
+
: sensitive && part
|
|
218
|
+
? redactSecretValue(part)
|
|
219
|
+
: literalPayload(part, false)
|
|
220
|
+
)
|
|
221
|
+
.join('');
|
|
222
|
+
let result = original.slice(0, prefix);
|
|
223
|
+
let offset = node.startIndex + prefix;
|
|
224
|
+
for (const child of substitutions) {
|
|
225
|
+
result += payload(
|
|
226
|
+
original.slice(offset - node.startIndex, child.startIndex - node.startIndex)
|
|
227
|
+
);
|
|
228
|
+
result += redactPowershellNode(child); // Runtime references/subexpressions are not credential values.
|
|
229
|
+
offset = child.endIndex;
|
|
230
|
+
}
|
|
231
|
+
return (
|
|
232
|
+
result + payload(original.slice(offset - node.startIndex, -prefix)) + original.slice(-prefix)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (sensitive && type === 'generic_token') return literalPayload(original, true);
|
|
236
|
+
if (sensitive && ['integer_literal', 'real_literal'].includes(type))
|
|
237
|
+
return "'" + redactSecretValue(original) + "'";
|
|
238
|
+
const assignment = type === 'assignment_expression';
|
|
239
|
+
const key = assignment || type === 'hash_entry' ? node.firstNamedChild : null;
|
|
240
|
+
const value = assignment
|
|
241
|
+
? node.childForFieldName('value')
|
|
242
|
+
: type === 'hash_entry'
|
|
243
|
+
? node.namedChildren[1]
|
|
244
|
+
: null;
|
|
245
|
+
let label = key;
|
|
246
|
+
while (label && label.type !== 'string_literal') {
|
|
247
|
+
if (label.type === 'element_access' || label.namedChildCount === 1)
|
|
248
|
+
label = label.firstNamedChild;
|
|
249
|
+
else break;
|
|
250
|
+
}
|
|
251
|
+
if (label?.type === 'member_access')
|
|
252
|
+
label = label.lastNamedChild?.firstNamedChild ?? label.lastNamedChild;
|
|
253
|
+
const labelText = label?.type === 'string_literal' ? label.text.slice(1, -1) : label?.text;
|
|
254
|
+
const labeled =
|
|
255
|
+
!!labelText &&
|
|
256
|
+
SECRET_LABEL_RE.test(
|
|
257
|
+
labelText
|
|
258
|
+
.replace(/^\$\{?|}$/g, '')
|
|
259
|
+
.split(':')
|
|
260
|
+
.at(-1) ?? ''
|
|
261
|
+
);
|
|
262
|
+
const valueWrapper =
|
|
263
|
+
sensitive &&
|
|
264
|
+
(type === 'pipeline' ||
|
|
265
|
+
type === 'pipeline_chain' ||
|
|
266
|
+
type === 'statement_list' ||
|
|
267
|
+
type === 'hash_literal_body' ||
|
|
268
|
+
type === 'expression_with_unary_operator' ||
|
|
269
|
+
(type.endsWith('_expression') &&
|
|
270
|
+
![
|
|
271
|
+
'sub_expression',
|
|
272
|
+
'member_access',
|
|
273
|
+
'invokation_expression',
|
|
274
|
+
'script_block_expression',
|
|
275
|
+
].includes(type)));
|
|
276
|
+
let result = '';
|
|
277
|
+
let offset = node.startIndex;
|
|
278
|
+
let argumentSensitive = false;
|
|
279
|
+
for (const child of node.namedChildren) {
|
|
280
|
+
result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
|
|
281
|
+
if (type === 'command_elements' && child.type === 'command_parameter')
|
|
282
|
+
argumentSensitive = SECRET_LABEL_RE.test(child.text.replace(/^-|:$/g, ''));
|
|
283
|
+
const argument =
|
|
284
|
+
type === 'command_elements' &&
|
|
285
|
+
!['command_parameter', 'command_argument_sep'].includes(child.type);
|
|
286
|
+
result += redactPowershellNode(
|
|
287
|
+
child,
|
|
288
|
+
valueWrapper ||
|
|
289
|
+
((labeled || (sensitive && type === 'hash_entry')) && child.id === value?.id) ||
|
|
290
|
+
(argument && argumentSensitive)
|
|
291
|
+
);
|
|
292
|
+
if (argument) argumentSensitive = false;
|
|
293
|
+
offset = child.endIndex;
|
|
294
|
+
}
|
|
295
|
+
return result + original.slice(offset - node.startIndex);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Nix literal payloads have spans; lambda bodies and application arguments remain runtime code. */
|
|
299
|
+
function redactNixNode(node: Node, sensitive = false): string {
|
|
300
|
+
const type = node.type;
|
|
301
|
+
const original = node.text;
|
|
302
|
+
if (type === 'comment') return redactNode(node);
|
|
303
|
+
if (type === 'string_fragment')
|
|
304
|
+
return sensitive ? redactSecretValue(original) : literalPayload(original, false);
|
|
305
|
+
if (type === 'escape_sequence' || type === 'dollar_escape') return original;
|
|
306
|
+
if (
|
|
307
|
+
(type === 'integer_expression' ||
|
|
308
|
+
type === 'float_expression' ||
|
|
309
|
+
(type === 'unary_expression' &&
|
|
310
|
+
['integer_expression', 'float_expression'].includes(
|
|
311
|
+
node.childForFieldName('argument')?.type ?? ''
|
|
312
|
+
))) &&
|
|
313
|
+
sensitive
|
|
314
|
+
)
|
|
315
|
+
return '"' + redactSecretValue(original) + '"';
|
|
316
|
+
if (type === 'uri_expression') {
|
|
317
|
+
const clean = literalPayload(original, sensitive);
|
|
318
|
+
return clean === original ? original : JSON.stringify(clean);
|
|
319
|
+
}
|
|
320
|
+
if (
|
|
321
|
+
['path_expression', 'hpath_expression', 'spath_expression'].includes(type) &&
|
|
322
|
+
(sensitive || scrubPayload(original) !== original)
|
|
323
|
+
)
|
|
324
|
+
throw new Error('Nix path fidelity unavailable');
|
|
325
|
+
const key = type === 'binding' ? node.childForFieldName('attrpath') : null;
|
|
326
|
+
const name = key?.lastNamedChild ?? (type === 'formal' ? node.childForFieldName('name') : null);
|
|
327
|
+
const labeled =
|
|
328
|
+
name?.type === 'identifier'
|
|
329
|
+
? secretLabel(name)
|
|
330
|
+
: name?.type === 'string_expression' &&
|
|
331
|
+
!name.namedChildren.some((n) => n.type === 'interpolation')
|
|
332
|
+
? SECRET_LABEL_RE.test(name.text.slice(1, -1))
|
|
333
|
+
: false;
|
|
334
|
+
const value = node.childForFieldName(type === 'formal' ? 'default' : 'expression');
|
|
335
|
+
let result = '';
|
|
336
|
+
let offset = node.startIndex;
|
|
337
|
+
let escapedDollar = false;
|
|
338
|
+
for (const child of node.namedChildren) {
|
|
339
|
+
result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
|
|
340
|
+
const carry =
|
|
341
|
+
sensitive &&
|
|
342
|
+
([
|
|
343
|
+
'string_expression',
|
|
344
|
+
'indented_string_expression',
|
|
345
|
+
'list_expression',
|
|
346
|
+
'parenthesized_expression',
|
|
347
|
+
'attrset_expression',
|
|
348
|
+
'rec_attrset_expression',
|
|
349
|
+
'binding_set',
|
|
350
|
+
].includes(type) ||
|
|
351
|
+
(type === 'binding' && child.id === value?.id) ||
|
|
352
|
+
(['let_expression', 'with_expression', 'assert_expression'].includes(type) &&
|
|
353
|
+
child.id === node.childForFieldName('body')?.id) ||
|
|
354
|
+
(type === 'interpolation' && child.id === node.childForFieldName('expression')?.id) ||
|
|
355
|
+
(type === 'unary_expression' && child.id === node.childForFieldName('argument')?.id) ||
|
|
356
|
+
(type === 'if_expression' && child.id !== node.childForFieldName('condition')?.id) ||
|
|
357
|
+
(type === 'binary_expression' &&
|
|
358
|
+
['+', '++'].includes(node.childForFieldName('operator')?.text ?? '')) ||
|
|
359
|
+
(type === 'apply_expression' && child.id === node.childForFieldName('function')?.id) ||
|
|
360
|
+
(type === 'select_expression' && child.type !== 'attrpath'));
|
|
361
|
+
const structural =
|
|
362
|
+
child.id === key?.id ||
|
|
363
|
+
(escapedDollar && child.type === 'string_fragment' && child.text === '$');
|
|
364
|
+
result += structural
|
|
365
|
+
? child.text
|
|
366
|
+
: redactNixNode(child, carry || (labeled && child.id === value?.id));
|
|
367
|
+
escapedDollar = child.type === 'dollar_escape';
|
|
368
|
+
offset = child.endIndex;
|
|
369
|
+
}
|
|
370
|
+
return result + original.slice(offset - node.startIndex);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** HCL exposes literal spans separately from interpolation/directive expressions. */
|
|
374
|
+
function redactTerraformNode(node: Node, sensitive = false): string {
|
|
375
|
+
const original = node.text;
|
|
376
|
+
const type = node.type;
|
|
377
|
+
if (type === 'comment') return redactNode(node);
|
|
378
|
+
// Resource labels and object keys are identifiers, even when the AST calls them strings.
|
|
379
|
+
if (type === 'block') sensitive = false;
|
|
380
|
+
if (type === 'template_literal') {
|
|
381
|
+
if (!sensitive) return literalPayload(original, false);
|
|
382
|
+
// Escape tokens remain literal; scrubbing must not turn $${ into an interpolation.
|
|
383
|
+
return original
|
|
384
|
+
.split(/(\\["\\nrt]|\$\$\{|%%\{|[{}])/g)
|
|
385
|
+
.map((part, index) => (index % 2 ? part : literalPayload(part, sensitive)))
|
|
386
|
+
.join('');
|
|
387
|
+
}
|
|
388
|
+
if (type === 'numeric_lit' && sensitive) return '"' + redactSecretValue(original) + '"';
|
|
389
|
+
const key = type === 'attribute' ? node.firstNamedChild : node.childForFieldName('key');
|
|
390
|
+
const value = type === 'attribute' ? node.namedChildren[1] : node.childForFieldName('val');
|
|
391
|
+
const keyName = key?.type === 'expression' ? key.firstNamedChild : key;
|
|
392
|
+
const literalKey = keyName?.type === 'literal_value' ? keyName.firstNamedChild : keyName;
|
|
393
|
+
const container = type === 'attribute' ? node.parent?.parent : null;
|
|
394
|
+
const containerName = container?.namedChildren[1];
|
|
395
|
+
const secretDefault =
|
|
396
|
+
container?.type === 'block' &&
|
|
397
|
+
((container.firstNamedChild?.text === 'variable' && key?.text === 'default') ||
|
|
398
|
+
(container.firstNamedChild?.text === 'output' && key?.text === 'value')) &&
|
|
399
|
+
containerName?.type === 'string_lit' &&
|
|
400
|
+
SECRET_LABEL_RE.test(containerName.text.slice(1, -1));
|
|
401
|
+
const labeled =
|
|
402
|
+
secretDefault ||
|
|
403
|
+
(literalKey?.type === 'string_lit'
|
|
404
|
+
? SECRET_LABEL_RE.test(literalKey.text.slice(1, -1))
|
|
405
|
+
: secretLabel(literalKey ?? null));
|
|
406
|
+
let result = '';
|
|
407
|
+
let offset = node.startIndex;
|
|
408
|
+
for (const child of node.namedChildren) {
|
|
409
|
+
result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
|
|
410
|
+
const carry =
|
|
411
|
+
sensitive &&
|
|
412
|
+
([
|
|
413
|
+
'expression',
|
|
414
|
+
'operation',
|
|
415
|
+
'unary_operation',
|
|
416
|
+
'binary_operation',
|
|
417
|
+
'literal_value',
|
|
418
|
+
'template_expr',
|
|
419
|
+
'string_lit',
|
|
420
|
+
'quoted_template',
|
|
421
|
+
'heredoc_template',
|
|
422
|
+
'collection_value',
|
|
423
|
+
'tuple',
|
|
424
|
+
'object',
|
|
425
|
+
'template_directive',
|
|
426
|
+
].includes(type) ||
|
|
427
|
+
(['template_for', 'template_if'].includes(type) &&
|
|
428
|
+
![
|
|
429
|
+
'template_for_start',
|
|
430
|
+
'template_for_end',
|
|
431
|
+
'template_if_intro',
|
|
432
|
+
'template_else_intro',
|
|
433
|
+
'template_if_end',
|
|
434
|
+
].includes(child.type)) ||
|
|
435
|
+
(type === 'conditional' && child.id !== node.firstNamedChild?.id) ||
|
|
436
|
+
(type === 'object_elem' && child.id === value?.id));
|
|
437
|
+
// Block labels and object keys are never literal values to scrub.
|
|
438
|
+
result +=
|
|
439
|
+
(type === 'block' && child.type === 'string_lit') || child.id === key?.id
|
|
440
|
+
? child.text
|
|
441
|
+
: redactTerraformNode(child, carry || (labeled && child.id === value?.id));
|
|
442
|
+
offset = child.endIndex;
|
|
443
|
+
}
|
|
444
|
+
return result + original.slice(offset - node.startIndex);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Groovy's quoted strings and comments have spans; command calls remain executable syntax. */
|
|
448
|
+
function redactGroovyNode(node: Node, sensitive = false): string {
|
|
449
|
+
const original = node.text;
|
|
450
|
+
const type = node.type;
|
|
451
|
+
if (type === 'line_comment') return '//' + scrubPayload(original.slice(2));
|
|
452
|
+
if (type === 'block_comment') return '/*' + scrubPayload(original.slice(2, -2)) + '*/';
|
|
453
|
+
const operand = type === 'unary_expression' ? node.childForFieldName('operand') : null;
|
|
454
|
+
const numeric = (kind: string): boolean =>
|
|
455
|
+
kind.endsWith('_integer_literal') || kind.endsWith('_floating_point_literal');
|
|
456
|
+
if (sensitive && (numeric(type) || (operand && numeric(operand.type))))
|
|
457
|
+
return '"' + redactSecretValue(original) + '"';
|
|
458
|
+
if (type === 'character_literal' || type === 'string_literal') {
|
|
459
|
+
const delimiter = type === 'character_literal' ? "'" : node.firstChild?.text;
|
|
460
|
+
if (!delimiter) throw new Error('Incomplete Groovy string');
|
|
461
|
+
// The inherited Java rule leaves Groovy GString expressions opaque, not AST substitutions.
|
|
462
|
+
if (delimiter.startsWith('"') && original.includes('$'))
|
|
463
|
+
throw new Error('Opaque Groovy GString interpolation');
|
|
464
|
+
return (
|
|
465
|
+
delimiter +
|
|
466
|
+
literalPayload(original.slice(delimiter.length, -delimiter.length), sensitive) +
|
|
467
|
+
delimiter
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
const key =
|
|
471
|
+
type === 'variable_declarator'
|
|
472
|
+
? node.childForFieldName('name')
|
|
473
|
+
: type === 'assignment_expression'
|
|
474
|
+
? node.childForFieldName('left')
|
|
475
|
+
: type === 'map_item'
|
|
476
|
+
? node.childForFieldName('key')
|
|
477
|
+
: null;
|
|
478
|
+
const value = node.childForFieldName(type === 'assignment_expression' ? 'right' : 'value');
|
|
479
|
+
const field = key?.childForFieldName('field') ?? key;
|
|
480
|
+
const name =
|
|
481
|
+
field && ['character_literal', 'string_literal'].includes(field.type)
|
|
482
|
+
? field.text.slice(1, -1)
|
|
483
|
+
: field?.text;
|
|
484
|
+
const labeled = !!name && SECRET_LABEL_RE.test(name);
|
|
485
|
+
let result = '';
|
|
486
|
+
let offset = node.startIndex;
|
|
487
|
+
for (const child of node.namedChildren) {
|
|
488
|
+
result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
|
|
489
|
+
const carry =
|
|
490
|
+
sensitive &&
|
|
491
|
+
(type === 'parenthesized_expression' ||
|
|
492
|
+
type === 'array_literal' ||
|
|
493
|
+
type === 'map_literal' ||
|
|
494
|
+
(type === 'map_item' && child.id === value?.id) ||
|
|
495
|
+
(type === 'method_invocation' && child.id === node.childForFieldName('object')?.id) ||
|
|
496
|
+
(type === 'field_access' && child.id === node.childForFieldName('object')?.id) ||
|
|
497
|
+
(type === 'juxt_function_call' && child.id === node.childForFieldName('name')?.id) ||
|
|
498
|
+
(type === 'binary_expression' && node.childForFieldName('operator')?.text === '+') ||
|
|
499
|
+
(type === 'ternary_expression' && child.id !== node.childForFieldName('condition')?.id));
|
|
500
|
+
result += redactGroovyNode(child, carry || (labeled && child.id === value?.id));
|
|
501
|
+
offset = child.endIndex;
|
|
502
|
+
}
|
|
503
|
+
return result + original.slice(offset - node.startIndex);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Keep C/Objective-C++ prefixes, raw delimiters and runtime expressions outside literal payloads. */
|
|
507
|
+
function redactCFamilyNode(node: Node, sensitive = false): string {
|
|
508
|
+
const original = node.text;
|
|
509
|
+
const type = node.type;
|
|
510
|
+
if (
|
|
511
|
+
sensitive &&
|
|
512
|
+
(type === 'number_literal' ||
|
|
513
|
+
(type === 'unary_expression' &&
|
|
514
|
+
node.childForFieldName('argument')?.type === 'number_literal'))
|
|
515
|
+
)
|
|
516
|
+
return '"' + redactSecretValue(original) + '"';
|
|
517
|
+
if (type === 'comment')
|
|
518
|
+
return original.startsWith('/*')
|
|
519
|
+
? '/*' + scrubPayload(original.slice(2, -2)) + '*/'
|
|
520
|
+
: '//' + scrubPayload(original.slice(2));
|
|
521
|
+
// Unknown macro arguments have no literal/expression boundaries to preserve.
|
|
522
|
+
if (type === 'objc_macro_argument_text') throw new Error('Opaque Objective-C++ macro arguments');
|
|
523
|
+
if (type === 'preproc_arg') return scrubPayload(original);
|
|
524
|
+
if (type === 'raw_string_literal') {
|
|
525
|
+
const content = node.namedChildren.find((child) => child.type === 'raw_string_content');
|
|
526
|
+
if (!content) return original;
|
|
527
|
+
return (
|
|
528
|
+
original.slice(0, content.startIndex - node.startIndex) +
|
|
529
|
+
literalPayload(content.text, sensitive) +
|
|
530
|
+
original.slice(content.endIndex - node.startIndex)
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
if (type === 'string_literal' || type === 'char_literal') {
|
|
534
|
+
const quote = type === 'char_literal' ? "'" : '"';
|
|
535
|
+
const start = original.indexOf(quote) + 1;
|
|
536
|
+
const end = original.lastIndexOf(quote);
|
|
537
|
+
return (
|
|
538
|
+
original.slice(0, start) +
|
|
539
|
+
literalPayload(original.slice(start, end), sensitive) +
|
|
540
|
+
original.slice(end)
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
const label = (key: Node | null): boolean => {
|
|
544
|
+
if (!key) return false;
|
|
545
|
+
let current: Node = key;
|
|
546
|
+
while (current.type.endsWith('_declarator') || current.type === 'objc_string_literal') {
|
|
547
|
+
const inner: Node | null =
|
|
548
|
+
current.childForFieldName('declarator') ??
|
|
549
|
+
current.childForFieldName('value') ??
|
|
550
|
+
current.firstNamedChild;
|
|
551
|
+
if (!inner) return false;
|
|
552
|
+
current = inner;
|
|
553
|
+
}
|
|
554
|
+
const field =
|
|
555
|
+
current.childForFieldName('field') ??
|
|
556
|
+
(current.type === 'field_designator' ? current.firstNamedChild : null) ??
|
|
557
|
+
current;
|
|
558
|
+
const content = field.namedChildren.find((child) => child.type === 'string_content');
|
|
559
|
+
return SECRET_LABEL_RE.test(content?.text ?? field.text);
|
|
560
|
+
};
|
|
561
|
+
const key =
|
|
562
|
+
type === 'init_declarator'
|
|
563
|
+
? node.childForFieldName('declarator')
|
|
564
|
+
: type === 'assignment_expression'
|
|
565
|
+
? node.childForFieldName('left')
|
|
566
|
+
: type === 'objc_dictionary_entry'
|
|
567
|
+
? node.childForFieldName('key')
|
|
568
|
+
: null;
|
|
569
|
+
const sensitiveValue =
|
|
570
|
+
type === 'initializer_pair' ? node.childrenForFieldName('designator').some(label) : label(key);
|
|
571
|
+
const value = node.childForFieldName(type === 'assignment_expression' ? 'right' : 'value');
|
|
572
|
+
let result = '';
|
|
573
|
+
let offset = node.startIndex;
|
|
574
|
+
for (const child of node.namedChildren) {
|
|
575
|
+
result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
|
|
576
|
+
const carry =
|
|
577
|
+
sensitive &&
|
|
578
|
+
(type === 'objc_string_literal' ||
|
|
579
|
+
type === 'concatenated_string' ||
|
|
580
|
+
type === 'user_defined_literal' ||
|
|
581
|
+
type === 'parenthesized_expression' ||
|
|
582
|
+
type === 'initializer_list' ||
|
|
583
|
+
(type === 'unary_expression' && child.id === node.childForFieldName('argument')?.id) ||
|
|
584
|
+
(type === 'initializer_pair' && child.id === value?.id) ||
|
|
585
|
+
type === 'objc_array_literal' ||
|
|
586
|
+
type === 'objc_dictionary_literal' ||
|
|
587
|
+
(type === 'objc_dictionary_entry' && child.id === value?.id) ||
|
|
588
|
+
(type === 'objc_boxed_expression' && child.id === value?.id) ||
|
|
589
|
+
(type === 'message_expression' && child.id === node.childForFieldName('receiver')?.id) ||
|
|
590
|
+
(type === 'call_expression' && child.id === node.childForFieldName('function')?.id) ||
|
|
591
|
+
(type === 'field_expression' && child.id === node.childForFieldName('argument')?.id) ||
|
|
592
|
+
(type === 'conditional_expression' &&
|
|
593
|
+
child.id !== node.childForFieldName('condition')?.id) ||
|
|
594
|
+
(type === 'binary_expression' && node.childForFieldName('operator')?.text === '+'));
|
|
595
|
+
result += redactCFamilyNode(child, carry || (sensitiveValue && child.id === value?.id));
|
|
596
|
+
offset = child.endIndex;
|
|
597
|
+
}
|
|
598
|
+
return result + original.slice(offset - node.startIndex);
|
|
599
|
+
}
|
|
600
|
+
|
|
74
601
|
/** Rust raw/byte prefixes belong to the delimiter, never to the credential payload. */
|
|
75
602
|
function redactRustNode(node: Node, sensitive = false, labeled = new Set<number>()): string {
|
|
76
603
|
sensitive ||= labeled.has(node.id);
|
|
@@ -661,7 +1188,11 @@ function redactNode(node: Node, sensitive = false): string {
|
|
|
661
1188
|
// Carry labels through value wrappers, never through call arguments or conditions.
|
|
662
1189
|
const carry =
|
|
663
1190
|
sensitive &&
|
|
664
|
-
(node.type === '
|
|
1191
|
+
(node.type === 'array' ||
|
|
1192
|
+
node.type === 'object' ||
|
|
1193
|
+
(node.type === 'pair' && child.id === value?.id) ||
|
|
1194
|
+
(node.type === 'unary_expression' && child.id === node.childForFieldName('argument')?.id) ||
|
|
1195
|
+
node.type === 'concatenated_string' ||
|
|
665
1196
|
node.type === 'binary_operator' ||
|
|
666
1197
|
node.type === 'unary_operator' ||
|
|
667
1198
|
(node.type === 'call' && child.id === node.childForFieldName('function')?.id) ||
|
|
@@ -686,3 +1217,161 @@ function redactNode(node: Node, sensitive = false): string {
|
|
|
686
1217
|
}
|
|
687
1218
|
return result + original.slice(offset - node.startIndex);
|
|
688
1219
|
}
|
|
1220
|
+
|
|
1221
|
+
/** Sanitize parser-owned payload spans; no SFC boundary is inferred from text matching. */
|
|
1222
|
+
async function scrubVueSource(text: string): Promise<string> {
|
|
1223
|
+
if (!text) return text;
|
|
1224
|
+
const parsed = await parseVueSource(text, true);
|
|
1225
|
+
const fallback = (): string =>
|
|
1226
|
+
(text.startsWith(VUE_FIDELITY_MARKER) ? '' : VUE_FIDELITY_MARKER) + scrubPayload(text);
|
|
1227
|
+
if ('reason' in parsed) return fallback();
|
|
1228
|
+
const { descriptor, nodeTypes: VueNodeTypes } = parsed;
|
|
1229
|
+
const edits: Array<{ start: number; end: number; clean: string }> = [];
|
|
1230
|
+
let faithful = true;
|
|
1231
|
+
const script = descriptor.scriptSetup ?? descriptor.script;
|
|
1232
|
+
// Reuse exactly the JS/TS AST literal/comment rules, with their existing credential engine.
|
|
1233
|
+
const grammar = await loadGrammar(script?.lang === 'ts' ? 'typescript' : 'javascript');
|
|
1234
|
+
if ('unavailable' in grammar) return fallback();
|
|
1235
|
+
const parser = new Parser();
|
|
1236
|
+
parser.setLanguage(grammar.ok.language);
|
|
1237
|
+
const syntaxPayload = (
|
|
1238
|
+
source: string,
|
|
1239
|
+
sensitive = false,
|
|
1240
|
+
expression = false,
|
|
1241
|
+
decoded = source
|
|
1242
|
+
): string => {
|
|
1243
|
+
if (decoded !== source) {
|
|
1244
|
+
const clean = syntaxPayload(decoded, sensitive, expression);
|
|
1245
|
+
if (clean === decoded) return source;
|
|
1246
|
+
faithful = false;
|
|
1247
|
+
// Entity decoding has no raw literal-span proof. Keep valid HTML and withhold graph facts.
|
|
1248
|
+
if (
|
|
1249
|
+
JSON.stringify(clean.match(/\r\n|\r|\n/g)) !== JSON.stringify(source.match(/\r\n|\r|\n/g))
|
|
1250
|
+
)
|
|
1251
|
+
return literalPayload(source, true);
|
|
1252
|
+
return clean
|
|
1253
|
+
.replace(/&/g, '&')
|
|
1254
|
+
.replace(/"/g, '"')
|
|
1255
|
+
.replace(/'/g, ''')
|
|
1256
|
+
.replace(/</g, '<')
|
|
1257
|
+
.replace(/>/g, '>');
|
|
1258
|
+
}
|
|
1259
|
+
const prefix = expression ? `const ${sensitive ? 'password' : '__mnemonik_vue'} = (` : '';
|
|
1260
|
+
const suffix = expression ? '\n);' : '';
|
|
1261
|
+
const full = prefix + source + suffix;
|
|
1262
|
+
const tree = parser.parse(full);
|
|
1263
|
+
try {
|
|
1264
|
+
if (!tree || tree.rootNode.hasError) {
|
|
1265
|
+
const clean = sensitive ? redactSecretValue(source) : scrubPayload(source);
|
|
1266
|
+
// Unsupported sensitive values cannot establish source fidelity.
|
|
1267
|
+
if (!expression || sensitive || clean !== source) faithful = false;
|
|
1268
|
+
return clean;
|
|
1269
|
+
}
|
|
1270
|
+
const clean =
|
|
1271
|
+
full.slice(0, tree.rootNode.startIndex) +
|
|
1272
|
+
redactNode(tree.rootNode) +
|
|
1273
|
+
full.slice(tree.rootNode.endIndex);
|
|
1274
|
+
return clean.slice(prefix.length, clean.length - suffix.length);
|
|
1275
|
+
} finally {
|
|
1276
|
+
tree?.delete();
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
const edit = (start: number, end: number, clean: string): void => {
|
|
1280
|
+
edits.push({ start, end, clean });
|
|
1281
|
+
};
|
|
1282
|
+
try {
|
|
1283
|
+
if (script) edit(script.loc.start.offset, script.loc.end.offset, syntaxPayload(script.content));
|
|
1284
|
+
walkVueTemplate(parsed, (node) => {
|
|
1285
|
+
if (node.type === VueNodeTypes.TEXT)
|
|
1286
|
+
edit(node.loc.start.offset, node.loc.end.offset, literalPayload(node.loc.source, false));
|
|
1287
|
+
if (node.type === VueNodeTypes.COMMENT)
|
|
1288
|
+
edit(
|
|
1289
|
+
node.loc.start.offset,
|
|
1290
|
+
node.loc.end.offset,
|
|
1291
|
+
'<!--' + scrubPayload(node.loc.source.slice(4, -3)) + '-->'
|
|
1292
|
+
);
|
|
1293
|
+
if (
|
|
1294
|
+
node.type === VueNodeTypes.INTERPOLATION &&
|
|
1295
|
+
node.content.type === VueNodeTypes.SIMPLE_EXPRESSION
|
|
1296
|
+
)
|
|
1297
|
+
edit(
|
|
1298
|
+
node.content.loc.start.offset,
|
|
1299
|
+
node.content.loc.end.offset,
|
|
1300
|
+
syntaxPayload(node.content.loc.source, false, true, node.content.content)
|
|
1301
|
+
);
|
|
1302
|
+
if (node.type !== VueNodeTypes.ELEMENT) return;
|
|
1303
|
+
for (const prop of node.props) {
|
|
1304
|
+
if (prop.type === VueNodeTypes.ATTRIBUTE && prop.value) {
|
|
1305
|
+
const raw = prop.value.loc.source;
|
|
1306
|
+
const quoted = raw.startsWith('"') || raw.startsWith("'");
|
|
1307
|
+
edit(
|
|
1308
|
+
prop.value.loc.start.offset,
|
|
1309
|
+
prop.value.loc.end.offset,
|
|
1310
|
+
quoted
|
|
1311
|
+
? raw.slice(0, 1) +
|
|
1312
|
+
literalPayload(raw.slice(1, -1), SECRET_LABEL_RE.test(prop.name)) +
|
|
1313
|
+
raw.slice(-1)
|
|
1314
|
+
: literalPayload(raw, SECRET_LABEL_RE.test(prop.name))
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
if (prop.type !== VueNodeTypes.DIRECTIVE) continue;
|
|
1318
|
+
if (prop.exp?.type === VueNodeTypes.SIMPLE_EXPRESSION)
|
|
1319
|
+
edit(
|
|
1320
|
+
prop.exp.loc.start.offset,
|
|
1321
|
+
prop.exp.loc.end.offset,
|
|
1322
|
+
syntaxPayload(
|
|
1323
|
+
prop.exp.loc.source,
|
|
1324
|
+
prop.arg?.type === VueNodeTypes.SIMPLE_EXPRESSION &&
|
|
1325
|
+
prop.arg.isStatic &&
|
|
1326
|
+
SECRET_LABEL_RE.test(prop.arg.content),
|
|
1327
|
+
true,
|
|
1328
|
+
prop.exp.content
|
|
1329
|
+
)
|
|
1330
|
+
);
|
|
1331
|
+
if (prop.arg?.type === VueNodeTypes.SIMPLE_EXPRESSION && !prop.arg.isStatic)
|
|
1332
|
+
edit(
|
|
1333
|
+
prop.arg.loc.start.offset,
|
|
1334
|
+
prop.arg.loc.end.offset,
|
|
1335
|
+
syntaxPayload(prop.arg.loc.source, false, true, prop.arg.content)
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
for (const style of descriptor.styles) {
|
|
1340
|
+
const clean = scrubPayload(style.content);
|
|
1341
|
+
// No CSS/preprocessor AST proof is claimed. Changed opaque content blocks graph publication.
|
|
1342
|
+
if (clean !== style.content) faithful = false;
|
|
1343
|
+
edit(style.loc.start.offset, style.loc.end.offset, clean);
|
|
1344
|
+
}
|
|
1345
|
+
// Descriptor gaps contain outer tags/comments, not executable script/template bodies.
|
|
1346
|
+
// Policy still applies there; altered opaque boundaries cannot establish graph fidelity.
|
|
1347
|
+
const blocks = [script, descriptor.template, ...descriptor.styles]
|
|
1348
|
+
.filter((b) => b !== null && b !== undefined)
|
|
1349
|
+
.sort((a, b) => a.loc.start.offset - b.loc.start.offset);
|
|
1350
|
+
let gapStart = 0;
|
|
1351
|
+
for (const block of blocks) {
|
|
1352
|
+
const gap = text.slice(gapStart, block.loc.start.offset);
|
|
1353
|
+
const clean = scrubPayload(gap);
|
|
1354
|
+
if (clean !== gap) faithful = false;
|
|
1355
|
+
edit(gapStart, block.loc.start.offset, clean);
|
|
1356
|
+
gapStart = block.loc.end.offset;
|
|
1357
|
+
}
|
|
1358
|
+
const tail = text.slice(gapStart);
|
|
1359
|
+
const cleanTail = scrubPayload(tail);
|
|
1360
|
+
if (cleanTail !== tail) faithful = false;
|
|
1361
|
+
edit(gapStart, text.length, cleanTail);
|
|
1362
|
+
edits.sort((a, b) => b.start - a.start);
|
|
1363
|
+
let clean = text;
|
|
1364
|
+
let boundary = text.length;
|
|
1365
|
+
for (const e of edits) {
|
|
1366
|
+
if (e.end > boundary) return fallback();
|
|
1367
|
+
clean = clean.slice(0, e.start) + e.clean + clean.slice(e.end);
|
|
1368
|
+
boundary = e.start;
|
|
1369
|
+
}
|
|
1370
|
+
if ('reason' in (await parseVueSource(clean))) faithful = false;
|
|
1371
|
+
return faithful || clean.startsWith(VUE_FIDELITY_MARKER) ? clean : VUE_FIDELITY_MARKER + clean;
|
|
1372
|
+
} catch {
|
|
1373
|
+
return fallback();
|
|
1374
|
+
} finally {
|
|
1375
|
+
parser.delete();
|
|
1376
|
+
}
|
|
1377
|
+
}
|