node-avo-inspector 1.1.0 → 1.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/.claude/settings.local.json +11 -0
  3. package/.claude/worktrees/release-1.1.1/LICENSE +21 -0
  4. package/.claude/worktrees/release-1.1.1/README.md +69 -0
  5. package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/index.js +30 -0
  6. package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/package.json +15 -0
  7. package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/yarn.lock +6 -0
  8. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/dist/index.js +34 -0
  9. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/index.ts +30 -0
  10. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/package.json +18 -0
  11. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/tsconfig.json +15 -0
  12. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/yarn.lock +11 -0
  13. package/.claude/worktrees/release-1.1.1/package-lock.json +4039 -0
  14. package/.claude/worktrees/release-1.1.1/package.json +48 -0
  15. package/.claude/worktrees/release-1.1.1/reason/AvoInspector.rei +11 -0
  16. package/.claude/worktrees/release-1.1.1/reason/NodeAvoInspector.re +27 -0
  17. package/.claude/worktrees/release-1.1.1/src/AvoDeduplicator.ts +146 -0
  18. package/.claude/worktrees/release-1.1.1/src/AvoEncryption.ts +90 -0
  19. package/.claude/worktrees/release-1.1.1/src/AvoGuid.ts +9 -0
  20. package/.claude/worktrees/release-1.1.1/src/AvoInspector.ts +383 -0
  21. package/.claude/worktrees/release-1.1.1/src/AvoInspectorEnv.ts +8 -0
  22. package/.claude/worktrees/release-1.1.1/src/AvoNetworkCallsHandler.ts +267 -0
  23. package/.claude/worktrees/release-1.1.1/src/AvoSchemaParser.ts +115 -0
  24. package/.claude/worktrees/release-1.1.1/src/AvoStreamId.ts +16 -0
  25. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEncryption.ts +515 -0
  26. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEventSpecCache.ts +242 -0
  27. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEventSpecFetcher.ts +241 -0
  28. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoInspector.ts +360 -0
  29. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoStreamId.ts +72 -0
  30. package/.claude/worktrees/release-1.1.1/src/__tests__/TestDeduplicator.ts +191 -0
  31. package/.claude/worktrees/release-1.1.1/src/__tests__/TestEventValidator.ts +231 -0
  32. package/.claude/worktrees/release-1.1.1/src/__tests__/TestNetworkCallsHandler.ts +179 -0
  33. package/.claude/worktrees/release-1.1.1/src/__tests__/TestNetworkCallsHandlerPublic.ts +208 -0
  34. package/.claude/worktrees/release-1.1.1/src/__tests__/TestParsing.ts +153 -0
  35. package/.claude/worktrees/release-1.1.1/src/__tests__/constants.ts +63 -0
  36. package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecCache.ts +109 -0
  37. package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecFetchTypes.ts +47 -0
  38. package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecFetcher.ts +106 -0
  39. package/.claude/worktrees/release-1.1.1/src/eventSpec/EventValidator.ts +74 -0
  40. package/.claude/worktrees/release-1.1.1/src/index.ts +2 -0
  41. package/.claude/worktrees/release-1.1.1/src/utils.ts +49 -0
  42. package/.claude/worktrees/release-1.1.1/tsconfig.json +13 -0
  43. package/.claude/worktrees/release-1.1.1/yarn.lock +2310 -0
  44. package/.github/workflows/claude.yml +55 -0
  45. package/dist/AvoDeduplicator.d.ts +4 -3
  46. package/dist/AvoDeduplicator.js +25 -24
  47. package/dist/AvoEncryption.d.ts +26 -0
  48. package/dist/AvoEncryption.js +77 -0
  49. package/dist/AvoInspector.d.ts +34 -3
  50. package/dist/AvoInspector.js +196 -40
  51. package/dist/AvoNetworkCallsHandler.d.ts +39 -13
  52. package/dist/AvoNetworkCallsHandler.js +127 -33
  53. package/dist/AvoStreamId.d.ts +5 -0
  54. package/dist/AvoStreamId.js +15 -0
  55. package/dist/__tests__/TestAvoEncryption.d.ts +1 -0
  56. package/dist/__tests__/TestAvoEncryption.js +288 -0
  57. package/dist/__tests__/TestAvoEventSpecCache.d.ts +1 -0
  58. package/dist/__tests__/TestAvoEventSpecCache.js +194 -0
  59. package/dist/__tests__/TestAvoEventSpecFetcher.d.ts +1 -0
  60. package/dist/__tests__/TestAvoEventSpecFetcher.js +219 -0
  61. package/dist/__tests__/TestAvoInspector.js +76 -2
  62. package/dist/__tests__/TestAvoStreamId.d.ts +1 -0
  63. package/dist/__tests__/TestAvoStreamId.js +55 -0
  64. package/dist/__tests__/TestDeduplicator.js +3 -0
  65. package/dist/__tests__/TestEventValidator.d.ts +1 -0
  66. package/dist/__tests__/TestEventValidator.js +196 -0
  67. package/dist/__tests__/TestNetworkCallsHandler.js +37 -10
  68. package/dist/__tests__/TestNetworkCallsHandlerPublic.js +93 -58
  69. package/dist/eventSpec/AvoEventSpecCache.d.ts +19 -0
  70. package/dist/eventSpec/AvoEventSpecCache.js +100 -0
  71. package/dist/eventSpec/AvoEventSpecFetchTypes.d.ts +41 -0
  72. package/dist/eventSpec/AvoEventSpecFetchTypes.js +5 -0
  73. package/dist/eventSpec/AvoEventSpecFetcher.d.ts +25 -0
  74. package/dist/eventSpec/AvoEventSpecFetcher.js +159 -0
  75. package/dist/eventSpec/EventValidator.d.ts +15 -0
  76. package/dist/eventSpec/EventValidator.js +53 -0
  77. package/package.json +4 -2
  78. package/src/AvoDeduplicator.ts +31 -27
  79. package/src/AvoEncryption.ts +90 -0
  80. package/src/AvoInspector.ts +278 -54
  81. package/src/AvoNetworkCallsHandler.ts +205 -45
  82. package/src/AvoStreamId.ts +16 -0
  83. package/src/__tests__/TestAvoEncryption.ts +515 -0
  84. package/src/__tests__/TestAvoEventSpecCache.ts +242 -0
  85. package/src/__tests__/TestAvoEventSpecFetcher.ts +279 -0
  86. package/src/__tests__/TestAvoInspector.ts +114 -2
  87. package/src/__tests__/TestAvoStreamId.ts +72 -0
  88. package/src/__tests__/TestDeduplicator.ts +4 -0
  89. package/src/__tests__/TestEventValidator.ts +231 -0
  90. package/src/__tests__/TestNetworkCallsHandler.ts +82 -15
  91. package/src/__tests__/TestNetworkCallsHandlerPublic.ts +134 -63
  92. package/src/eventSpec/AvoEventSpecCache.ts +119 -0
  93. package/src/eventSpec/AvoEventSpecFetchTypes.ts +47 -0
  94. package/src/eventSpec/AvoEventSpecFetcher.ts +186 -0
  95. package/src/eventSpec/EventValidator.ts +74 -0
