@mnemonik/shared 7.143.0 → 7.144.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 } 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 { redactHighEntropyTokens, redactSecretValue, scrubSecrets, SECRET_LABEL_RE, } from './secretPatterns.js';
@@ -10,61 +11,549 @@ function scrubPayload(text) {
10
11
  /** Source syntax stays executable; unsupported or incomplete input uses the conservative scrubber. */
11
12
  export async function scrubSourceSecrets(text, filePath) {
12
13
  const extension = extname(filePath).toLowerCase();
13
- const language = ['.ts', '.mts', '.cts'].includes(extension)
14
- ? 'typescript'
15
- : extension === '.tsx'
16
- ? 'tsx'
17
- : ['.js', '.jsx', '.mjs', '.cjs'].includes(extension)
18
- ? 'javascript'
19
- : ['.py', '.pyi'].includes(extension)
20
- ? 'python'
21
- : ['.sh', '.bash', '.zsh'].includes(extension)
22
- ? 'bash'
23
- : extension === '.swift'
24
- ? 'swift'
25
- : extension === '.sql'
26
- ? 'sql'
27
- : extension === '.lua'
28
- ? 'lua'
29
- : extension === '.rs'
30
- ? 'rust'
31
- : null;
14
+ if (extension === '.vue')
15
+ return scrubVueSource(text);
16
+ const language = extension === '.cmake' || basename(filePath) === 'CMakeLists.txt'
17
+ ? 'cmake'
18
+ : ['.ts', '.mts', '.cts'].includes(extension)
19
+ ? 'typescript'
20
+ : extension === '.tsx'
21
+ ? 'tsx'
22
+ : ['.js', '.jsx', '.mjs', '.cjs'].includes(extension)
23
+ ? 'javascript'
24
+ : ['.py', '.pyi'].includes(extension)
25
+ ? 'python'
26
+ : ['.sh', '.bash', '.zsh'].includes(extension)
27
+ ? 'bash'
28
+ : extension === '.swift'
29
+ ? 'swift'
30
+ : extension === '.sql'
31
+ ? 'sql'
32
+ : extension === '.lua'
33
+ ? 'lua'
34
+ : extension === '.rs'
35
+ ? 'rust'
36
+ : extension === '.nix'
37
+ ? 'nix'
38
+ : extension === '.tf'
39
+ ? 'terraform'
40
+ : ['.m', '.mm'].includes(extension)
41
+ ? 'objcpp'
42
+ : extension === '.c'
43
+ ? 'c'
44
+ : ['.h', '.cpp', '.cc', '.cxx', '.hpp', '.hh', '.hxx'].includes(extension)
45
+ ? 'cpp'
46
+ : ['.groovy', '.gradle'].includes(extension)
47
+ ? 'groovy'
48
+ : ['.ps1', '.psm1'].includes(extension)
49
+ ? 'powershell'
50
+ : null;
51
+ const fallback = () => {
52
+ const clean = scrubPayload(text);
53
+ if (!text)
54
+ return clean;
55
+ if (language === 'powershell') {
56
+ const marker = '# mnemonik:powershell-source-fidelity-unavailable\n';
57
+ return clean.startsWith(marker) ? clean : marker + clean;
58
+ }
59
+ // A fallback may repair syntax accidentally; these graph extractors must never trust it.
60
+ if (language === 'cmake' || language === 'groovy') {
61
+ const marker = `// mnemonik:${language}-source-fidelity-unavailable`;
62
+ const comment = language === 'cmake' ? marker.replace('//', '#') : marker;
63
+ return clean.startsWith(comment) ? clean : comment + '\n' + clean;
64
+ }
65
+ const marker = language === 'nix'
66
+ ? '/* mnemonik:nix-source-fidelity-unavailable */'
67
+ : '/* mnemonik:terraform-source-fidelity-unavailable */';
68
+ return (language === 'terraform' || language === 'nix') && !clean.startsWith(marker)
69
+ ? marker + clean
70
+ : clean;
71
+ };
32
72
  if (!text || !language || Buffer.byteLength(text, 'utf8') > MAX_AST_PARSE_BYTES) {
33
- return scrubPayload(text);
73
+ return fallback();
34
74
  }
35
75
  let parser;
36
76
  let tree = null;
37
77
  try {
38
78
  const grammar = await loadGrammar(language);
39
79
  if ('unavailable' in grammar)
40
- return scrubPayload(text);
80
+ return fallback();
41
81
  parser = new Parser();
42
82
  parser.setLanguage(grammar.ok.language);
43
83
  tree = parser.parse(text);
44
84
  if (!tree || tree.rootNode.hasError)
45
- return scrubPayload(text);
85
+ return fallback();
46
86
  const root = tree.rootNode;
47
- const redacted = language === 'bash'
48
- ? redactShellNode(root, commandLineCredentialRanges(text))
49
- : language === 'swift'
50
- ? redactSwiftNode(root)
51
- : language === 'sql'
52
- ? redactSqlNode(root)
53
- : language === 'lua'
54
- ? redactLuaNode(root)
55
- : language === 'rust'
56
- ? redactRustNode(root)
57
- : redactNode(root);
87
+ if (language === 'objcpp' && objcppSyntaxIssue(root))
88
+ return fallback();
89
+ const redacted = language === 'cmake'
90
+ ? redactCmakeNode(root)
91
+ : language === 'bash'
92
+ ? redactShellNode(root, commandLineCredentialRanges(text))
93
+ : language === 'swift'
94
+ ? redactSwiftNode(root)
95
+ : language === 'sql'
96
+ ? redactSqlNode(root)
97
+ : language === 'lua'
98
+ ? redactLuaNode(root)
99
+ : language === 'rust'
100
+ ? redactRustNode(root)
101
+ : language === 'nix'
102
+ ? redactNixNode(root)
103
+ : language === 'terraform'
104
+ ? redactTerraformNode(root)
105
+ : ['objcpp', 'c', 'cpp'].includes(language)
106
+ ? redactCFamilyNode(root)
107
+ : language === 'groovy'
108
+ ? redactGroovyNode(root)
109
+ : language === 'powershell'
110
+ ? redactPowershellNode(root)
111
+ : redactNode(root);
58
112
  return text.slice(0, root.startIndex) + redacted + text.slice(root.endIndex);
59
113
  }
60
114
  catch {
61
- return scrubPayload(text);
115
+ return fallback();
62
116
  }
63
117
  finally {
64
118
  tree?.delete();
65
119
  parser?.delete();
66
120
  }
67
121
  }
122
+ /** CMake substitutions remain source expressions; only AST argument/comment payloads are scrubbed. */
123
+ function redactCmakeNode(node, sensitive = false) {
124
+ const original = node.text;
125
+ const type = node.type;
126
+ const payload = (value) => sensitive && value.trim() ? redactSecretValue(value) : literalPayload(value, false);
127
+ if (type === 'line_comment')
128
+ return '#' + scrubPayload(original.slice(1));
129
+ if (type === 'bracket_comment' || type === 'bracket_argument') {
130
+ const payload = node.namedChildren.find((n) => n.type.endsWith('_content'));
131
+ if (!payload)
132
+ return original;
133
+ const clean = type === 'bracket_comment'
134
+ ? scrubPayload(payload.text)
135
+ : sensitive
136
+ ? redactSecretValue(payload.text)
137
+ : literalPayload(payload.text, false);
138
+ return (original.slice(0, payload.startIndex - node.startIndex) +
139
+ clean +
140
+ original.slice(payload.endIndex - node.startIndex));
141
+ }
142
+ if (type === 'variable_ref')
143
+ return original;
144
+ if (type === 'escape_sequence')
145
+ return sensitive ? redactSecretValue(original) : original;
146
+ if (type === 'quoted_element' || type === 'unquoted_argument') {
147
+ // Generator expressions have no inner AST in this grammar; withhold fidelity instead of rewriting them.
148
+ if (original.includes('$<'))
149
+ throw new Error('Opaque CMake generator expression');
150
+ let result = '';
151
+ let offset = node.startIndex;
152
+ for (const child of node.namedChildren) {
153
+ result += payload(original.slice(offset - node.startIndex, child.startIndex - node.startIndex));
154
+ result += redactCmakeNode(child, sensitive);
155
+ offset = child.endIndex;
156
+ }
157
+ result += payload(original.slice(offset - node.startIndex));
158
+ return type === 'unquoted_argument' && result !== original ? '"' + result + '"' : result;
159
+ }
160
+ const args = type === 'argument_list' ? node.namedChildren.filter((n) => n.type === 'argument') : [];
161
+ const set = node.parent?.namedChildren.find((n) => n.type === 'identifier')?.text.toLowerCase() === 'set';
162
+ const key = set ? args[0] : null;
163
+ const label = key?.firstNamedChild;
164
+ const literalKey = label?.type === 'unquoted_argument' && !label.namedChildCount
165
+ ? label.text
166
+ : label?.type === 'quoted_argument' && !label.firstNamedChild?.namedChildCount
167
+ ? label.text.slice(1, -1)
168
+ : null;
169
+ const name = literalKey?.match(/^ENV\{([^{}]+)\}$/)?.[1] ?? literalKey;
170
+ const labeled = name !== null && SECRET_LABEL_RE.test(name);
171
+ let result = '';
172
+ let offset = node.startIndex;
173
+ for (const child of node.namedChildren) {
174
+ result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
175
+ result +=
176
+ child.id === key?.id || (type === 'normal_command' && child.type === 'identifier')
177
+ ? child.text
178
+ : redactCmakeNode(child, sensitive || (labeled && args.some((a) => a.id === child.id)));
179
+ offset = child.endIndex;
180
+ }
181
+ return result + original.slice(offset - node.startIndex);
182
+ }
183
+ /** PowerShell's AST separates command syntax from literal strings and interpolation nodes. */
184
+ function redactPowershellNode(node, sensitive = false) {
185
+ const original = node.text;
186
+ const type = node.type;
187
+ if (type === 'comment')
188
+ return original.startsWith('<#')
189
+ ? '<#' + scrubPayload(original.slice(2, -2)) + '#>'
190
+ : '#' + scrubPayload(original.slice(1));
191
+ if (type === 'string_literal') {
192
+ const here = original.startsWith('@');
193
+ const prefix = here ? 2 : 1;
194
+ const expandable = node.firstNamedChild;
195
+ const substitutions = expandable?.type.startsWith('expandable_')
196
+ ? expandable.namedChildren
197
+ : [];
198
+ const payload = (text) => text
199
+ // Retain quote/dollar/control escapes and physical continuation boundaries verbatim.
200
+ .split(/(`(?:\r\n|[0abefnrtv`"'$\r\n])|''|\r\n|[\r\n])/g)
201
+ .map((part, index) => index % 2
202
+ ? part
203
+ : sensitive && part
204
+ ? redactSecretValue(part)
205
+ : literalPayload(part, false))
206
+ .join('');
207
+ let result = original.slice(0, prefix);
208
+ let offset = node.startIndex + prefix;
209
+ for (const child of substitutions) {
210
+ result += payload(original.slice(offset - node.startIndex, child.startIndex - node.startIndex));
211
+ result += redactPowershellNode(child); // Runtime references/subexpressions are not credential values.
212
+ offset = child.endIndex;
213
+ }
214
+ return (result + payload(original.slice(offset - node.startIndex, -prefix)) + original.slice(-prefix));
215
+ }
216
+ if (sensitive && type === 'generic_token')
217
+ return literalPayload(original, true);
218
+ if (sensitive && ['integer_literal', 'real_literal'].includes(type))
219
+ return "'" + redactSecretValue(original) + "'";
220
+ const assignment = type === 'assignment_expression';
221
+ const key = assignment || type === 'hash_entry' ? node.firstNamedChild : null;
222
+ const value = assignment
223
+ ? node.childForFieldName('value')
224
+ : type === 'hash_entry'
225
+ ? node.namedChildren[1]
226
+ : null;
227
+ let label = key;
228
+ while (label && label.type !== 'string_literal') {
229
+ if (label.type === 'element_access' || label.namedChildCount === 1)
230
+ label = label.firstNamedChild;
231
+ else
232
+ break;
233
+ }
234
+ if (label?.type === 'member_access')
235
+ label = label.lastNamedChild?.firstNamedChild ?? label.lastNamedChild;
236
+ const labelText = label?.type === 'string_literal' ? label.text.slice(1, -1) : label?.text;
237
+ const labeled = !!labelText &&
238
+ SECRET_LABEL_RE.test(labelText
239
+ .replace(/^\$\{?|}$/g, '')
240
+ .split(':')
241
+ .at(-1) ?? '');
242
+ const valueWrapper = sensitive &&
243
+ (type === 'pipeline' ||
244
+ type === 'pipeline_chain' ||
245
+ type === 'statement_list' ||
246
+ type === 'hash_literal_body' ||
247
+ type === 'expression_with_unary_operator' ||
248
+ (type.endsWith('_expression') &&
249
+ ![
250
+ 'sub_expression',
251
+ 'member_access',
252
+ 'invokation_expression',
253
+ 'script_block_expression',
254
+ ].includes(type)));
255
+ let result = '';
256
+ let offset = node.startIndex;
257
+ let argumentSensitive = false;
258
+ for (const child of node.namedChildren) {
259
+ result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
260
+ if (type === 'command_elements' && child.type === 'command_parameter')
261
+ argumentSensitive = SECRET_LABEL_RE.test(child.text.replace(/^-|:$/g, ''));
262
+ const argument = type === 'command_elements' &&
263
+ !['command_parameter', 'command_argument_sep'].includes(child.type);
264
+ result += redactPowershellNode(child, valueWrapper ||
265
+ ((labeled || (sensitive && type === 'hash_entry')) && child.id === value?.id) ||
266
+ (argument && argumentSensitive));
267
+ if (argument)
268
+ argumentSensitive = false;
269
+ offset = child.endIndex;
270
+ }
271
+ return result + original.slice(offset - node.startIndex);
272
+ }
273
+ /** Nix literal payloads have spans; lambda bodies and application arguments remain runtime code. */
274
+ function redactNixNode(node, sensitive = false) {
275
+ const type = node.type;
276
+ const original = node.text;
277
+ if (type === 'comment')
278
+ return redactNode(node);
279
+ if (type === 'string_fragment')
280
+ return sensitive ? redactSecretValue(original) : literalPayload(original, false);
281
+ if (type === 'escape_sequence' || type === 'dollar_escape')
282
+ return original;
283
+ if ((type === 'integer_expression' ||
284
+ type === 'float_expression' ||
285
+ (type === 'unary_expression' &&
286
+ ['integer_expression', 'float_expression'].includes(node.childForFieldName('argument')?.type ?? ''))) &&
287
+ sensitive)
288
+ return '"' + redactSecretValue(original) + '"';
289
+ if (type === 'uri_expression') {
290
+ const clean = literalPayload(original, sensitive);
291
+ return clean === original ? original : JSON.stringify(clean);
292
+ }
293
+ if (['path_expression', 'hpath_expression', 'spath_expression'].includes(type) &&
294
+ (sensitive || scrubPayload(original) !== original))
295
+ throw new Error('Nix path fidelity unavailable');
296
+ const key = type === 'binding' ? node.childForFieldName('attrpath') : null;
297
+ const name = key?.lastNamedChild ?? (type === 'formal' ? node.childForFieldName('name') : null);
298
+ const labeled = name?.type === 'identifier'
299
+ ? secretLabel(name)
300
+ : name?.type === 'string_expression' &&
301
+ !name.namedChildren.some((n) => n.type === 'interpolation')
302
+ ? SECRET_LABEL_RE.test(name.text.slice(1, -1))
303
+ : false;
304
+ const value = node.childForFieldName(type === 'formal' ? 'default' : 'expression');
305
+ let result = '';
306
+ let offset = node.startIndex;
307
+ let escapedDollar = false;
308
+ for (const child of node.namedChildren) {
309
+ result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
310
+ const carry = sensitive &&
311
+ ([
312
+ 'string_expression',
313
+ 'indented_string_expression',
314
+ 'list_expression',
315
+ 'parenthesized_expression',
316
+ 'attrset_expression',
317
+ 'rec_attrset_expression',
318
+ 'binding_set',
319
+ ].includes(type) ||
320
+ (type === 'binding' && child.id === value?.id) ||
321
+ (['let_expression', 'with_expression', 'assert_expression'].includes(type) &&
322
+ child.id === node.childForFieldName('body')?.id) ||
323
+ (type === 'interpolation' && child.id === node.childForFieldName('expression')?.id) ||
324
+ (type === 'unary_expression' && child.id === node.childForFieldName('argument')?.id) ||
325
+ (type === 'if_expression' && child.id !== node.childForFieldName('condition')?.id) ||
326
+ (type === 'binary_expression' &&
327
+ ['+', '++'].includes(node.childForFieldName('operator')?.text ?? '')) ||
328
+ (type === 'apply_expression' && child.id === node.childForFieldName('function')?.id) ||
329
+ (type === 'select_expression' && child.type !== 'attrpath'));
330
+ const structural = child.id === key?.id ||
331
+ (escapedDollar && child.type === 'string_fragment' && child.text === '$');
332
+ result += structural
333
+ ? child.text
334
+ : redactNixNode(child, carry || (labeled && child.id === value?.id));
335
+ escapedDollar = child.type === 'dollar_escape';
336
+ offset = child.endIndex;
337
+ }
338
+ return result + original.slice(offset - node.startIndex);
339
+ }
340
+ /** HCL exposes literal spans separately from interpolation/directive expressions. */
341
+ function redactTerraformNode(node, sensitive = false) {
342
+ const original = node.text;
343
+ const type = node.type;
344
+ if (type === 'comment')
345
+ return redactNode(node);
346
+ // Resource labels and object keys are identifiers, even when the AST calls them strings.
347
+ if (type === 'block')
348
+ sensitive = false;
349
+ if (type === 'template_literal') {
350
+ if (!sensitive)
351
+ return literalPayload(original, false);
352
+ // Escape tokens remain literal; scrubbing must not turn $${ into an interpolation.
353
+ return original
354
+ .split(/(\\["\\nrt]|\$\$\{|%%\{|[{}])/g)
355
+ .map((part, index) => (index % 2 ? part : literalPayload(part, sensitive)))
356
+ .join('');
357
+ }
358
+ if (type === 'numeric_lit' && sensitive)
359
+ return '"' + redactSecretValue(original) + '"';
360
+ const key = type === 'attribute' ? node.firstNamedChild : node.childForFieldName('key');
361
+ const value = type === 'attribute' ? node.namedChildren[1] : node.childForFieldName('val');
362
+ const keyName = key?.type === 'expression' ? key.firstNamedChild : key;
363
+ const literalKey = keyName?.type === 'literal_value' ? keyName.firstNamedChild : keyName;
364
+ const container = type === 'attribute' ? node.parent?.parent : null;
365
+ const containerName = container?.namedChildren[1];
366
+ const secretDefault = container?.type === 'block' &&
367
+ ((container.firstNamedChild?.text === 'variable' && key?.text === 'default') ||
368
+ (container.firstNamedChild?.text === 'output' && key?.text === 'value')) &&
369
+ containerName?.type === 'string_lit' &&
370
+ SECRET_LABEL_RE.test(containerName.text.slice(1, -1));
371
+ const labeled = secretDefault ||
372
+ (literalKey?.type === 'string_lit'
373
+ ? SECRET_LABEL_RE.test(literalKey.text.slice(1, -1))
374
+ : secretLabel(literalKey ?? null));
375
+ let result = '';
376
+ let offset = node.startIndex;
377
+ for (const child of node.namedChildren) {
378
+ result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
379
+ const carry = sensitive &&
380
+ ([
381
+ 'expression',
382
+ 'operation',
383
+ 'unary_operation',
384
+ 'binary_operation',
385
+ 'literal_value',
386
+ 'template_expr',
387
+ 'string_lit',
388
+ 'quoted_template',
389
+ 'heredoc_template',
390
+ 'collection_value',
391
+ 'tuple',
392
+ 'object',
393
+ 'template_directive',
394
+ ].includes(type) ||
395
+ (['template_for', 'template_if'].includes(type) &&
396
+ ![
397
+ 'template_for_start',
398
+ 'template_for_end',
399
+ 'template_if_intro',
400
+ 'template_else_intro',
401
+ 'template_if_end',
402
+ ].includes(child.type)) ||
403
+ (type === 'conditional' && child.id !== node.firstNamedChild?.id) ||
404
+ (type === 'object_elem' && child.id === value?.id));
405
+ // Block labels and object keys are never literal values to scrub.
406
+ result +=
407
+ (type === 'block' && child.type === 'string_lit') || child.id === key?.id
408
+ ? child.text
409
+ : redactTerraformNode(child, carry || (labeled && child.id === value?.id));
410
+ offset = child.endIndex;
411
+ }
412
+ return result + original.slice(offset - node.startIndex);
413
+ }
414
+ /** Groovy's quoted strings and comments have spans; command calls remain executable syntax. */
415
+ function redactGroovyNode(node, sensitive = false) {
416
+ const original = node.text;
417
+ const type = node.type;
418
+ if (type === 'line_comment')
419
+ return '//' + scrubPayload(original.slice(2));
420
+ if (type === 'block_comment')
421
+ return '/*' + scrubPayload(original.slice(2, -2)) + '*/';
422
+ const operand = type === 'unary_expression' ? node.childForFieldName('operand') : null;
423
+ const numeric = (kind) => kind.endsWith('_integer_literal') || kind.endsWith('_floating_point_literal');
424
+ if (sensitive && (numeric(type) || (operand && numeric(operand.type))))
425
+ return '"' + redactSecretValue(original) + '"';
426
+ if (type === 'character_literal' || type === 'string_literal') {
427
+ const delimiter = type === 'character_literal' ? "'" : node.firstChild?.text;
428
+ if (!delimiter)
429
+ throw new Error('Incomplete Groovy string');
430
+ // The inherited Java rule leaves Groovy GString expressions opaque, not AST substitutions.
431
+ if (delimiter.startsWith('"') && original.includes('$'))
432
+ throw new Error('Opaque Groovy GString interpolation');
433
+ return (delimiter +
434
+ literalPayload(original.slice(delimiter.length, -delimiter.length), sensitive) +
435
+ delimiter);
436
+ }
437
+ const key = type === 'variable_declarator'
438
+ ? node.childForFieldName('name')
439
+ : type === 'assignment_expression'
440
+ ? node.childForFieldName('left')
441
+ : type === 'map_item'
442
+ ? node.childForFieldName('key')
443
+ : null;
444
+ const value = node.childForFieldName(type === 'assignment_expression' ? 'right' : 'value');
445
+ const field = key?.childForFieldName('field') ?? key;
446
+ const name = field && ['character_literal', 'string_literal'].includes(field.type)
447
+ ? field.text.slice(1, -1)
448
+ : field?.text;
449
+ const labeled = !!name && SECRET_LABEL_RE.test(name);
450
+ let result = '';
451
+ let offset = node.startIndex;
452
+ for (const child of node.namedChildren) {
453
+ result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
454
+ const carry = sensitive &&
455
+ (type === 'parenthesized_expression' ||
456
+ type === 'array_literal' ||
457
+ type === 'map_literal' ||
458
+ (type === 'map_item' && child.id === value?.id) ||
459
+ (type === 'method_invocation' && child.id === node.childForFieldName('object')?.id) ||
460
+ (type === 'field_access' && child.id === node.childForFieldName('object')?.id) ||
461
+ (type === 'juxt_function_call' && child.id === node.childForFieldName('name')?.id) ||
462
+ (type === 'binary_expression' && node.childForFieldName('operator')?.text === '+') ||
463
+ (type === 'ternary_expression' && child.id !== node.childForFieldName('condition')?.id));
464
+ result += redactGroovyNode(child, carry || (labeled && child.id === value?.id));
465
+ offset = child.endIndex;
466
+ }
467
+ return result + original.slice(offset - node.startIndex);
468
+ }
469
+ /** Keep C/Objective-C++ prefixes, raw delimiters and runtime expressions outside literal payloads. */
470
+ function redactCFamilyNode(node, sensitive = false) {
471
+ const original = node.text;
472
+ const type = node.type;
473
+ if (sensitive &&
474
+ (type === 'number_literal' ||
475
+ (type === 'unary_expression' &&
476
+ node.childForFieldName('argument')?.type === 'number_literal')))
477
+ return '"' + redactSecretValue(original) + '"';
478
+ if (type === 'comment')
479
+ return original.startsWith('/*')
480
+ ? '/*' + scrubPayload(original.slice(2, -2)) + '*/'
481
+ : '//' + scrubPayload(original.slice(2));
482
+ // Unknown macro arguments have no literal/expression boundaries to preserve.
483
+ if (type === 'objc_macro_argument_text')
484
+ throw new Error('Opaque Objective-C++ macro arguments');
485
+ if (type === 'preproc_arg')
486
+ return scrubPayload(original);
487
+ if (type === 'raw_string_literal') {
488
+ const content = node.namedChildren.find((child) => child.type === 'raw_string_content');
489
+ if (!content)
490
+ return original;
491
+ return (original.slice(0, content.startIndex - node.startIndex) +
492
+ literalPayload(content.text, sensitive) +
493
+ original.slice(content.endIndex - node.startIndex));
494
+ }
495
+ if (type === 'string_literal' || type === 'char_literal') {
496
+ const quote = type === 'char_literal' ? "'" : '"';
497
+ const start = original.indexOf(quote) + 1;
498
+ const end = original.lastIndexOf(quote);
499
+ return (original.slice(0, start) +
500
+ literalPayload(original.slice(start, end), sensitive) +
501
+ original.slice(end));
502
+ }
503
+ const label = (key) => {
504
+ if (!key)
505
+ return false;
506
+ let current = key;
507
+ while (current.type.endsWith('_declarator') || current.type === 'objc_string_literal') {
508
+ const inner = current.childForFieldName('declarator') ??
509
+ current.childForFieldName('value') ??
510
+ current.firstNamedChild;
511
+ if (!inner)
512
+ return false;
513
+ current = inner;
514
+ }
515
+ const field = current.childForFieldName('field') ??
516
+ (current.type === 'field_designator' ? current.firstNamedChild : null) ??
517
+ current;
518
+ const content = field.namedChildren.find((child) => child.type === 'string_content');
519
+ return SECRET_LABEL_RE.test(content?.text ?? field.text);
520
+ };
521
+ const key = type === 'init_declarator'
522
+ ? node.childForFieldName('declarator')
523
+ : type === 'assignment_expression'
524
+ ? node.childForFieldName('left')
525
+ : type === 'objc_dictionary_entry'
526
+ ? node.childForFieldName('key')
527
+ : null;
528
+ const sensitiveValue = type === 'initializer_pair' ? node.childrenForFieldName('designator').some(label) : label(key);
529
+ const value = node.childForFieldName(type === 'assignment_expression' ? 'right' : 'value');
530
+ let result = '';
531
+ let offset = node.startIndex;
532
+ for (const child of node.namedChildren) {
533
+ result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
534
+ const carry = sensitive &&
535
+ (type === 'objc_string_literal' ||
536
+ type === 'concatenated_string' ||
537
+ type === 'user_defined_literal' ||
538
+ type === 'parenthesized_expression' ||
539
+ type === 'initializer_list' ||
540
+ (type === 'unary_expression' && child.id === node.childForFieldName('argument')?.id) ||
541
+ (type === 'initializer_pair' && child.id === value?.id) ||
542
+ type === 'objc_array_literal' ||
543
+ type === 'objc_dictionary_literal' ||
544
+ (type === 'objc_dictionary_entry' && child.id === value?.id) ||
545
+ (type === 'objc_boxed_expression' && child.id === value?.id) ||
546
+ (type === 'message_expression' && child.id === node.childForFieldName('receiver')?.id) ||
547
+ (type === 'call_expression' && child.id === node.childForFieldName('function')?.id) ||
548
+ (type === 'field_expression' && child.id === node.childForFieldName('argument')?.id) ||
549
+ (type === 'conditional_expression' &&
550
+ child.id !== node.childForFieldName('condition')?.id) ||
551
+ (type === 'binary_expression' && node.childForFieldName('operator')?.text === '+'));
552
+ result += redactCFamilyNode(child, carry || (sensitiveValue && child.id === value?.id));
553
+ offset = child.endIndex;
554
+ }
555
+ return result + original.slice(offset - node.startIndex);
556
+ }
68
557
  /** Rust raw/byte prefixes belong to the delimiter, never to the credential payload. */
69
558
  function redactRustNode(node, sensitive = false, labeled = new Set()) {
70
559
  sensitive ||= labeled.has(node.id);
@@ -603,7 +1092,11 @@ function redactNode(node, sensitive = false) {
603
1092
  result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
604
1093
  // Carry labels through value wrappers, never through call arguments or conditions.
605
1094
  const carry = sensitive &&
606
- (node.type === 'concatenated_string' ||
1095
+ (node.type === 'array' ||
1096
+ node.type === 'object' ||
1097
+ (node.type === 'pair' && child.id === value?.id) ||
1098
+ (node.type === 'unary_expression' && child.id === node.childForFieldName('argument')?.id) ||
1099
+ node.type === 'concatenated_string' ||
607
1100
  node.type === 'binary_operator' ||
608
1101
  node.type === 'unary_operator' ||
609
1102
  (node.type === 'call' && child.id === node.childForFieldName('function')?.id) ||
@@ -626,4 +1119,141 @@ function redactNode(node, sensitive = false) {
626
1119
  }
627
1120
  return result + original.slice(offset - node.startIndex);
628
1121
  }
1122
+ /** Sanitize parser-owned payload spans; no SFC boundary is inferred from text matching. */
1123
+ async function scrubVueSource(text) {
1124
+ if (!text)
1125
+ return text;
1126
+ const parsed = await parseVueSource(text, true);
1127
+ const fallback = () => (text.startsWith(VUE_FIDELITY_MARKER) ? '' : VUE_FIDELITY_MARKER) + scrubPayload(text);
1128
+ if ('reason' in parsed)
1129
+ return fallback();
1130
+ const { descriptor, nodeTypes: VueNodeTypes } = parsed;
1131
+ const edits = [];
1132
+ let faithful = true;
1133
+ const script = descriptor.scriptSetup ?? descriptor.script;
1134
+ // Reuse exactly the JS/TS AST literal/comment rules, with their existing credential engine.
1135
+ const grammar = await loadGrammar(script?.lang === 'ts' ? 'typescript' : 'javascript');
1136
+ if ('unavailable' in grammar)
1137
+ return fallback();
1138
+ const parser = new Parser();
1139
+ parser.setLanguage(grammar.ok.language);
1140
+ const syntaxPayload = (source, sensitive = false, expression = false, decoded = source) => {
1141
+ if (decoded !== source) {
1142
+ const clean = syntaxPayload(decoded, sensitive, expression);
1143
+ if (clean === decoded)
1144
+ return source;
1145
+ faithful = false;
1146
+ // Entity decoding has no raw literal-span proof. Keep valid HTML and withhold graph facts.
1147
+ if (JSON.stringify(clean.match(/\r\n|\r|\n/g)) !== JSON.stringify(source.match(/\r\n|\r|\n/g)))
1148
+ return literalPayload(source, true);
1149
+ return clean
1150
+ .replace(/&/g, '&amp;')
1151
+ .replace(/"/g, '&quot;')
1152
+ .replace(/'/g, '&#39;')
1153
+ .replace(/</g, '&lt;')
1154
+ .replace(/>/g, '&gt;');
1155
+ }
1156
+ const prefix = expression ? `const ${sensitive ? 'password' : '__mnemonik_vue'} = (` : '';
1157
+ const suffix = expression ? '\n);' : '';
1158
+ const full = prefix + source + suffix;
1159
+ const tree = parser.parse(full);
1160
+ try {
1161
+ if (!tree || tree.rootNode.hasError) {
1162
+ const clean = sensitive ? redactSecretValue(source) : scrubPayload(source);
1163
+ // Unsupported sensitive values cannot establish source fidelity.
1164
+ if (!expression || sensitive || clean !== source)
1165
+ faithful = false;
1166
+ return clean;
1167
+ }
1168
+ const clean = full.slice(0, tree.rootNode.startIndex) +
1169
+ redactNode(tree.rootNode) +
1170
+ full.slice(tree.rootNode.endIndex);
1171
+ return clean.slice(prefix.length, clean.length - suffix.length);
1172
+ }
1173
+ finally {
1174
+ tree?.delete();
1175
+ }
1176
+ };
1177
+ const edit = (start, end, clean) => {
1178
+ edits.push({ start, end, clean });
1179
+ };
1180
+ try {
1181
+ if (script)
1182
+ edit(script.loc.start.offset, script.loc.end.offset, syntaxPayload(script.content));
1183
+ walkVueTemplate(parsed, (node) => {
1184
+ if (node.type === VueNodeTypes.TEXT)
1185
+ edit(node.loc.start.offset, node.loc.end.offset, literalPayload(node.loc.source, false));
1186
+ if (node.type === VueNodeTypes.COMMENT)
1187
+ edit(node.loc.start.offset, node.loc.end.offset, '<!--' + scrubPayload(node.loc.source.slice(4, -3)) + '-->');
1188
+ if (node.type === VueNodeTypes.INTERPOLATION &&
1189
+ node.content.type === VueNodeTypes.SIMPLE_EXPRESSION)
1190
+ edit(node.content.loc.start.offset, node.content.loc.end.offset, syntaxPayload(node.content.loc.source, false, true, node.content.content));
1191
+ if (node.type !== VueNodeTypes.ELEMENT)
1192
+ return;
1193
+ for (const prop of node.props) {
1194
+ if (prop.type === VueNodeTypes.ATTRIBUTE && prop.value) {
1195
+ const raw = prop.value.loc.source;
1196
+ const quoted = raw.startsWith('"') || raw.startsWith("'");
1197
+ edit(prop.value.loc.start.offset, prop.value.loc.end.offset, quoted
1198
+ ? raw.slice(0, 1) +
1199
+ literalPayload(raw.slice(1, -1), SECRET_LABEL_RE.test(prop.name)) +
1200
+ raw.slice(-1)
1201
+ : literalPayload(raw, SECRET_LABEL_RE.test(prop.name)));
1202
+ }
1203
+ if (prop.type !== VueNodeTypes.DIRECTIVE)
1204
+ continue;
1205
+ if (prop.exp?.type === VueNodeTypes.SIMPLE_EXPRESSION)
1206
+ edit(prop.exp.loc.start.offset, prop.exp.loc.end.offset, syntaxPayload(prop.exp.loc.source, prop.arg?.type === VueNodeTypes.SIMPLE_EXPRESSION &&
1207
+ prop.arg.isStatic &&
1208
+ SECRET_LABEL_RE.test(prop.arg.content), true, prop.exp.content));
1209
+ if (prop.arg?.type === VueNodeTypes.SIMPLE_EXPRESSION && !prop.arg.isStatic)
1210
+ edit(prop.arg.loc.start.offset, prop.arg.loc.end.offset, syntaxPayload(prop.arg.loc.source, false, true, prop.arg.content));
1211
+ }
1212
+ });
1213
+ for (const style of descriptor.styles) {
1214
+ const clean = scrubPayload(style.content);
1215
+ // No CSS/preprocessor AST proof is claimed. Changed opaque content blocks graph publication.
1216
+ if (clean !== style.content)
1217
+ faithful = false;
1218
+ edit(style.loc.start.offset, style.loc.end.offset, clean);
1219
+ }
1220
+ // Descriptor gaps contain outer tags/comments, not executable script/template bodies.
1221
+ // Policy still applies there; altered opaque boundaries cannot establish graph fidelity.
1222
+ const blocks = [script, descriptor.template, ...descriptor.styles]
1223
+ .filter((b) => b !== null && b !== undefined)
1224
+ .sort((a, b) => a.loc.start.offset - b.loc.start.offset);
1225
+ let gapStart = 0;
1226
+ for (const block of blocks) {
1227
+ const gap = text.slice(gapStart, block.loc.start.offset);
1228
+ const clean = scrubPayload(gap);
1229
+ if (clean !== gap)
1230
+ faithful = false;
1231
+ edit(gapStart, block.loc.start.offset, clean);
1232
+ gapStart = block.loc.end.offset;
1233
+ }
1234
+ const tail = text.slice(gapStart);
1235
+ const cleanTail = scrubPayload(tail);
1236
+ if (cleanTail !== tail)
1237
+ faithful = false;
1238
+ edit(gapStart, text.length, cleanTail);
1239
+ edits.sort((a, b) => b.start - a.start);
1240
+ let clean = text;
1241
+ let boundary = text.length;
1242
+ for (const e of edits) {
1243
+ if (e.end > boundary)
1244
+ return fallback();
1245
+ clean = clean.slice(0, e.start) + e.clean + clean.slice(e.end);
1246
+ boundary = e.start;
1247
+ }
1248
+ if ('reason' in (await parseVueSource(clean)))
1249
+ faithful = false;
1250
+ return faithful || clean.startsWith(VUE_FIDELITY_MARKER) ? clean : VUE_FIDELITY_MARKER + clean;
1251
+ }
1252
+ catch {
1253
+ return fallback();
1254
+ }
1255
+ finally {
1256
+ parser.delete();
1257
+ }
1258
+ }
629
1259
  //# sourceMappingURL=sourceRedaction.js.map