node-avo-inspector 2.0.0-beta.1 → 2.0.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.
@@ -2,52 +2,365 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EventValidator = void 0;
4
4
  const safe_regex2_1 = require("safe-regex2");
5
+ const AvoInspector_1 = require("../AvoInspector");
6
+ // Validation covers a property (depth 0) and the properties of an object or of each object in
7
+ // a list (depth 1); deeper objects are not checked, as in the web and Android SDKs.
8
+ const MAX_CHILD_DEPTH = 2;
9
+ // Compiled patterns and parsed allowed-value lists, keyed by their wire string. Both come from
10
+ // the tracking plan, so they stay few; the bound only guards a long-lived process against a
11
+ // plan that keeps changing. null marks a pattern that was skipped (unsafe or invalid).
12
+ const MAX_CACHED_CONSTRAINTS = 1000;
13
+ // Validation runs synchronously on the track path, so a value past these bounds is not
14
+ // checked: a list with more items, or a string longer than this against a regex (safe-regex2
15
+ // rejects exponential patterns, not quadratic ones).
16
+ const MAX_VALIDATED_LIST_ITEMS = 1000;
17
+ const MAX_REGEX_INPUT_LENGTH = 10000;
18
+ const regexCache = new Map();
19
+ const allowedValuesCache = new Map();
20
+ /**
21
+ * Checks event property values against the value constraints of an event spec: pinned values,
22
+ * allowed values, regex patterns and min/max ranges, on a property and on the properties of an
23
+ * object (or of each object in a list).
24
+ *
25
+ * Each constraint applies to the tracking-plan event IDs (base event and variants) the spec
26
+ * lists for it, so a value can pass for one variant and fail for another. Types and required
27
+ * properties are not checked here; Avo compares the reported schema with the plan for those.
28
+ */
5
29
  class EventValidator {
6
30
  /**
7
- * Validates event properties against the spec.
8
- * Returns per-property validation results with bandwidth optimization:
9
- * passedEventIds is returned only when strictly smaller than failedEventIds.
31
+ * Returns one result per property that failed for at least one event ID, or whose object
32
+ * properties did. Properties the spec has no constraints for are left out.
10
33
  */
11
- validate(spec, eventProperties, eventId) {
34
+ validate(spec, eventSchema, rawEventProperties) {
35
+ const constraintsByName = collectConstraintsByName(spec.events);
12
36
  const results = [];
13
- const eventPropMap = new Map();
14
- for (const prop of eventProperties) {
15
- eventPropMap.set(prop.propertyName, prop);
16
- }
17
- for (const specProp of spec.properties) {
18
- const actualProp = eventPropMap.get(specProp.propertyName);
19
- let passed = false;
20
- if (actualProp) {
21
- // Type check
22
- passed = actualProp.propertyType === specProp.propertyType;
23
- // Regex check (only if type passed, regex is defined, and value is available)
24
- if (passed && specProp.regex && actualProp.propertyValue !== undefined) {
25
- if (!(0, safe_regex2_1.default)(specProp.regex)) {
26
- console.warn(`[Avo Inspector] Warning: Unsafe regex pattern skipped for property "${specProp.propertyName}": ${specProp.regex}`);
27
- }
28
- else {
29
- try {
30
- const re = new RegExp(specProp.regex);
31
- passed = re.test(actualProp.propertyValue);
32
- }
33
- catch (e) {
34
- console.warn(`[Avo Inspector] Warning: Invalid regex pattern for property "${specProp.propertyName}": ${specProp.regex}`);
35
- }
36
- }
37
- }
37
+ for (const { propertyName } of eventSchema) {
38
+ const constraints = constraintsByName.get(propertyName);
39
+ if (constraints === undefined) {
40
+ continue;
38
41
  }
39
- const result = {
40
- propertyName: specProp.propertyName,
41
- failedEventIds: passed ? [] : [eventId],
42
- passedEventIds: passed ? [eventId] : [],
43
- };
44
- // Bandwidth optimization: return passedEventIds only when strictly smaller than failedEventIds
45
- if (result.passedEventIds.length >= result.failedEventIds.length) {
46
- result.passedEventIds = [];
42
+ const value = rawEventProperties ? ownValue(rawEventProperties, propertyName) : undefined;
43
+ const outcome = validateProperty(value, constraints, 0);
44
+ if (outcome !== null) {
45
+ results.push(Object.assign({ propertyName }, outcome));
47
46
  }
48
- results.push(result);
49
47
  }
50
48
  return results;
51
49
  }
52
50
  }
53
51
  exports.EventValidator = EventValidator;
52
+ // =============================================================================
53
+ // Constraints across events
54
+ // =============================================================================
55
+ /**
56
+ * One lookup of constraints per property name across every event in the spec. Name mapping
57
+ * can match several events; each constraint keeps its own event IDs, so merging is a union.
58
+ */
59
+ function collectConstraintsByName(events) {
60
+ const result = new Map();
61
+ for (const event of events) {
62
+ for (const name of Object.keys(event.props)) {
63
+ const constraints = event.props[name];
64
+ const existing = result.get(name);
65
+ result.set(name, existing === undefined ? constraints : mergeConstraints(existing, constraints));
66
+ }
67
+ }
68
+ return result;
69
+ }
70
+ function mergeConstraints(a, b) {
71
+ const merged = {
72
+ type: a.type,
73
+ // A missing value fails only where every matching event requires the property.
74
+ required: a.required && b.required,
75
+ };
76
+ if (a.isList || b.isList) {
77
+ merged.isList = true;
78
+ }
79
+ const pinnedValues = mergeIdMappings(a.pinnedValues, b.pinnedValues);
80
+ if (pinnedValues) {
81
+ merged.pinnedValues = pinnedValues;
82
+ }
83
+ const allowedValues = mergeIdMappings(a.allowedValues, b.allowedValues);
84
+ if (allowedValues) {
85
+ merged.allowedValues = allowedValues;
86
+ }
87
+ const regexPatterns = mergeIdMappings(a.regexPatterns, b.regexPatterns);
88
+ if (regexPatterns) {
89
+ merged.regexPatterns = regexPatterns;
90
+ }
91
+ const minMaxRanges = mergeIdMappings(a.minMaxRanges, b.minMaxRanges);
92
+ if (minMaxRanges) {
93
+ merged.minMaxRanges = minMaxRanges;
94
+ }
95
+ if (a.children || b.children) {
96
+ const children = Object.create(null);
97
+ for (const source of [a.children, b.children]) {
98
+ if (!source) {
99
+ continue;
100
+ }
101
+ for (const name of Object.keys(source)) {
102
+ const existing = children[name];
103
+ children[name] = existing === undefined ? source[name] : mergeConstraints(existing, source[name]);
104
+ }
105
+ }
106
+ merged.children = children;
107
+ }
108
+ return merged;
109
+ }
110
+ function mergeIdMappings(a, b) {
111
+ if (!a || !b) {
112
+ return a !== null && a !== void 0 ? a : b;
113
+ }
114
+ const merged = Object.create(null);
115
+ for (const source of [a, b]) {
116
+ for (const key of Object.keys(source)) {
117
+ const ids = merged[key];
118
+ merged[key] = ids === undefined ? source[key] : Array.from(new Set([...ids, ...source[key]]));
119
+ }
120
+ }
121
+ return merged;
122
+ }
123
+ // =============================================================================
124
+ // Validation
125
+ // =============================================================================
126
+ function validateProperty(value, constraints, depth) {
127
+ if (depth >= MAX_CHILD_DEPTH) {
128
+ return null;
129
+ }
130
+ if (constraints.isList) {
131
+ return validateList(value, constraints, depth);
132
+ }
133
+ if (constraints.children) {
134
+ return validateObject(value, constraints.children, depth);
135
+ }
136
+ // An optional property sent as null or undefined has no value to check.
137
+ if ((value === null || value === undefined) && !constraints.required) {
138
+ return null;
139
+ }
140
+ return outcomeOf(checkValue(value, constraints, new Set()));
141
+ }
142
+ /**
143
+ * A list of objects is checked property by property across every item: an event ID fails a
144
+ * child property when any item's value fails it. A list of primitives is checked item by item.
145
+ * A value that is not an array is not checked (its type is reported in the schema).
146
+ */
147
+ function validateList(value, constraints, depth) {
148
+ if (!Array.isArray(value)) {
149
+ return null;
150
+ }
151
+ if (value.length > MAX_VALIDATED_LIST_ITEMS) {
152
+ if (AvoInspector_1.AvoInspector.shouldLog) {
153
+ console.warn(`[Avo Inspector] Warning: List of ${value.length} items not validated (limit ${MAX_VALIDATED_LIST_ITEMS})`);
154
+ }
155
+ return null;
156
+ }
157
+ if (constraints.children) {
158
+ const children = Object.create(null);
159
+ let failed = false;
160
+ for (const name of Object.keys(constraints.children)) {
161
+ const failedIds = new Set();
162
+ for (const item of value) {
163
+ const outcome = validateProperty(childValue(item, name), constraints.children[name], depth + 1);
164
+ if (outcome !== null) {
165
+ outcome.failedEventIds.forEach((id) => failedIds.add(id));
166
+ }
167
+ }
168
+ const outcome = outcomeOf(failedIds);
169
+ if (outcome !== null) {
170
+ children[name] = outcome;
171
+ failed = true;
172
+ }
173
+ }
174
+ return failed ? { failedEventIds: [], children } : null;
175
+ }
176
+ const failedIds = new Set();
177
+ for (const item of value) {
178
+ checkValue(item, constraints, failedIds);
179
+ }
180
+ return outcomeOf(failedIds);
181
+ }
182
+ function validateObject(value, childConstraints, depth) {
183
+ const children = Object.create(null);
184
+ let failed = false;
185
+ for (const name of Object.keys(childConstraints)) {
186
+ const outcome = validateProperty(childValue(value, name), childConstraints[name], depth + 1);
187
+ if (outcome !== null) {
188
+ children[name] = outcome;
189
+ failed = true;
190
+ }
191
+ }
192
+ return failed ? { failedEventIds: [], children } : null;
193
+ }
194
+ function outcomeOf(failedIds) {
195
+ return failedIds.size > 0 ? { failedEventIds: Array.from(failedIds) } : null;
196
+ }
197
+ // A property of a plain object (null-prototype included); anything else has none.
198
+ function childValue(container, name) {
199
+ if (container === null || typeof container !== "object" || Array.isArray(container)) {
200
+ return undefined;
201
+ }
202
+ return ownValue(container, name);
203
+ }
204
+ // Own properties only, so "constructor" or "toString" never reads a prototype method.
205
+ function ownValue(container, name) {
206
+ return Object.prototype.hasOwnProperty.call(container, name) ? container[name] : undefined;
207
+ }
208
+ /** Adds to failedIds the event IDs whose constraints the value breaks. */
209
+ function checkValue(value, constraints, failedIds) {
210
+ if (constraints.pinnedValues || constraints.allowedValues) {
211
+ const actual = comparableString(value);
212
+ if (constraints.pinnedValues) {
213
+ checkPinnedValues(actual, constraints.pinnedValues, failedIds);
214
+ }
215
+ if (constraints.allowedValues) {
216
+ checkAllowedValues(actual, constraints.allowedValues, failedIds);
217
+ }
218
+ }
219
+ if (constraints.regexPatterns) {
220
+ checkRegexPatterns(value, constraints.regexPatterns, failedIds);
221
+ }
222
+ if (constraints.minMaxRanges) {
223
+ checkMinMaxRanges(value, constraints.minMaxRanges, failedIds);
224
+ }
225
+ return failedIds;
226
+ }
227
+ function addAll(ids, failedIds) {
228
+ for (const id of ids) {
229
+ failedIds.add(id);
230
+ }
231
+ }
232
+ /**
233
+ * The string a value is compared as against pinned and allowed values: String() for a
234
+ * primitive, JSON for an object or list. A value with neither (a cyclic object, a throwing
235
+ * toJSON or toString) falls back to its tag, e.g. "[object Object]".
236
+ */
237
+ function comparableString(value) {
238
+ if (value !== null && typeof value === "object") {
239
+ try {
240
+ const json = JSON.stringify(value);
241
+ if (json !== undefined) {
242
+ return json;
243
+ }
244
+ }
245
+ catch (e) {
246
+ // Falls through to String().
247
+ }
248
+ }
249
+ try {
250
+ return String(value);
251
+ }
252
+ catch (e) {
253
+ return Object.prototype.toString.call(value);
254
+ }
255
+ }
256
+ function checkPinnedValues(actual, pinnedValues, failedIds) {
257
+ for (const pinned of Object.keys(pinnedValues)) {
258
+ if (actual !== pinned) {
259
+ addAll(pinnedValues[pinned], failedIds);
260
+ }
261
+ }
262
+ }
263
+ function checkAllowedValues(actual, allowedValues, failedIds) {
264
+ for (const json of Object.keys(allowedValues)) {
265
+ const allowed = parseAllowedValues(json);
266
+ // A list that is not a JSON array of strings is skipped: nothing fails it.
267
+ if (allowed !== null && !allowed.has(actual)) {
268
+ addAll(allowedValues[json], failedIds);
269
+ }
270
+ }
271
+ }
272
+ function checkRegexPatterns(value, regexPatterns, failedIds) {
273
+ if (typeof value === "string" && value.length > MAX_REGEX_INPUT_LENGTH) {
274
+ if (AvoInspector_1.AvoInspector.shouldLog) {
275
+ console.warn(`[Avo Inspector] Warning: String of ${value.length} characters not checked against regex patterns (limit ${MAX_REGEX_INPUT_LENGTH})`);
276
+ }
277
+ return;
278
+ }
279
+ for (const pattern of Object.keys(regexPatterns)) {
280
+ // Only a string can match; an unsafe or invalid pattern is skipped (nothing fails it).
281
+ if (typeof value !== "string") {
282
+ addAll(regexPatterns[pattern], failedIds);
283
+ continue;
284
+ }
285
+ const regex = compileRegex(pattern);
286
+ if (regex !== null && !regex.test(value)) {
287
+ addAll(regexPatterns[pattern], failedIds);
288
+ }
289
+ }
290
+ }
291
+ /**
292
+ * "min,max" with either bound possibly empty: "0," has no maximum, ",100" no minimum. Only a
293
+ * number (or bigint) can be in range; NaN never is. A range that does not parse is skipped.
294
+ */
295
+ function checkMinMaxRanges(value, minMaxRanges, failedIds) {
296
+ const actual = typeof value === "number" ? value : typeof value === "bigint" ? Number(value) : NaN;
297
+ for (const range of Object.keys(minMaxRanges)) {
298
+ const bounds = parseRange(range);
299
+ if (bounds === null) {
300
+ if (AvoInspector_1.AvoInspector.shouldLog) {
301
+ console.warn(`[Avo Inspector] Warning: Invalid min/max range skipped: ${range}`);
302
+ }
303
+ continue;
304
+ }
305
+ if (Number.isNaN(actual) || actual < bounds.min || actual > bounds.max) {
306
+ addAll(minMaxRanges[range], failedIds);
307
+ }
308
+ }
309
+ }
310
+ function parseRange(range) {
311
+ const parts = range.split(",");
312
+ if (parts.length !== 2) {
313
+ return null;
314
+ }
315
+ const [minPart, maxPart] = parts.map((part) => part.trim());
316
+ const min = minPart === "" ? -Infinity : Number(minPart);
317
+ const max = maxPart === "" ? Infinity : Number(maxPart);
318
+ return Number.isNaN(min) || Number.isNaN(max) ? null : { min, max };
319
+ }
320
+ function parseAllowedValues(json) {
321
+ const cached = allowedValuesCache.get(json);
322
+ if (cached !== undefined) {
323
+ return cached;
324
+ }
325
+ let allowed = null;
326
+ try {
327
+ const parsed = JSON.parse(json);
328
+ if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
329
+ allowed = new Set(parsed);
330
+ }
331
+ }
332
+ catch (e) {
333
+ // Left null: skipped below.
334
+ }
335
+ if (allowed === null && AvoInspector_1.AvoInspector.shouldLog) {
336
+ console.warn(`[Avo Inspector] Warning: Invalid allowed values skipped: ${json}`);
337
+ }
338
+ cacheConstraint(allowedValuesCache, json, allowed);
339
+ return allowed;
340
+ }
341
+ function compileRegex(pattern) {
342
+ const cached = regexCache.get(pattern);
343
+ if (cached !== undefined) {
344
+ return cached;
345
+ }
346
+ let regex = null;
347
+ if (!(0, safe_regex2_1.default)(pattern)) {
348
+ console.warn(`[Avo Inspector] Warning: Unsafe regex pattern skipped: ${pattern}`);
349
+ }
350
+ else {
351
+ try {
352
+ regex = new RegExp(pattern);
353
+ }
354
+ catch (e) {
355
+ console.warn(`[Avo Inspector] Warning: Invalid regex pattern skipped: ${pattern}`);
356
+ }
357
+ }
358
+ cacheConstraint(regexCache, pattern, regex);
359
+ return regex;
360
+ }
361
+ function cacheConstraint(cache, key, value) {
362
+ if (cache.size >= MAX_CACHED_CONSTRAINTS) {
363
+ cache.clear();
364
+ }
365
+ cache.set(key, value);
366
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export { AvoInspector } from "./AvoInspector";
2
+ export type { InspectorEvent } from "./AvoInspector";
2
3
  export { AvoInspectorEnv } from "./AvoInspectorEnv";
4
+ export { VERSION, SPEC_VERSION } from "./AvoInspectorVersion";
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AvoInspectorEnv = exports.AvoInspector = void 0;
3
+ exports.SPEC_VERSION = exports.VERSION = exports.AvoInspectorEnv = exports.AvoInspector = void 0;
4
4
  var AvoInspector_1 = require("./AvoInspector");
5
5
  Object.defineProperty(exports, "AvoInspector", { enumerable: true, get: function () { return AvoInspector_1.AvoInspector; } });
6
6
  var AvoInspectorEnv_1 = require("./AvoInspectorEnv");
7
7
  Object.defineProperty(exports, "AvoInspectorEnv", { enumerable: true, get: function () { return AvoInspectorEnv_1.AvoInspectorEnv; } });
8
+ var AvoInspectorVersion_1 = require("./AvoInspectorVersion");
9
+ Object.defineProperty(exports, "VERSION", { enumerable: true, get: function () { return AvoInspectorVersion_1.VERSION; } });
10
+ Object.defineProperty(exports, "SPEC_VERSION", { enumerable: true, get: function () { return AvoInspectorVersion_1.SPEC_VERSION; } });
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,31 @@
1
+ /** True for null, undefined, or a string that is empty after trimming. */
1
2
  declare const isValueEmpty: (value: string | null | undefined) => boolean;
3
+ /**
4
+ * Structural equality, within the schema extraction limits: objects and lists nested more
5
+ * than 10 levels deep, past the 10,000th compared in one call, or past 10,000 compared
6
+ * properties, are treated as not equal (unless they are the same reference), so a huge
7
+ * payload cannot overflow the stack or block. Binary data is compared by its bytes, never
8
+ * enumerated.
9
+ */
2
10
  declare function deepEquals(x: any, y: any): boolean;
3
- export { isValueEmpty, deepEquals };
11
+ /** A schema as printed in debug logs: `{`, one `\t"name": "type"` per property (`;`-separated), `}`. */
12
+ declare const formatSchema: (schema: Array<{
13
+ propertyName: string;
14
+ propertyType: string;
15
+ }>) => string;
16
+ /** Trims a gateway option value; anything that is not a non-blank string is absent. */
17
+ declare const normalizeOption: (value: unknown) => string | undefined;
18
+ /**
19
+ * No control character (Unicode Cc: C0, DEL, C1) may reach a header value; CR, LF and NUL
20
+ * would split or end the header field. Tab is the one exception: it is valid in a value.
21
+ */
22
+ declare const hasHeaderControlChar: (value: string) => boolean;
23
+ /** Node sends header values as Latin-1 and throws synchronously on anything above U+00FF. */
24
+ declare const hasNonLatin1Char: (value: string) => boolean;
25
+ /**
26
+ * Milliseconds from a monotonic clock (process.hrtime): only differences are meaningful, and
27
+ * they never jump with wall-clock changes. Shared by the deduplicator, AvoLog and the exit
28
+ * drain deadline.
29
+ */
30
+ declare const monotonicNowMs: () => number;
31
+ export { monotonicNowMs, isValueEmpty, deepEquals, formatSchema, normalizeOption, hasHeaderControlChar, hasNonLatin1Char };
package/dist/utils.js CHANGED
@@ -1,26 +1,73 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isValueEmpty = void 0;
3
+ exports.hasNonLatin1Char = exports.hasHeaderControlChar = exports.normalizeOption = exports.formatSchema = exports.isValueEmpty = exports.monotonicNowMs = void 0;
4
4
  exports.deepEquals = deepEquals;
5
+ /** True for null, undefined, or a string that is empty after trimming. */
5
6
  const isValueEmpty = (value) => {
6
7
  return value === null || value === undefined || value.trim().length == 0;
7
8
  };
8
9
  exports.isValueEmpty = isValueEmpty;
10
+ // The schema extraction limits: deepEquals expands no deeper and no more than this.
11
+ const DEEP_EQUALS_MAX_DEPTH = 10;
12
+ const DEEP_EQUALS_MAX_EXPANSIONS = 10000;
13
+ const DEEP_EQUALS_MAX_PROPERTIES = 10000;
14
+ // Binary data (an ArrayBuffer view, ArrayBuffer or SharedArrayBuffer) as bytes, or null.
15
+ const binaryBytes = (value) => {
16
+ if (ArrayBuffer.isView(value)) {
17
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
18
+ }
19
+ const tag = Object.prototype.toString.call(value);
20
+ return tag === "[object ArrayBuffer]" || tag === "[object SharedArrayBuffer]" ? new Uint8Array(value) : null;
21
+ };
22
+ const sameBytes = (a, b) => a.byteLength === b.byteLength && Buffer.compare(a, b) === 0;
23
+ /**
24
+ * Structural equality, within the schema extraction limits: objects and lists nested more
25
+ * than 10 levels deep, past the 10,000th compared in one call, or past 10,000 compared
26
+ * properties, are treated as not equal (unless they are the same reference), so a huge
27
+ * payload cannot overflow the stack or block. Binary data is compared by its bytes, never
28
+ * enumerated.
29
+ */
9
30
  function deepEquals(x, y) {
31
+ return deepEqualsWithin(x, y, 0, new Map(), { expansions: 0, properties: 0 });
32
+ }
33
+ // `comparing` holds the object pairs already compared, so cyclic structures terminate: a
34
+ // pair met again is assumed equal (its other properties are still compared where it was
35
+ // first met).
36
+ function deepEqualsWithin(x, y, depth, comparing, budget) {
10
37
  if (x === y) {
11
38
  return true;
12
39
  }
13
- if (!(x instanceof Object) || !(y instanceof Object)) {
40
+ // typeof, not instanceof Object: a null-prototype object is not an instance of Object.
41
+ if (x === null || y === null || typeof x !== "object" || typeof y !== "object") {
14
42
  return false;
15
43
  }
16
- if (x.constructor !== y.constructor) {
44
+ if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y)) {
17
45
  return false;
18
46
  }
19
- for (var p in x) {
20
- if (!x.hasOwnProperty(p)) {
21
- continue;
47
+ const xBytes = binaryBytes(x);
48
+ if (xBytes !== null) {
49
+ const yBytes = binaryBytes(y);
50
+ return yBytes !== null && sameBytes(xBytes, yBytes);
51
+ }
52
+ let partners = comparing.get(x);
53
+ if (partners && partners.has(y)) {
54
+ return true;
55
+ }
56
+ if (depth >= DEEP_EQUALS_MAX_DEPTH || budget.expansions >= DEEP_EQUALS_MAX_EXPANSIONS) {
57
+ return false;
58
+ }
59
+ budget.expansions++;
60
+ if (!partners) {
61
+ partners = new Set();
62
+ comparing.set(x, partners);
63
+ }
64
+ partners.add(y);
65
+ // hasOwn, not x.hasOwnProperty: user objects may lack the method or shadow it.
66
+ for (const p of Object.keys(x)) {
67
+ if (++budget.properties > DEEP_EQUALS_MAX_PROPERTIES) {
68
+ return false;
22
69
  }
23
- if (!y.hasOwnProperty(p)) {
70
+ if (!hasOwn(y, p)) {
24
71
  return false;
25
72
  }
26
73
  if (x[p] === y[p]) {
@@ -29,14 +76,43 @@ function deepEquals(x, y) {
29
76
  if (typeof x[p] !== "object") {
30
77
  return false;
31
78
  }
32
- if (!deepEquals(x[p], y[p])) {
79
+ if (!deepEqualsWithin(x[p], y[p], depth + 1, comparing, budget)) {
33
80
  return false;
34
81
  }
35
82
  }
36
- for (p in y) {
37
- if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) {
83
+ for (const p of Object.keys(y)) {
84
+ if (!hasOwn(x, p)) {
38
85
  return false;
39
86
  }
40
87
  }
41
88
  return true;
42
89
  }
90
+ /** A schema as printed in debug logs: `{`, one `\t"name": "type"` per property (`;`-separated), `}`. */
91
+ const formatSchema = (schema) => "{\n" + schema.map((p) => '\t"' + p.propertyName + '": "' + p.propertyType + '"').join(";\n") + "\n}";
92
+ exports.formatSchema = formatSchema;
93
+ const hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
94
+ /** Trims a gateway option value; anything that is not a non-blank string is absent. */
95
+ const normalizeOption = (value) => {
96
+ if (typeof value !== "string") {
97
+ return undefined;
98
+ }
99
+ const trimmed = value.trim();
100
+ return trimmed.length > 0 ? trimmed : undefined;
101
+ };
102
+ exports.normalizeOption = normalizeOption;
103
+ /**
104
+ * No control character (Unicode Cc: C0, DEL, C1) may reach a header value; CR, LF and NUL
105
+ * would split or end the header field. Tab is the one exception: it is valid in a value.
106
+ */
107
+ const hasHeaderControlChar = (value) => /[\u0000-\u0008\u000A-\u001F\u007F-\u009F]/.test(value);
108
+ exports.hasHeaderControlChar = hasHeaderControlChar;
109
+ /** Node sends header values as Latin-1 and throws synchronously on anything above U+00FF. */
110
+ const hasNonLatin1Char = (value) => /[^\u0000-\u00FF]/.test(value);
111
+ exports.hasNonLatin1Char = hasNonLatin1Char;
112
+ /**
113
+ * Milliseconds from a monotonic clock (process.hrtime): only differences are meaningful, and
114
+ * they never jump with wall-clock changes. Shared by the deduplicator, AvoLog and the exit
115
+ * drain deadline.
116
+ */
117
+ const monotonicNowMs = () => Number(process.hrtime.bigint()) / 1e6;
118
+ exports.monotonicNowMs = monotonicNowMs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-avo-inspector",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0",
4
4
  "description": "Avo Inspector for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,6 +24,7 @@
24
24
  "homepage": "https://github.com/avohq/node-avo-inspector#readme",
25
25
  "scripts": {
26
26
  "build": "tsc --outDir dist",
27
+ "prepack": "rm -rf dist && npm run build",
27
28
  "test": "jest",
28
29
  "typecheck": "tsc --noEmit"
29
30
  },