@contrast/assess 1.78.0 → 1.79.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.
@@ -49,6 +49,8 @@ module.exports = function(core) {
49
49
  require('./install/path')(core);
50
50
  require('./install/reg-exp-prototype-exec')(core);
51
51
  require('./install/joi')(core);
52
+ require('./install/zod/v4')(core);
53
+ require('./install/zod/v3')(core);
52
54
  require('./install/util-format')(core);
53
55
 
54
56
  propagation.install = function() {
@@ -16,67 +16,20 @@
16
16
  'use strict';
17
17
 
18
18
  const {
19
- isString,
20
19
  primordials: {
21
20
  ArrayPrototypeSlice,
22
- StringPrototypeReplace,
23
- StringPrototypeMatch,
24
- StringPrototypeMatchAll,
25
- StringPrototypeSlice,
26
21
  }
27
22
  } = require('@contrast/common');
28
- const crypto = require('crypto');
29
23
  const { createMergedTags, getAdjustedUntrackedValue, truncateStringValue } = require('../../../tag-utils');
30
24
  const { patchType } = require('../../common');
31
-
32
- function makeCanary() {
33
- return StringPrototypeReplace.call(
34
- crypto
35
- .randomBytes(12)
36
- .toString('base64'),
37
- /[+\\]/g,
38
- (match) => match === '+' ? '%' : '#'
39
- );
40
- }
41
-
42
- const canary = makeCanary();
43
- const regex = new RegExp(`"${canary}(\\d+)~`, 'g');
44
- function identity(_key, val) {
45
- return val;
46
- }
47
-
48
- function patchReplacer(replacer) {
49
- if (Array.isArray(replacer)) {
50
- // keys that are numbers get cast to string when they come through the replacer
51
- // add '' to the list of keys as that's always the first iteration of replacer, the entire json string
52
- const filterKeys = replacer
53
- .filter((key) => isString(key) || typeof key === 'number')
54
- .concat('')
55
- .map((value) => value.toString());
56
-
57
- let nextArray = [];
58
- replacer = function(key, value) {
59
- // we need to keep track of arrays
60
- // and shift each element as the replacer hits it
61
- // so we can compare those values are expected
62
- // when filtering
63
- const nextEl = nextArray.shift();
64
- if (Array.isArray(value)) {
65
- nextArray = [...value];
66
- }
67
-
68
- if (filterKeys.includes(key) || value === nextEl) {
69
- return value;
70
- }
71
- };
72
- } else if (typeof replacer === 'function') {
73
- // do nothing!
74
- } else {
75
- replacer = identity;
76
- }
77
-
78
- return replacer;
79
- }
25
+ const {
26
+ regex,
27
+ patchReplacer,
28
+ getUntrustedSpaceProps,
29
+ createSpaceTagRanges,
30
+ markCanaryValue,
31
+ applyCanaryTagRanges,
32
+ } = require('./tag-ranges');
80
33
 
81
34
  module.exports = function(core) {
82
35
  const {
@@ -88,77 +41,6 @@ module.exports = function(core) {
88
41
  }
89
42
  } = core;
90
43
 
91
- function getUntrustedSpaceProps(space) {
92
- // it can't be a problem if it's not a string, if the string is all spaces, or
93
- // if the string is zero length.
94
- if (!isString(space) || StringPrototypeMatch.call(space, /^\s+$/) || !space) {
95
- return null;
96
- }
97
- const props = tracker.getData(StringPrototypeSlice.call(space, 0, 10));
98
- if (!props || !Object.keys(props.tags).length) {
99
- return null;
100
- }
101
-
102
- return props;
103
- }
104
-
105
-
106
- function createSpaceTagRanges(result, metadata) {
107
- const tags = {};
108
- const spaceValue = StringPrototypeSlice.call(metadata.origArgs[2], 0, 10);
109
- const { spaceProps: { tags: spacePropsTags } } = metadata;
110
-
111
- const spaces = Array.from(StringPrototypeMatchAll.call(result, new RegExp(`(?<=\\n)(${spaceValue})+?(?=\\d|"|null|]|})`, 'g')));
112
-
113
- for (const space of spaces) {
114
- const match = space[0];
115
- const { index } = space;
116
- for (const tagKey of Object.keys(spacePropsTags)) {
117
- if (!tags[tagKey]) {
118
- tags[tagKey] = [];
119
- }
120
-
121
- if (space !== spaceValue && (spacePropsTags[tagKey].length !== 2 || spacePropsTags[tagKey][1] !== spaceValue.length - 1)) {
122
- for (let i = 0; i < match.length / spaceValue.length; i++) {
123
- tags[tagKey].push(...spacePropsTags[tagKey].map((rangeIdx) => index + (rangeIdx + i * spaceValue.length)));
124
- }
125
- } else {
126
- tags[tagKey].push(index, index + match.length - 1);
127
- }
128
- }
129
- }
130
-
131
- return tags;
132
- }
133
-
134
- function applyCanaryTagRanges(result, metadata, tags) {
135
- metadata.sourceEvents = [];
136
- // the diff doesn't include the " that opens the regex.
137
- let canaryReplacementDiff = 0;
138
- // for each marker in the stringify's result, remove the marker and
139
- // create a TagRange for the value.
140
- return StringPrototypeReplace.call(result, metadata.regex, (m, id, offset) => {
141
- // adjust offset by total characters removed so far
142
- offset = offset - canaryReplacementDiff + 1;
143
- // we don't replace the opening " in the regex - that just makes sure
144
- // we only find our marker at the start of a string value.
145
- canaryReplacementDiff += m.length - 1;
146
- for (const tagKey of Object.keys(metadata.strInfos[id].tags)) {
147
- if (!tags[tagKey]) {
148
- tags[tagKey] = [];
149
- }
150
-
151
- for (const tagIdx of metadata.strInfos[id].tags[tagKey]) {
152
- tags[tagKey].push(tagIdx + offset);
153
- }
154
- }
155
-
156
- metadata.history.add(metadata.strInfos[id]);
157
- // replace the canary with the starting quote
158
- return '"';
159
- });
160
- }
161
-
162
44
  return core.assess.dataflow.propagation.jsonInstrumentation.stringify = {
163
45
  install() {
164
46
  patcher.patch(JSON, 'stringify', {
@@ -186,16 +68,13 @@ module.exports = function(core) {
186
68
 
187
69
  // if the space can't be trusted then remember space's tracking info so it
188
70
  // can be applied to the entire JSON.stringify result string.
189
- const spaceProps = getUntrustedSpaceProps(space);
71
+ const spaceProps = getUntrustedSpaceProps(tracker, space);
190
72
  if (spaceProps) {
191
73
  data.metadata.spaceProps = spaceProps;
192
74
  data.metadata.history.add(spaceProps);
193
75
  data.metadata.propagate = true;
194
76
  }
195
77
 
196
- // each canary tag gets a separate id to make matching easier.
197
- let id = 0;
198
-
199
78
  /**
200
79
  * wraps the replacer method and updates propagate boolean if value of replacer
201
80
  * is tracked
@@ -204,20 +83,14 @@ module.exports = function(core) {
204
83
  * @param {string} value
205
84
  */
206
85
  function contrastReplacer(key, val) {
207
- const valProperties = tracker.getData(val);
208
- if (valProperties && Object.keys(valProperties.tags).length) {
86
+ const value = replacer.call(this, key, val);
87
+ const marked = markCanaryValue(tracker, data.metadata.strInfos, val, value);
88
+
89
+ if (marked.propagate) {
209
90
  data.metadata.propagate = true;
210
91
  }
211
92
 
212
- let value = replacer.call(this, key, val);
213
-
214
- // keys aren't propagated, so check only the value
215
- if (isString(value) && valProperties) {
216
- data.metadata.strInfos[id] = valProperties;
217
- value = `${canary}${id}~${value}`;
218
- id++;
219
- }
220
- return value;
93
+ return marked.value;
221
94
  }
222
95
 
223
96
  data.args = [input, contrastReplacer, space];
@@ -0,0 +1,174 @@
1
+ /*
2
+ * Copyright: 2026 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const {
19
+ isString,
20
+ primordials: {
21
+ StringPrototypeReplace,
22
+ StringPrototypeMatch,
23
+ StringPrototypeMatchAll,
24
+ StringPrototypeSlice,
25
+ }
26
+ } = require('@contrast/common');
27
+ const crypto = require('crypto');
28
+
29
+ // Shared functions between JSON.stringify instrumentation
30
+ // and util.format when 'j' format string is used
31
+ // for keeping accurate tag ranges
32
+
33
+ function makeCanary() {
34
+ return StringPrototypeReplace.call(
35
+ crypto
36
+ .randomBytes(12)
37
+ .toString('base64'),
38
+ /[+\\]/g,
39
+ (match) => match === '+' ? '%' : '#'
40
+ );
41
+ }
42
+
43
+ const canary = makeCanary();
44
+ const regex = new RegExp(`"${canary}(\\d+)~`, 'g');
45
+
46
+ function identity(_key, val) {
47
+ return val;
48
+ }
49
+
50
+ function patchReplacer(replacer) {
51
+ if (Array.isArray(replacer)) {
52
+ // keys that are numbers get cast to string when they come through the replacer
53
+ // add '' to the list of keys as that's always the first iteration of replacer, the entire json string
54
+ const filterKeys = replacer
55
+ .filter((key) => isString(key) || typeof key === 'number')
56
+ .concat('')
57
+ .map((value) => value.toString());
58
+
59
+ let nextArray = [];
60
+ replacer = function(key, value) {
61
+ // we need to keep track of arrays
62
+ // and shift each element as the replacer hits it
63
+ // so we can compare those values are expected
64
+ // when filtering
65
+ const nextEl = nextArray.shift();
66
+ if (Array.isArray(value)) {
67
+ nextArray = [...value];
68
+ }
69
+
70
+ if (filterKeys.includes(key) || value === nextEl) {
71
+ return value;
72
+ }
73
+ };
74
+ } else if (typeof replacer === 'function') {
75
+ // do nothing!
76
+ } else {
77
+ replacer = identity;
78
+ }
79
+
80
+ return replacer;
81
+ }
82
+
83
+ function getUntrustedSpaceProps(tracker, space) {
84
+ // it can't be a problem if it's not a string, if the string is all spaces, or
85
+ // if the string is zero length.
86
+ if (!isString(space) || StringPrototypeMatch.call(space, /^\s+$/) || !space) {
87
+ return null;
88
+ }
89
+ const props = tracker.getData(StringPrototypeSlice.call(space, 0, 10));
90
+ if (!props || !Object.keys(props.tags).length) {
91
+ return null;
92
+ }
93
+
94
+ return props;
95
+ }
96
+
97
+ function createSpaceTagRanges(result, metadata) {
98
+ const tags = {};
99
+ const spaceValue = StringPrototypeSlice.call(metadata.origArgs[2], 0, 10);
100
+ const { spaceProps: { tags: spacePropsTags } } = metadata;
101
+
102
+ const spaces = Array.from(StringPrototypeMatchAll.call(result, new RegExp(`(?<=\\n)(${spaceValue})+?(?=\\d|"|null|]|})`, 'g')));
103
+
104
+ for (const space of spaces) {
105
+ const match = space[0];
106
+ const { index } = space;
107
+ for (const tagKey of Object.keys(spacePropsTags)) {
108
+ if (!tags[tagKey]) {
109
+ tags[tagKey] = [];
110
+ }
111
+
112
+ if (space !== spaceValue && (spacePropsTags[tagKey].length !== 2 || spacePropsTags[tagKey][1] !== spaceValue.length - 1)) {
113
+ for (let i = 0; i < match.length / spaceValue.length; i++) {
114
+ tags[tagKey].push(...spacePropsTags[tagKey].map((rangeIdx) => index + (rangeIdx + i * spaceValue.length)));
115
+ }
116
+ } else {
117
+ tags[tagKey].push(index, index + match.length - 1);
118
+ }
119
+ }
120
+ }
121
+
122
+ return tags;
123
+ }
124
+
125
+ function markCanaryValue(tracker, strInfos, val, value) {
126
+ const valInfo = tracker.getData(val);
127
+ const propagate = !!(valInfo && Object.keys(valInfo.tags).length);
128
+
129
+ if (isString(value) && valInfo) {
130
+ const id = Object.keys(strInfos).length;
131
+ strInfos[id] = valInfo;
132
+ value = `${canary}${id}~${value}`;
133
+ }
134
+
135
+ return { value, propagate };
136
+ }
137
+
138
+ function applyCanaryTagRanges(result, metadata, tags) {
139
+ metadata.sourceEvents = [];
140
+ // the diff doesn't include the " that opens the regex.
141
+ let canaryReplacementDiff = 0;
142
+ // for each marker in the stringify's result, remove the marker and
143
+ // create a TagRange for the value.
144
+ return StringPrototypeReplace.call(result, metadata.regex, (m, id, offset) => {
145
+ // adjust offset by total characters removed so far
146
+ offset = offset - canaryReplacementDiff + 1;
147
+ // we don't replace the opening " in the regex - that just makes sure
148
+ // we only find our marker at the start of a string value.
149
+ canaryReplacementDiff += m.length - 1;
150
+ for (const tagKey of Object.keys(metadata.strInfos[id].tags)) {
151
+ if (!tags[tagKey]) {
152
+ tags[tagKey] = [];
153
+ }
154
+
155
+ for (const tagIdx of metadata.strInfos[id].tags[tagKey]) {
156
+ tags[tagKey].push(tagIdx + offset);
157
+ }
158
+ }
159
+
160
+ metadata.history.add(metadata.strInfos[id]);
161
+ // replace the canary with the starting quote
162
+ return '"';
163
+ });
164
+ }
165
+
166
+ module.exports = {
167
+ regex,
168
+ identity,
169
+ patchReplacer,
170
+ getUntrustedSpaceProps,
171
+ createSpaceTagRanges,
172
+ markCanaryValue,
173
+ applyCanaryTagRanges,
174
+ };
@@ -15,7 +15,7 @@
15
15
  */
16
16
  'use strict';
17
17
 
18
- const { kComponentName, ComponentBase } = require('@contrast/core/lib/ioc/core');
18
+ const { kComponentName, ComponentBase } = require('@contrast/core');
19
19
  const { isString, set, traverseValues, primordials } = require('@contrast/common');
20
20
  const { createFullLengthCopyTags } = require('../../tag-utils');
21
21
  const { patchType } = require('../common');
@@ -18,7 +18,7 @@
18
18
  const {
19
19
  primordials: { StringPrototypeSubstr },
20
20
  DataflowTag: { CUSTOM_VALIDATED_REFLECTED_XSS } } = require('@contrast/common');
21
- const { kComponentName, ComponentBase } = require('@contrast/core/lib/ioc/core');
21
+ const { kComponentName, ComponentBase } = require('@contrast/core');
22
22
  const { patchType } = require('../common');
23
23
 
24
24
  const COMPONENT_NAME = 'assess.dataflow.propagation.sanitizeHtml';
@@ -16,8 +16,11 @@
16
16
  'use strict';
17
17
 
18
18
  const { patchType } = require('../../common');
19
+ const { createSubsetTags, createAppendTags } = require('../../../tag-utils');
19
20
 
20
- const { primordials: { StringPrototypeSplit } } = require('@contrast/common');
21
+ const {
22
+ primordials: { StringPrototypeSplit }
23
+ } = require('@contrast/common');
21
24
 
22
25
  module.exports = function(core) {
23
26
  const {
@@ -49,7 +52,12 @@ module.exports = function(core) {
49
52
  ]
50
53
  ];
51
54
 
52
- function getPropagationEvent(strInfo, partInfo, data) {
55
+ function calculateSubsetRange(tags, start, len) {
56
+ const clampedStart = Math.max(start, 0);
57
+ return createSubsetTags(tags, clampedStart, start + len - clampedStart);
58
+ }
59
+
60
+ function getPropagationEvent(url, tags, data) {
53
61
  const args = data.args.map((v) => {
54
62
  const strInfo = tracker.getData(v);
55
63
  return {
@@ -63,7 +71,7 @@ module.exports = function(core) {
63
71
  moduleName: 'url',
64
72
  methodName: 'URL',
65
73
  get context() {
66
- return `url.URL('${strInfo.value}')`;
74
+ return `url.URL('${url}')`;
67
75
  },
68
76
  object: {
69
77
  value: 'url',
@@ -74,8 +82,8 @@ module.exports = function(core) {
74
82
  tracked: true
75
83
  },
76
84
  args,
77
- tags: partInfo.tags,
78
- history: [partInfo],
85
+ tags,
86
+ history: [{ tags, value: url }],
79
87
  source: 'P',
80
88
  target: 'R',
81
89
  stacktraceOpts: {
@@ -98,28 +106,47 @@ module.exports = function(core) {
98
106
  if (!result || !args[0] || !getPropagatorContext()) return;
99
107
 
100
108
  const [input, basename] = args;
109
+ const inputInfo = tracker.getData(input);
110
+ const basenameInfo = tracker.getData(basename);
111
+
101
112
  let url = input;
113
+ let tags = inputInfo?.tags ?? null;
114
+
102
115
  if (basename && url !== result.toString()) {
103
116
  const isAbsoluteURL = url.indexOf('://') > 0 || url.indexOf('//') === 0;
104
117
  if (isAbsoluteURL) {
105
- const splitUrl = url.split(new RegExp('(?=//)', 'g'));
118
+ // input is split into chunks so any leading slashes on the input
119
+ // can be replaced with the basename's protocol when needed. Each
120
+ // chunk's tag range is calculated from its offset within `input`.
121
+ const splitUrl = StringPrototypeSplit.call(url, new RegExp('(?=//)', 'g'));
106
122
  let endOfSlashes = splitUrl.length === 1 ? true : false;
107
- let newUrl = splitUrl[0] === result.protocol ? splitUrl[0] : basename.split(/(?<=:)/)[0];
123
+ const useInputProtocol = splitUrl[0] === result.protocol;
124
+ let newUrl = useInputProtocol ? splitUrl[0] : StringPrototypeSplit.call(basename, /(?<=:)/)[0];
125
+ let newTags = useInputProtocol
126
+ ? createSubsetTags(inputInfo?.tags, 0, newUrl.length)
127
+ : createSubsetTags(basenameInfo?.tags, 0, newUrl.length);
128
+
129
+ let inputOffset = 0;
108
130
  for (let i = 0; i < splitUrl.length; i++) {
131
+ const piece = splitUrl[i];
109
132
  if (endOfSlashes) {
110
- newUrl = newUrl.concat(splitUrl[i]);
133
+ const pieceTags = createSubsetTags(inputInfo?.tags, inputOffset, piece.length);
134
+ newTags = createAppendTags(newTags, pieceTags, newUrl.length);
135
+ newUrl += piece;
111
136
  }
112
- if (splitUrl[i] === '/' && splitUrl[i + 1] !== '/') {
137
+ if (piece === '/' && splitUrl[i + 1] !== '/') {
113
138
  endOfSlashes = true;
114
139
  }
140
+ inputOffset += piece.length;
115
141
  }
116
142
  url = newUrl;
143
+ tags = newTags;
117
144
  } else {
118
- url = basename.concat(url);
145
+ tags = createAppendTags(basenameInfo?.tags, inputInfo?.tags, basename.length);
146
+ url = basename + url;
119
147
  }
120
148
  }
121
- const strInfo = tracker.getData(url);
122
- if (!strInfo) return;
149
+ if (!tags) return;
123
150
 
124
151
  const proxy = new Proxy(data.result, {
125
152
  set(obj, prop, value) {
@@ -139,31 +166,38 @@ module.exports = function(core) {
139
166
  }
140
167
  });
141
168
 
142
- const traverse = function(href, url, keys, idx = 0) {
169
+ const traverse = function(href, hrefTags, urlObj, keys, idx = 0) {
143
170
  let substr = href;
171
+ let substrTags = hrefTags;
144
172
  keys.forEach((key) => {
145
173
  if (typeof key === 'string') {
146
- const part = url[key];
174
+ const part = urlObj[key];
147
175
  if (part !== 'null') {
176
+ let partTags;
148
177
 
149
178
  if (key === 'origin') {
150
179
  const [protocol, originWithoutProtocol] = StringPrototypeSplit.call(part, new RegExp('(?<=//)'));
151
180
  const idx1 = href.indexOf(protocol);
152
181
  const idx2 = href.indexOf(originWithoutProtocol, idx - 1);
153
- substr = href.substring(idx1, idx1 + protocol.length).concat(href.substring(idx2, idx2 + originWithoutProtocol.length));
182
+ partTags = createAppendTags(
183
+ calculateSubsetRange(hrefTags, idx1, protocol.length),
184
+ calculateSubsetRange(hrefTags, idx2, originWithoutProtocol.length),
185
+ protocol.length
186
+ );
154
187
  } else {
155
188
  const index = href.indexOf(part, idx - 1);
156
- substr = href.substring(index, index + part.length);
189
+ partTags = calculateSubsetRange(hrefTags, index, part.length);
157
190
  idx += part.length;
158
191
  }
159
192
 
160
- const partInfo = tracker.getData(substr);
161
- if (!partInfo) return;
193
+ substr = part;
194
+ substrTags = partTags;
195
+
196
+ if (!partTags) return;
162
197
 
163
- const event = getPropagationEvent(strInfo, partInfo, data);
198
+ const event = getPropagationEvent(url, partTags, data);
164
199
  if (!event) return;
165
200
 
166
- Object.assign(partInfo, event);
167
201
  const { extern } = tracker.track(part, event);
168
202
 
169
203
  if (extern) {
@@ -171,12 +205,12 @@ module.exports = function(core) {
171
205
  }
172
206
  }
173
207
  } else {
174
- traverse(substr, url, key, 0);
208
+ traverse(substr, substrTags, urlObj, key, 0);
175
209
  }
176
210
  });
177
211
  };
178
212
 
179
- traverse(url, result, keys, 0);
213
+ traverse(url, tags, result, keys, 0);
180
214
 
181
215
  if (proxy.search) {
182
216
  proxy._contrast_searchParams = new URLSearchParams(proxy.search);
@@ -18,10 +18,12 @@
18
18
  const { isString, primordials: { StringPrototypeMatch } } = require('@contrast/common');
19
19
  const { createAppendTags } = require('../../tag-utils');
20
20
  const { patchType } = require('../common');
21
+ const { regex, markCanaryValue, applyCanaryTagRanges } = require('./JSON/tag-ranges');
21
22
 
22
23
  module.exports = function(core) {
23
24
  const {
24
25
  patcher,
26
+ scopes,
25
27
  depHooks,
26
28
  assess: {
27
29
  inspect,
@@ -53,6 +55,32 @@ module.exports = function(core) {
53
55
  return tags;
54
56
  }
55
57
 
58
+ function stringifyWithTags(value) {
59
+ const strInfos = {};
60
+ let propagate = false;
61
+
62
+ function markCanaries(_key, val) {
63
+ const marked = markCanaryValue(tracker, strInfos, val, val);
64
+ if (marked.propagate) propagate = true;
65
+ return marked.value;
66
+ }
67
+
68
+ const result = scopes.instrumentation.run({ lock: true }, () => JSON.stringify(value, markCanaries));
69
+ if (!propagate || result === undefined) {
70
+ return { value: result, tags: null, history: [] };
71
+ }
72
+
73
+ const tags = {};
74
+ const metadata = { regex, strInfos, history: new Set() };
75
+ const taggedResult = applyCanaryTagRanges(result, metadata, tags);
76
+
77
+ return {
78
+ value: taggedResult,
79
+ tags: Object.keys(tags).length ? tags : null,
80
+ history: Array.from(metadata.history),
81
+ };
82
+ }
83
+
56
84
  return core.assess.dataflow.propagation.utilFormat = {
57
85
  install() {
58
86
  depHooks.resolve({ name: 'util', version: '*' }, (util) => {
@@ -97,9 +125,21 @@ module.exports = function(core) {
97
125
  case 'f':
98
126
  // won't be tracked
99
127
  break;
100
- case 'j':
101
- arg = JSON.stringify(arg) ?? 'undefined';
102
- break;
128
+ case 'j': {
129
+ const stringified = stringifyWithTags(arg);
130
+ arg = stringified.value ?? 'undefined';
131
+
132
+ if (!stringified.tags) break;
133
+
134
+ const currIdx = result.indexOf(arg, idx);
135
+ if (currIdx === -1) break;
136
+
137
+ idx = currIdx + arg.length;
138
+ newTags = createAppendTags(newTags, stringified.tags, currIdx);
139
+ stringified.history.forEach((info) => history.push({ ...info }));
140
+ eventArgs.push({ value: arg, tracked: true });
141
+ continue;
142
+ }
103
143
  case 'o':
104
144
  break; // handled below
105
145
  case 'O':