@@ -0,0 +1,25 @@
1
+ import { EventSpecResponse } from "./AvoEventSpecFetchTypes";
2
+ type FetchCallback = (result: EventSpecResponse | null) => void;
3
+ export declare class AvoEventSpecFetcher {
4
+ private apiKey;
5
+ private inFlight;
6
+ private agent;
7
+ private static specEndpoint;
8
+ constructor(apiKey: string);
9
+ fetch(eventName: string, streamId: string, callback: FetchCallback): void;
10
+ private resolveCallbacks;
11
+ /**
12
+ * Parse the wire format from /trackingPlan/eventSpec into our internal types.
13
+ *
14
+ * Wire format:
15
+ * { events: [{ b, id, vids, p: { "PropName": { t: "string", r: true, v: {...}, rx: {...} } } }],
16
+ * metadata: { schemaId, branchId, latestActionId, sourceId } }
17
+ *
18
+ * Internal format:
19
+ * { eventSpec: { eventName, properties: [{ propertyName, propertyType, regex? }] } | null,
20
+ * metadata: { schemaId, branchId, latestActionId, sourceId } }
21
+ */
22
+ static parseWireResponse(wire: any, eventName: string): EventSpecResponse;
23
+ destroy(): void;
24
+ }
25
+ export {};
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AvoEventSpecFetcher = void 0;
4
+ const https_1 = require("https");
5
+ const AvoInspector_1 = require("../AvoInspector");
6
+ class AvoEventSpecFetcher {
7
+ constructor(apiKey) {
8
+ this.inFlight = new Map();
9
+ this.apiKey = apiKey;
10
+ this.agent = new https_1.Agent({ keepAlive: true });
11
+ }
12
+ fetch(eventName, streamId, callback) {
13
+ const dedupeKey = `${this.apiKey}:${streamId}:${eventName}`;
14
+ // In-flight dedup: if there's already a request in flight for this key,
15
+ // queue the callback
16
+ const existingCallbacks = this.inFlight.get(dedupeKey);
17
+ if (existingCallbacks) {
18
+ existingCallbacks.push(callback);
19
+ return;
20
+ }
21
+ // Register the callback and start the request
22
+ this.inFlight.set(dedupeKey, [callback]);
23
+ const queryParams = new URLSearchParams({
24
+ apiKey: this.apiKey,
25
+ eventName,
26
+ streamId,
27
+ });
28
+ const options = {
29
+ hostname: "api.avo.app",
30
+ port: 443,
31
+ path: `${AvoEventSpecFetcher.specEndpoint}?${queryParams.toString()}`,
32
+ method: "GET",
33
+ agent: this.agent,
34
+ headers: {
35
+ Accept: "application/json",
36
+ },
37
+ };
38
+ if (AvoInspector_1.AvoInspector.shouldLog) {
39
+ console.log("Avo Inspector: [network] GET https://" + options.hostname + AvoEventSpecFetcher.specEndpoint + "?eventName=" + encodeURIComponent(eventName));
40
+ }
41
+ const req = (0, https_1.request)(options, (res) => {
42
+ if (AvoInspector_1.AvoInspector.shouldLog) {
43
+ console.log("Avo Inspector: [network] Spec response status: " + res.statusCode + " " + res.statusMessage);
44
+ }
45
+ const chunks = [];
46
+ res.on("data", (data) => chunks.push(data));
47
+ res.on("end", () => {
48
+ let result = null;
49
+ if (res.statusCode !== 200) {
50
+ if (AvoInspector_1.AvoInspector.shouldLog) {
51
+ const body = Buffer.concat(chunks).toString();
52
+ console.warn("Avo Inspector: [network] Spec fetch failed with status " + res.statusCode + ": " + body);
53
+ }
54
+ this.resolveCallbacks(dedupeKey, null);
55
+ return;
56
+ }
57
+ try {
58
+ const body = Buffer.concat(chunks).toString();
59
+ if (AvoInspector_1.AvoInspector.shouldLog) {
60
+ console.log("Avo Inspector: [network] Spec response body: " + body);
61
+ }
62
+ const wire = JSON.parse(body);
63
+ result = AvoEventSpecFetcher.parseWireResponse(wire, eventName);
64
+ if (AvoInspector_1.AvoInspector.shouldLog) {
65
+ console.log("Avo Inspector: [network] Parsed spec: " + JSON.stringify(result));
66
+ }
67
+ }
68
+ catch (e) {
69
+ if (AvoInspector_1.AvoInspector.shouldLog) {
70
+ console.warn("Avo Inspector: [network] Failed to parse spec response: " + e);
71
+ }
72
+ }
73
+ this.resolveCallbacks(dedupeKey, result);
74
+ });
75
+ });
76
+ req.on("error", (err) => {
77
+ if (AvoInspector_1.AvoInspector.shouldLog) {
78
+ console.error("Avo Inspector: [network] Spec fetch error: " + err);
79
+ }
80
+ this.resolveCallbacks(dedupeKey, null);
81
+ });
82
+ req.setTimeout(10000);
83
+ req.on("timeout", () => {
84
+ if (AvoInspector_1.AvoInspector.shouldLog) {
85
+ console.error("Avo Inspector: [network] Spec fetch timed out after 10s");
86
+ }
87
+ req.destroy();
88
+ this.resolveCallbacks(dedupeKey, null);
89
+ });
90
+ req.end();
91
+ }
92
+ resolveCallbacks(key, result) {
93
+ const callbacks = this.inFlight.get(key);
94
+ this.inFlight.delete(key);
95
+ if (callbacks) {
96
+ for (const cb of callbacks) {
97
+ try {
98
+ cb(result);
99
+ }
100
+ catch (e) {
101
+ // Don't let one callback failure affect others
102
+ }
103
+ }
104
+ }
105
+ }
106
+ /**
107
+ * Parse the wire format from /trackingPlan/eventSpec into our internal types.
108
+ *
109
+ * Wire format:
110
+ * { events: [{ b, id, vids, p: { "PropName": { t: "string", r: true, v: {...}, rx: {...} } } }],
111
+ * metadata: { schemaId, branchId, latestActionId, sourceId } }
112
+ *
113
+ * Internal format:
114
+ * { eventSpec: { eventName, properties: [{ propertyName, propertyType, regex? }] } | null,
115
+ * metadata: { schemaId, branchId, latestActionId, sourceId } }
116
+ */
117
+ static parseWireResponse(wire, eventName) {
118
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
119
+ const metadata = {
120
+ schemaId: (_b = (_a = wire.metadata) === null || _a === void 0 ? void 0 : _a.schemaId) !== null && _b !== void 0 ? _b : "",
121
+ branchId: (_d = (_c = wire.metadata) === null || _c === void 0 ? void 0 : _c.branchId) !== null && _d !== void 0 ? _d : "",
122
+ latestActionId: (_f = (_e = wire.metadata) === null || _e === void 0 ? void 0 : _e.latestActionId) !== null && _f !== void 0 ? _f : "",
123
+ sourceId: (_h = (_g = wire.metadata) === null || _g === void 0 ? void 0 : _g.sourceId) !== null && _h !== void 0 ? _h : "",
124
+ };
125
+ if (!wire.events || !Array.isArray(wire.events) || wire.events.length === 0) {
126
+ return { eventSpec: null, metadata };
127
+ }
128
+ // Use the first event entry (the endpoint returns specs for the requested event)
129
+ const entry = wire.events[0];
130
+ const properties = [];
131
+ if (entry.p && typeof entry.p === "object") {
132
+ for (const propName of Object.keys(entry.p)) {
133
+ const constraint = entry.p[propName];
134
+ const prop = {
135
+ propertyName: propName,
136
+ propertyType: (_j = constraint === null || constraint === void 0 ? void 0 : constraint.t) !== null && _j !== void 0 ? _j : "unknown",
137
+ };
138
+ // Extract regex pattern if present (rx field maps regex patterns to event IDs)
139
+ if ((constraint === null || constraint === void 0 ? void 0 : constraint.rx) && typeof constraint.rx === "object") {
140
+ const patterns = Object.keys(constraint.rx);
141
+ if (patterns.length > 0) {
142
+ prop.regex = patterns[0];
143
+ }
144
+ }
145
+ properties.push(prop);
146
+ }
147
+ }
148
+ const eventSpec = {
149
+ eventName,
150
+ properties,
151
+ };
152
+ return { eventSpec, metadata };
153
+ }
154
+ destroy() {
155
+ this.agent.destroy();
156
+ }
157
+ }
158
+ exports.AvoEventSpecFetcher = AvoEventSpecFetcher;
159
+ AvoEventSpecFetcher.specEndpoint = "/trackingPlan/eventSpec";
@@ -0,0 +1,15 @@
1
+ import { EventSpec, PropertyValidationResult } from "./AvoEventSpecFetchTypes";
2
+ interface EventProperty {
3
+ propertyName: string;
4
+ propertyType: string;
5
+ propertyValue?: string;
6
+ }
7
+ export declare class EventValidator {
8
+ /**
9
+ * Validates event properties against the spec.
10
+ * Returns per-property validation results with bandwidth optimization:
11
+ * passedEventIds is returned only when strictly smaller than failedEventIds.
12
+ */
13
+ validate(spec: EventSpec, eventProperties: EventProperty[], eventId: string): PropertyValidationResult[];
14
+ }
15
+ export {};
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventValidator = void 0;
4
+ const safe_regex2_1 = require("safe-regex2");
5
+ class EventValidator {
6
+ /**
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.
10
+ */
11
+ validate(spec, eventProperties, eventId) {
12
+ 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
+ }
38
+ }
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 = [];
47
+ }
48
+ results.push(result);
49
+ }
50
+ return results;
51
+ }
52
+ }
53
+ exports.EventValidator = EventValidator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-avo-inspector",
3
- "version": "1.1.0",
3
+ "version": "1.2.0-alpha.1",
4
4
  "description": "Avo Inspector for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "repository": "https://github.com/avohq/node-avo-inspector",
