@mnemonik/shared 7.142.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 (66) 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 +47 -1
  7. package/dist/ast/grammars.js.map +1 -1
  8. package/dist/codeScanner.d.ts +6 -0
  9. package/dist/codeScanner.d.ts.map +1 -1
  10. package/dist/codeScanner.js +12 -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 +1013 -21
  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/sql.tags.scm +5 -0
  28. package/queries/swift.tags.scm +11 -0
  29. package/queries/terraform.tags.scm +8 -0
  30. package/scripts/jsx-bare-ampersand.patch +61 -0
  31. package/scripts/vendor-grammars.ts +731 -37
  32. package/src/ast/astChunker.ts +10 -1
  33. package/src/ast/grammars.ts +44 -2
  34. package/src/codeScanner.ts +12 -0
  35. package/src/index.ts +7 -0
  36. package/src/sourceRedaction.ts +1099 -21
  37. package/src/vueSource.ts +77 -0
  38. package/wasm/bash.wasm +0 -0
  39. package/wasm/cmake.LICENSE +21 -0
  40. package/wasm/cmake.tags.scm +6 -0
  41. package/wasm/cmake.wasm +0 -0
  42. package/wasm/cpp.LICENSE +21 -0
  43. package/wasm/cpp.wasm +0 -0
  44. package/wasm/groovy.LICENSE +25 -0
  45. package/wasm/groovy.wasm +0 -0
  46. package/wasm/javascript.wasm +0 -0
  47. package/wasm/lua.LICENSE +21 -0
  48. package/wasm/lua.tags.scm +36 -0
  49. package/wasm/lua.wasm +0 -0
  50. package/wasm/nix.LICENSE +21 -0
  51. package/wasm/nix.tags.scm +4 -0
  52. package/wasm/nix.wasm +0 -0
  53. package/wasm/objcpp.LICENSE +28 -0
  54. package/wasm/objcpp.tags.scm +4 -0
  55. package/wasm/objcpp.wasm +0 -0
  56. package/wasm/sql.LICENSE +21 -0
  57. package/wasm/sql.tags.scm +5 -0
  58. package/wasm/sql.wasm +0 -0
  59. package/wasm/swift.LICENSE +21 -0
  60. package/wasm/swift.tags.scm +11 -0
  61. package/wasm/swift.wasm +0 -0
  62. package/wasm/terraform.LICENSE +201 -0
  63. package/wasm/terraform.tags.scm +8 -0
  64. package/wasm/terraform.wasm +0 -0
  65. package/wasm/tsx.wasm +0 -0
  66. 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,45 +11,895 @@ 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
- : 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
+ };
24
72
  if (!text || !language || Buffer.byteLength(text, 'utf8') > MAX_AST_PARSE_BYTES) {
25
- return scrubPayload(text);
73
+ return fallback();
26
74
  }
27
75
  let parser;
28
76
  let tree = null;
29
77
  try {
30
78
  const grammar = await loadGrammar(language);
31
79
  if ('unavailable' in grammar)
32
- return scrubPayload(text);
80
+ return fallback();
33
81
  parser = new Parser();
34
82
  parser.setLanguage(grammar.ok.language);
35
83
  tree = parser.parse(text);
36
84
  if (!tree || tree.rootNode.hasError)
37
- return scrubPayload(text);
85
+ return fallback();
38
86
  const root = tree.rootNode;
39
- const redacted = language === 'bash'
40
- ? redactShellNode(root, commandLineCredentialRanges(text))
41
- : 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);
42
112
  return text.slice(0, root.startIndex) + redacted + text.slice(root.endIndex);
43
113
  }
44
114
  catch {
45
- return scrubPayload(text);
115
+ return fallback();
46
116
  }
47
117
  finally {
48
118
  tree?.delete();
49
119
  parser?.delete();
50
120
  }
