@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.
Files changed (54) hide show
  1. package/dist/ast/astChunker.d.ts.map +1 -1
  2. package/dist/ast/astChunker.js +6 -1
  3. package/dist/ast/astChunker.js.map +1 -1
  4. package/dist/ast/grammars.d.ts +4 -2
  5. package/dist/ast/grammars.d.ts.map +1 -1
  6. package/dist/ast/grammars.js +37 -0
  7. package/dist/ast/grammars.js.map +1 -1
  8. package/dist/codeScanner.d.ts +3 -0
  9. package/dist/codeScanner.d.ts.map +1 -1
  10. package/dist/codeScanner.js +7 -0
  11. package/dist/codeScanner.js.map +1 -1
  12. package/dist/index.d.ts +3 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/sourceRedaction.d.ts.map +1 -1
  17. package/dist/sourceRedaction.js +667 -37
  18. package/dist/sourceRedaction.js.map +1 -1
  19. package/dist/vueSource.d.ts +17 -0
  20. package/dist/vueSource.d.ts.map +1 -0
  21. package/dist/vueSource.js +63 -0
  22. package/dist/vueSource.js.map +1 -0
  23. package/package.json +3 -1
  24. package/queries/cmake.tags.scm +6 -0
  25. package/queries/nix.tags.scm +4 -0
  26. package/queries/objcpp.tags.scm +4 -0
  27. package/queries/terraform.tags.scm +8 -0
  28. package/scripts/vendor-grammars.ts +468 -40
  29. package/src/ast/astChunker.ts +10 -1
  30. package/src/ast/grammars.ts +34 -1
  31. package/src/codeScanner.ts +6 -0
  32. package/src/index.ts +5 -0
  33. package/src/sourceRedaction.ts +726 -37
  34. package/src/vueSource.ts +77 -0
  35. package/wasm/bash.wasm +0 -0
  36. package/wasm/cmake.LICENSE +21 -0
  37. package/wasm/cmake.tags.scm +6 -0
  38. package/wasm/cmake.wasm +0 -0
  39. package/wasm/cpp.LICENSE +21 -0
  40. package/wasm/cpp.wasm +0 -0
  41. package/wasm/groovy.LICENSE +25 -0
  42. package/wasm/groovy.wasm +0 -0
  43. package/wasm/nix.LICENSE +21 -0
  44. package/wasm/nix.tags.scm +4 -0
  45. package/wasm/nix.wasm +0 -0
  46. package/wasm/objcpp.LICENSE +28 -0
  47. package/wasm/objcpp.tags.scm +4 -0
  48. package/wasm/objcpp.wasm +0 -0
  49. package/wasm/sql.wasm +0 -0
  50. package/wasm/terraform.LICENSE +201 -0
  51. package/wasm/terraform.tags.scm +8 -0
  52. package/wasm/terraform.wasm +0 -0
  53. package/wasm/tsx.wasm +0 -0
  54. package/wasm/typescript.wasm +0 -0
@@ -1,6 +1,7 @@
1
- import { extname } from 'node:path';
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
- const language = ['.ts', '.mts', '.cts'].includes(extension)
21
- ? 'typescript'
22
- : extension === '.tsx'
23
- ? 'tsx'
24
- : ['.js', '.jsx', '.mjs', '.cjs'].includes(extension)
25
- ? 'javascript'
26
- : ['.py', '.pyi'].includes(extension)
27
- ? 'python'
28
- : ['.sh', '.bash', '.zsh'].includes(extension)
29
- ? 'bash'
30
- : extension === '.swift'
31
- ? 'swift'
32
- : extension === '.sql'
33
- ? 'sql'
34
- : extension === '.lua'
35
- ? 'lua'
36
- : extension === '.rs'
37
- ? 'rust'
38
- : null;
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 scrubPayload(text);
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 scrubPayload(text);
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 scrubPayload(text);
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 === 'bash'
55
- ? redactShellNode(root, commandLineCredentialRanges(text))
56
- : language === 'swift'
57
- ? redactSwiftNode(root)
58
- : language === 'sql'
59
- ? redactSqlNode(root)
60
- : language === 'lua'
61
- ? redactLuaNode(root)
62
- : language === 'rust'
63
- ? redactRustNode(root)
64
- : redactNode(root);
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 scrubPayload(text);
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 === 'concatenated_string' ||
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, '&amp;')
1254
+ .replace(/"/g, '&quot;')
1255
+ .replace(/'/g, '&#39;')
1256
+ .replace(/</g, '&lt;')
1257
+ .replace(/>/g, '&gt;');
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
+ }