@@ -42,5 +42,7 @@
42
42
  "ts-jest": "^29.2.4",
43
43
  "typescript": "*"
44
44
  },
45
- "dependencies": {}
45
+ "dependencies": {
46
+ "safe-regex2": "^5.0.0"
47
+ }
46
48
  }
@@ -5,33 +5,41 @@ export class AvoDeduplicator {
5
5
  manualEvents: { [time: number]: string } = {};
6
6
  private msToConsiderOld = 500;
7
7
 
8
+ // Keyed by streamId\0eventName to prevent cross-stream suppression on server
8
9
  avoFunctionsEventsParams: {
9
- [eventName: string]: { [propName: string]: any };
10
+ [key: string]: { [propName: string]: any };
10
11
  } = {};
11
- manualEventsParams: { [eventName: string]: { [propName: string]: any } } = {};
12
+ manualEventsParams: { [key: string]: { [propName: string]: any } } = {};
13
+
14
+ private static dedupKey(eventName: string, streamId: string): string {
15
+ return streamId + "\0" + eventName;
16
+ }
12
17
 
13
18
  shouldRegisterEvent(
14
19
  eventName: string,
15
20
  params: { [propName: string]: any },
16
- fromAvoFunction: boolean
21
+ fromAvoFunction: boolean,
22
+ streamId: string = ""
17
23
  ): boolean {
18
24
  this.clearOldEvents();
19
25
 
26
+ const key = AvoDeduplicator.dedupKey(eventName, streamId);
27
+
20
28
  if (fromAvoFunction) {
21
- this.avoFunctionsEvents[Date.now()] = eventName;
22
- this.avoFunctionsEventsParams[eventName] = params;
29
+ this.avoFunctionsEvents[Date.now()] = key;
30
+ this.avoFunctionsEventsParams[key] = params;
23
31
  } else {
24
- this.manualEvents[Date.now()] = eventName;
25
- this.manualEventsParams[eventName] = params;
32
+ this.manualEvents[Date.now()] = key;
33
+ this.manualEventsParams[key] = params;
26
34
  }
27
35
 
28
36
  let checkInAvoFunctions = !fromAvoFunction;
29
37
 
30
- return !this.hasSameEventAs(eventName, params, checkInAvoFunctions);
38
+ return !this.hasSameEventAs(key, params, checkInAvoFunctions);
31
39
  }
32
40
 
33
41
  private hasSameEventAs(
34
- eventName: string,
42
+ key: string,
35
43
  params: { [propName: string]: any },
36
44
  checkInAvoFunctions: boolean
37
45
  ): boolean {
@@ -39,37 +47,33 @@ export class AvoDeduplicator {
39
47
 
40
48
  if (checkInAvoFunctions) {
41
49
  if (
42
- this.lookForEventIn(eventName, params, this.avoFunctionsEventsParams)
50
+ this.lookForEventIn(key, params, this.avoFunctionsEventsParams)
43
51
  ) {
44
52
  result = true;
45
53
  }
46
54
  } else {
47
- if (this.lookForEventIn(eventName, params, this.manualEventsParams)) {
55
+ if (this.lookForEventIn(key, params, this.manualEventsParams)) {
48
56
  result = true;
49
57
  }
50
58
  }
51
59
 
52
60
  if (result) {
53
- delete this.avoFunctionsEventsParams[eventName];
54
- delete this.manualEventsParams[eventName];
61
+ delete this.avoFunctionsEventsParams[key];
62
+ delete this.manualEventsParams[key];
55
63
  }
56
64
 
57
65
  return result;
58
66
  }
59
67
 
60
68
  private lookForEventIn(
61
- eventName: string,
69
+ key: string,
62
70
  params: { [propName: string]: any },
63
- eventsStorage: { [eventName: string]: { [propName: string]: any } }
71
+ eventsStorage: { [key: string]: { [propName: string]: any } }
64
72
  ): boolean {
65
- for (const otherEventName in eventsStorage) {
66
- if (eventsStorage.hasOwnProperty(otherEventName)) {
67
- if (otherEventName === eventName) {
68
- const otherParams = eventsStorage[eventName];
69
- if (otherParams && deepEquals(params, otherParams)) {
70
- return true;
71
- }
72
- }
73
+ if (eventsStorage.hasOwnProperty(key)) {
74
+ const otherParams = eventsStorage[key];
75
+ if (otherParams && deepEquals(params, otherParams)) {
76
+ return true;
73
77
  }
74
78
  }
75
79
  return false;
@@ -116,9 +120,9 @@ export class AvoDeduplicator {
116
120
  if (this.avoFunctionsEvents.hasOwnProperty(time)) {
117
121
  const timestamp = Number(time) || 0;
118
122
  if (now - timestamp > this.msToConsiderOld) {
119
- const eventName = this.avoFunctionsEvents[time];
123
+ const key = this.avoFunctionsEvents[time];
120
124
  delete this.avoFunctionsEvents[time];
121
- delete this.avoFunctionsEventsParams[eventName];
125
+ delete this.avoFunctionsEventsParams[key];
122
126
  }
123
127
  }
124
128
  }
@@ -127,9 +131,9 @@ export class AvoDeduplicator {
127
131
  if (this.manualEvents.hasOwnProperty(time)) {
128
132
  const timestamp = Number(time) || 0;
129
133
  if (now - timestamp > this.msToConsiderOld) {
130
- const eventName = this.manualEvents[time];
134
+ const key = this.manualEvents[time];
131
135
  delete this.manualEvents[time];
132
- delete this.manualEventsParams[eventName];
136
+ delete this.manualEventsParams[key];
133
137
  }
134
138
  }
135
139
  }
@@ -0,0 +1,90 @@
1
+ import * as crypto from "crypto";
2
+
3
+ export class AvoEncryption {
4
+ /**
5
+ * Determines whether encryption should be applied.
6
+ *
7
+ * Truth table:
8
+ * dev + key => true
9
+ * staging + key => true
10
+ * prod + key => false
11
+ * dev + null => false
12
+ * dev + "" => false
13
+ */
14
+ static shouldEncrypt(
15
+ env: string,
16
+ publicEncryptionKey: string | undefined | null
17
+ ): boolean {
18
+ if (!publicEncryptionKey || publicEncryptionKey.length === 0) {
19
+ return false;
20
+ }
21
+ if (env === "prod") {
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+
27
+ /**
28
+ * Returns true if the propertyType represents a list type (e.g. "list(string)").
29
+ */
30
+ static isListType(propertyType: string): boolean {
31
+ return propertyType.startsWith("list(");
32
+ }
33
+
34
+ /**
35
+ * Encrypts a string value using ECIES with prime256v1.
36
+ *
37
+ * Wire format: [0x00][65-byte ephemeral pubkey][16-byte IV][16-byte auth tag][ciphertext]
38
+ * Output is base64-encoded.
39
+ *
40
+ * Returns null on failure, logging a warning.
41
+ */
42
+ static encryptValue(
43
+ value: string,
44
+ recipientPubKeyHex: string
45
+ ): string | null {
46
+ try {
47
+ const recipientPubKey = Buffer.from(recipientPubKeyHex, "hex");
48
+
49
+ // Generate ephemeral key pair
50
+ const ecdh = crypto.createECDH("prime256v1");
51
+ ecdh.generateKeys();
52
+ const ephemeralPubKey = ecdh.getPublicKey(); // 65 bytes, uncompressed
53
+
54
+ // Compute shared secret (raw 32-byte X-coordinate)
55
+ const sharedSecret = ecdh.computeSecret(recipientPubKey);
56
+
57
+ // Derive AES key via SHA-256
58
+ const aesKey = crypto.createHash("sha256").update(sharedSecret).digest();
59
+
60
+ // Generate random 16-byte IV
61
+ const iv = crypto.randomBytes(16);
62
+
63
+ // Encrypt with AES-256-GCM
64
+ const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv, {
65
+ authTagLength: 16,
66
+ });
67
+ const encrypted = Buffer.concat([
68
+ cipher.update(value, "utf8"),
69
+ cipher.final(),
70
+ ]);
71
+ const authTag = cipher.getAuthTag(); // 16 bytes
72
+
73
+ // Assemble wire format: [version][ephemeralPubKey][IV][authTag][ciphertext]
74
+ const wire = Buffer.concat([
75
+ Buffer.from([0x00]), // version byte
76
+ ephemeralPubKey, // 65 bytes
77
+ iv, // 16 bytes
78
+ authTag, // 16 bytes
79
+ encrypted, // variable length
80
+ ]);
81
+
82
+ return wire.toString("base64");
83
+ } catch (e) {
84
+ console.warn(
85
+ `[Avo Inspector] Warning: encryption failed for value, omitting property. ${e}`
86
+ );
87
+ return null;
88
+ }
89
+ }
90
+ }