51
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
+ }
557
+ /** Rust raw/byte prefixes belong to the delimiter, never to the credential payload. */
558
+ function redactRustNode(node, sensitive = false, labeled = new Set()) {
559
+ sensitive ||= labeled.has(node.id);
560
+ const original = node.text;
561
+ const type = node.type;
562
+ if (type === 'line_comment')
563
+ return '//' + scrubPayload(original.slice(2));
564
+ if (type === 'block_comment')
565
+ return '/*' + scrubPayload(original.slice(2, -2)) + '*/';
566
+ if (sensitive && ['integer_literal', 'float_literal', 'char_literal'].includes(type))
567
+ return '"' + redactSecretValue(original) + '"';
568
+ if (type === 'string_literal' || type === 'raw_string_literal') {
569
+ const start = original.indexOf('"') + 1;
570
+ const end = original.lastIndexOf('"');
571
+ return (original.slice(0, start) +
572
+ literalPayload(original.slice(start, end), sensitive) +
573
+ original.slice(end));
574
+ }
575
+ const label = (key) => {
576
+ if (!key)
577
+ return false;
578
+ const name = (key.childForFieldName('field') ?? key).text;
579
+ return SECRET_LABEL_RE.test(['string_literal', 'raw_string_literal'].includes(key.type)
580
+ ? name.slice(name.indexOf('"') + 1, name.lastIndexOf('"'))
581
+ : name.replace(/^r#/, ''));
582
+ };
583
+ const mark = (pattern, value) => {
584
+ if (!pattern || !value)
585
+ return;
586
+ if (['ref_pattern', 'mut_pattern', 'reference_pattern'].includes(pattern.type)) {
587
+ mark(pattern.firstNamedChild, value);
588
+ return;
589
+ }
590
+ if (label(pattern))
591
+ labeled.add(value.id);
592
+ else if (pattern.type === 'tuple_pattern' && value.type === 'tuple_expression')
593
+ pattern.namedChildren.forEach((part, index) => mark(part, value.namedChildren[index] ?? null));
594
+ };
595
+ if (type === 'let_declaration')
596
+ mark(node.childForFieldName('pattern'), node.childForFieldName('value'));
597
+ if (type === 'const_item' || type === 'static_item')
598
+ mark(node.childForFieldName('name'), node.childForFieldName('value'));
599
+ if (type === 'field_initializer')
600
+ mark(node.childForFieldName('field'), node.childForFieldName('value'));
601
+ if (type === 'assignment_expression')
602
+ mark(node.childForFieldName('left'), node.childForFieldName('right'));
603
+ // Attribute/macro arguments are token trees, but literal key/value boundaries remain explicit.
604
+ if (type === 'token_tree') {
605
+ const children = node.children.filter((child) => !['line_comment', 'block_comment'].includes(child.type));
606
+ let valueSensitive = sensitive;
607
+ let genericDepth = 0;
608
+ for (const [index, child] of children.entries()) {
609
+ const previous = children[index - 1];
610
+ // Token trees flatten type arguments too. Their commas and associated-type
611
+ // bindings are not macro value boundaries; const arguments are not values.
612
+ if (child.text === '<' &&
613
+ (genericDepth > 0 || ['::', '=', ':', '('].includes(previous?.text ?? ''))) {
614
+ genericDepth++;
615
+ continue;
616
+ }
617
+ if (genericDepth > 0) {
618
+ if (child.text === '>' || child.text === '>>')
619
+ genericDepth -= child.text.length;
620
+ genericDepth = Math.max(0, genericDepth);
621
+ continue;
622
+ }
623
+ const nextKey = children[index + 1];
624
+ const nextBinding = nextKey &&
625
+ ['identifier', 'string_literal', 'raw_string_literal'].includes(nextKey.type) &&
626
+ ['=', ':'].includes(children[index + 2]?.text ?? '');
627
+ if (child.text === ';' || (child.text === ',' && nextBinding))
628
+ valueSensitive = sensitive;
629
+ else if (['=', ':'].includes(child.text) && label(previous ?? null))
630
+ valueSensitive = true;
631
+ else if (valueSensitive) {
632
+ const callArguments = child.type === 'token_tree' &&
633
+ child.text.startsWith('(') &&
634
+ (previous?.type === 'identifier' ||
635
+ ['>', ')', ']'].includes(previous?.text.at(-1) ?? ''));
636
+ const key = children[index + 1]?.text === ':';
637
+ if (!callArguments && !key)
638
+ labeled.add(child.id);
639
+ }
640
+ }
641
+ }
642
+ let result = '';
643
+ let offset = node.startIndex;
644
+ for (const child of node.namedChildren) {
645
+ result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
646
+ // Carry a secret label through value wrappers, never into runtime call arguments/conditions.
647
+ const carry = sensitive &&
648
+ ([
649
+ 'parenthesized_expression',
650
+ 'unary_expression',
651
+ 'binary_expression',
652
+ 'field_expression',
653
+ 'reference_expression',
654
+ 'try_expression',
655
+ 'await_expression',
656
+ 'block',
657
+ 'expression_statement',
658
+ 'else_clause',
659
+ 'tuple_expression',
660
+ 'array_expression',
661
+ ].includes(type) ||
662
+ (type === 'call_expression' && child.id === node.childForFieldName('function')?.id) ||
663
+ (type === 'match_expression' && child.id === node.childForFieldName('body')?.id) ||
664
+ type === 'match_block' ||
665
+ (type === 'match_arm' && child.id === node.childForFieldName('value')?.id) ||
666
+ (type === 'type_cast_expression' && child.id === node.childForFieldName('value')?.id) ||
667
+ (type === 'if_expression' && child.id !== node.childForFieldName('condition')?.id));
668
+ result += redactRustNode(child, carry, labeled);
669
+ offset = child.endIndex;
670
+ }
671
+ return result + original.slice(offset - node.startIndex);
672
+ }
673
+ /** Lua exposes payload spans for both quoted and level-matched long-bracket literals. */
674
+ function redactLuaNode(node, sensitive = false, labeled = new Set()) {
675
+ sensitive ||= labeled.has(node.id);
676
+ const original = node.text;
677
+ if (node.type === 'string' || node.type === 'comment') {
678
+ const content = node.childForFieldName('content');
679
+ if (!content)
680
+ return original;
681
+ const suffix = original.slice(content.endIndex - node.startIndex);
682
+ let payload = node.type === 'comment'
683
+ ? scrubPayload(content.text)
684
+ : literalPayload(content.text, sensitive);
685
+ if (payload !== content.text && suffix.startsWith(']')) {
686
+ // A placeholder must not introduce a closing long bracket, including at the boundary.
687
+ payload = payload.split(suffix).join('] ' + suffix.slice(1));
688
+ if ((payload + suffix).indexOf(suffix) < payload.length)
689
+ payload += ' ';
690
+ }
691
+ return original.slice(0, content.startIndex - node.startIndex) + payload + suffix;
692
+ }
693
+ if (node.type === 'number' && sensitive)
694
+ return "'" + redactSecretValue(original) + "'";
695
+ const label = (key) => {
696
+ if (key?.type === 'bracket_index_expression') {
697
+ const field = key.childForFieldName('field');
698
+ return field?.type === 'string' && secretLabel(field);
699
+ }
700
+ return secretLabel(key?.type === 'dot_index_expression' ? key.childForFieldName('field') : key);
701
+ };
702
+ if (node.type === 'assignment_statement') {
703
+ const names = node.namedChildren.find((child) => child.type === 'variable_list')?.namedChildren ?? [];
704
+ const values = node.namedChildren.find((child) => child.type === 'expression_list')?.namedChildren ?? [];
705
+ values.forEach((value, index) => {
706
+ if (label(names[index] ?? null))
707
+ labeled.add(value.id);
708
+ });
709
+ }
710
+ else if (node.type === 'field' && label(node.childForFieldName('name'))) {
711
+ const value = node.childForFieldName('value');
712
+ if (value)
713
+ labeled.add(value.id);
714
+ }
715
+ const carry = sensitive &&
716
+ (node.type === 'parenthesized_expression' ||
717
+ node.type === 'unary_expression' ||
718
+ (node.type === 'binary_expression' &&
719
+ ['..', 'and', 'or'].includes(node.childForFieldName('operator')?.text ?? '')));
720
+ let result = '';
721
+ let offset = node.startIndex;
722
+ for (const child of node.namedChildren) {
723
+ result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
724
+ result += redactLuaNode(child, carry, labeled);
725
+ offset = child.endIndex;
726
+ }
727
+ return result + original.slice(offset - node.startIndex);
728
+ }
729
+ function sqlSecretLabel(node) {
730
+ if (!node)
731
+ return false;
732
+ if (['field', 'object_reference', 'column'].includes(node.type))
733
+ return sqlSecretLabel(node.childForFieldName('name') ?? node.firstNamedChild);
734
+ // This dialect-neutral grammar sometimes calls PostgreSQL quoted identifiers literals.
735
+ if (node.type === 'identifier' || (node.type === 'literal' && node.text.startsWith('"')))
736
+ return SECRET_LABEL_RE.test(node.text.replace(/^"|"$/g, '').replaceAll('""', '"'));
737
+ return false;
738
+ }
739
+ function redactSqlNode(node, sensitive = false) {
740
+ if (node.type === 'create_function') {
741
+ const language = node.namedChildren.find((child) => child.type === 'function_language');
742
+ const body = node.namedChildren.find((child) => child.type === 'function_body');
743
+ // Opaque/procedural bodies have no established inner syntax; the caller uses fallback.
744
+ if (language?.namedChildren.find((child) => child.type === 'identifier')?.text.toLowerCase() !==
745
+ 'sql' ||
746
+ body?.namedChildren.some((child) => child.type === 'literal'))
747
+ throw new Error('Opaque SQL function body');
748
+ }
749
+ const original = node.text;
750
+ if (node.type === 'comment')
751
+ return '--' + scrubPayload(original.slice(2));
752
+ if (node.type === 'marginalia')
753
+ return '/*' + scrubPayload(original.slice(2, -2)) + '*/';
754
+ if (node.type === 'literal') {
755
+ if (original.startsWith('"'))
756
+ return original; // PostgreSQL identifier, never a secret value.
757
+ const opening = /^(?:[eEnNbBxX]|[uU]&)?'|^\$[A-Za-z_0-9]*\$/.exec(original)?.[0];
758
+ if (opening) {
759
+ const closing = opening.startsWith('$') ? opening : "'";
760
+ if (!original.endsWith(closing))
761
+ throw new Error('Incomplete SQL literal');
762
+ const payload = original.slice(opening.length, -closing.length);
763
+ // Scrub the whole payload on a match: SQL doubled quotes must not leave a secret suffix.
764
+ const redacted = literalPayload(payload, sensitive);
765
+ return opening + (redacted === payload ? payload : redactSecretValue(payload)) + closing;
766
+ }
767
+ if (node.namedChildren.some((child) => child.type.startsWith('keyword_')))
768
+ return original;
769
+ if (/^[+\-\d.]/.test(original))
770
+ return sensitive ? "'" + redactSecretValue(original) + "'" : original;
771
+ // shortcut: unrecognized typed literals use fallback until their boundaries are established.
772
+ throw new Error('Unsupported SQL literal');
773
+ }
774
+ const left = node.childForFieldName('left');
775
+ const right = node.childForFieldName('right');
776
+ const labeled = ['assignment', 'binary_expression'].includes(node.type) && sqlSecretLabel(left);
777
+ const columnLabel = node.type === 'column_definition' && sqlSecretLabel(node.childForFieldName('name'));
778
+ const argumentLabel = node.type === 'function_argument' &&
779
+ sqlSecretLabel(node.namedChildren.find((child) => child.type === 'identifier') ?? null);
780
+ const aliasLabel = node.type === 'term' && sqlSecretLabel(node.childForFieldName('alias'));
781
+ const insert = node.type === 'list' && node.parent?.type === 'insert' ? node.parent : null;
782
+ const columns = insert?.namedChildren.find((child) => child.type === 'list');
783
+ const columnValues = columns?.namedChildren.some((child) => child.type === 'column') && columns.id !== node.id
784
+ ? columns.namedChildren.filter((child) => !['comment', 'marginalia'].includes(child.type))
785
+ : [];
786
+ let result = '';
787
+ let offset = node.startIndex;
788
+ let index = 0;
789
+ let previous = null;
790
+ for (const child of node.namedChildren) {
791
+ result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
792
+ const carry = sensitive &&
793
+ ((node.type === 'binary_expression' && node.childForFieldName('operator')?.text === '||') ||
794
+ (node.type === 'cast' &&
795
+ child.id ===
796
+ (node.childForFieldName('parameter') ??
797
+ node.namedChildren.find((part) => !['comment', 'marginalia'].includes(part.type)))?.id) ||
798
+ ['list', 'array', 'parenthesized_expression'].includes(node.type) ||
799
+ (node.type === 'case' &&
800
+ (previous?.type === 'keyword_then' || previous?.type === 'keyword_else')));
801
+ result += redactSqlNode(child, carry ||
802
+ (labeled && child.id === right?.id) ||
803
+ (node.type === 'binary_expression' && sqlSecretLabel(right) && child.id === left?.id) ||
804
+ (columnLabel && previous?.type === 'keyword_default') ||
805
+ (argumentLabel && child.type === 'literal') ||
806
+ (aliasLabel && child.id === node.childForFieldName('value')?.id) ||
807
+ child.id === node.childForFieldName('password')?.id ||
808
+ sqlSecretLabel(columnValues[index] ?? null));
809
+ if (!['comment', 'marginalia'].includes(child.type)) {
810
+ previous = child;
811
+ index++;
812
+ }
813
+ offset = child.endIndex;
814
+ }
815
+ return result + original.slice(offset - node.startIndex);
816
+ }
817
+ function swiftSecretLabel(node) {
818
+ if (!node)
819
+ return false;
820
+ if (node.type.endsWith('string_literal')) {
821
+ const opening = /^(#+)?("""|")/.exec(node.text);
822
+ return Boolean(opening && SECRET_LABEL_RE.test(node.text.slice(opening[0].length, -opening[0].length)));
823
+ }
824
+ if (['simple_identifier', 'type_identifier'].includes(node.type))
825
+ return SECRET_LABEL_RE.test(node.text.replace(/^`|`$/g, ''));
826
+ return swiftSecretLabel(node.childForFieldName('suffix') ??
827
+ node.childForFieldName('bound_identifier') ??
828
+ node.childForFieldName('name') ??
829
+ node.firstNamedChild);
830
+ }
831
+ function redactSwiftNode(node, sensitive = false) {
832
+ const original = node.text;
833
+ if (node.type === 'comment' || node.type === 'multiline_comment') {
834
+ // Swift permits nested block comments; retain every delimiter from the parsed comment.
835
+ return original
836
+ .split(/(\/\*|\*\/)/)
837
+ .map((part) => (part === '/*' || part === '*/' ? part : scrubPayload(part)))
838
+ .join('');
839
+ }
840
+ if (['line_string_literal', 'multi_line_string_literal', 'raw_string_literal'].includes(node.type)) {
841
+ const opening = /^(#+)?("""|")/.exec(original);
842
+ if (!opening)
843
+ throw new Error('Swift string delimiter unavailable');
844
+ const closing = opening[2] + (opening[1] ?? '');
845
+ if (!original.endsWith(closing))
846
+ throw new Error('Swift string is incomplete');
847
+ const payload = (text) => {
848
+ if (opening[2] !== '"""')
849
+ return literalPayload(text, sensitive);
850
+ // Keep required opening newlines/closing indentation; scrub multiline secrets together.
851
+ const leading = /^\s*/.exec(text)?.[0] ?? '';
852
+ const body = text.slice(leading.length);
853
+ const trailing = /\s*$/.exec(body)?.[0] ?? '';
854
+ return (leading + literalPayload(body.slice(0, body.length - trailing.length), sensitive) + trailing);
855
+ };
856
+ let result = opening[0];
857
+ let offset = node.startIndex + opening[0].length;
858
+ for (const child of node.children) {
859
+ if (!['\\(', ')', 'interpolated_expression', 'raw_str_interpolation'].includes(child.type))
860
+ continue;
861
+ result += payload(original.slice(offset - node.startIndex, child.startIndex - node.startIndex));
862
+ result += redactSwiftNode(child);
863
+ offset = child.endIndex;
864
+ }
865
+ return result + payload(original.slice(offset - node.startIndex, -closing.length)) + closing;
866
+ }
867
+ if (sensitive && ['integer_literal', 'real_literal'].includes(node.type))
868
+ return '"' + redactSecretValue(original) + '"';
869
+ let labeled = node.type === 'assignment'
870
+ ? swiftSecretLabel(node.childForFieldName('target'))
871
+ : node.type === 'value_argument'
872
+ ? swiftSecretLabel(node.childForFieldName('name'))
873
+ : false;
874
+ let result = '';
875
+ let offset = node.startIndex;
876
+ for (const [index, child] of node.children.entries()) {
877
+ const field = node.fieldNameForChild(index);
878
+ if ((node.type === 'property_declaration' && field === 'name') ||
879
+ (node.type === 'dictionary_literal' && field === 'key') ||
880
+ child.type === 'parameter')
881
+ labeled = swiftSecretLabel(child);
882
+ const value = (field === 'value' &&
883
+ ['property_declaration', 'dictionary_literal', 'value_argument'].includes(node.type)) ||
884
+ (field === 'result' && node.type === 'assignment') ||
885
+ field === 'default_value';
886
+ const carry = sensitive &&
887
+ ([
888
+ 'tuple_expression',
889
+ 'additive_expression',
890
+ 'nil_coalescing_expression',
891
+ 'try_expression',
892
+ 'await_expression',
893
+ ].includes(node.type) ||
894
+ (node.type === 'as_expression' && field === 'expr') ||
895
+ (node.type === 'prefix_expression' && field === 'target') ||
896
+ (node.type === 'ternary_expression' && field !== 'condition'));
897
+ result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
898
+ result += redactSwiftNode(child, carry || (labeled && value));
899
+ offset = child.endIndex;
900
+ }
901
+ return result + original.slice(offset - node.startIndex);
902
+ }
52
903
  function redactShellNode(root, ranges) {
53
904
  for (const range of ranges) {
54
905
  let literal = root.descendantForIndex(range.start, range.start);
@@ -241,7 +1092,11 @@ function redactNode(node, sensitive = false) {
241
1092
  result += original.slice(offset - node.startIndex, child.startIndex - node.startIndex);
242
1093
  // Carry labels through value wrappers, never through call arguments or conditions.
243
1094
  const carry = sensitive &&
244
- (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' ||
245
1100
  node.type === 'binary_operator' ||
246
1101
  node.type === 'unary_operator' ||
247
1102
  (node.type === 'call' && child.id === node.childForFieldName('function')?.id) ||
@@ -264,4 +1119,141 @@ function redactNode(node, sensitive = false) {
264
1119
  }
265
1120
  return result + original.slice(offset - node.startIndex);
266
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
+ }
267
1259
  //# sourceMappingURL=sourceRedaction.js.map