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
@@ -1,3 +1,4 @@
1
+ import { EventSpecMetadata, PropertyValidationResult } from "./eventSpec/AvoEventSpecFetchTypes";
1
2
  export interface BaseBody {
2
3
  apiKey: string;
3
4
  appName: string;
@@ -7,25 +8,39 @@ export interface BaseBody {
7
8
  libPlatform: "node";
8
9
  messageId: string;
9
10
  trackingId: string;
10
- createdAt: string;
11
11
  sessionId: string;
12
+ anonymousId: string;
13
+ createdAt: string;
12
14
  samplingRate: number;
15
+ publicEncryptionKey?: string;
16
+ }
17
+ export interface EventPropertyEncrypted {
18
+ propertyName: string;
19
+ propertyType: string;
20
+ encryptedPropertyValue: string;
21
+ children?: any;
13
22
  }
14
- export interface SessionStartedBody extends BaseBody {
15
- type: "sessionStarted";
23
+ export interface EventPropertyPlain {
24
+ propertyName: string;
25
+ propertyType: string;
26
+ children?: any;
16
27
  }
28
+ export interface EventPropertyValidation {
29
+ failedEventIds?: string[];
30
+ passedEventIds?: string[];
31
+ }
32
+ export type EventProperty = (EventPropertyEncrypted | EventPropertyPlain) & EventPropertyValidation;
17
33
  export interface EventSchemaBody extends BaseBody {
18
34
  type: "event";
19
35
  eventName: string;
20
- eventProperties: Array<{
21
- propertyName: string;
22
- propertyType: string;
23
- children?: any;
24
- }>;
36
+ eventProperties: Array<EventProperty>;
25
37
  avoFunction: boolean;
26
38
  eventId: string | null;
27
39
  eventHash: string | null;
40
+ streamId?: string;
41
+ eventSpecMetadata?: EventSpecMetadata;
28
42
  }
43
+ export type InspectorBody = EventSchemaBody;
29
44
  export declare class AvoNetworkCallsHandler {
30
45
  private apiKey;
31
46
  private envName;
@@ -33,14 +48,25 @@ export declare class AvoNetworkCallsHandler {
33
48
  private appVersion;
34
49
  private libVersion;
35
50
  private samplingRate;
51
+ private publicEncryptionKey?;
36
52
  private static trackingEndpoint;
37
- constructor(apiKey: string, envName: string, appName: string, appVersion: string, libVersion: string);
38
- callInspectorWithBatchBody(inEvents: Array<SessionStartedBody | EventSchemaBody>): Promise<void>;
39
- bodyForSessionStartedCall(sessionId: string): SessionStartedBody;
40
- bodyForEventSchemaCall(sessionId: string, eventName: string, eventProperties: Array<{
53
+ constructor(apiKey: string, envName: string, appName: string, appVersion: string, libVersion: string, publicEncryptionKey?: string);
54
+ callInspectorWithBatchBody(inEvents: Array<InspectorBody>): Promise<void>;
55
+ bodyForEventSchemaCall(anonymousId: string, eventName: string, eventProperties: Array<{
56
+ propertyName: string;
57
+ propertyType: string;
58
+ children?: any;
59
+ }>, eventId: string | null, eventHash: string | null, rawEventProperties?: {
60
+ [propName: string]: any;
61
+ }): EventSchemaBody;
62
+ buildEventProperties(eventProperties: Array<{
41
63
  propertyName: string;
42
64
  propertyType: string;
43
65
  children?: any;
44
- }>, eventId: string | null, eventHash: string | null): EventSchemaBody;
66
+ }>, rawEventProperties?: {
67
+ [propName: string]: any;
68
+ }): Array<EventProperty>;
69
+ bodyForValidatedEventSchemaCall(anonymousId: string, eventName: string, eventProperties: Array<EventProperty>, eventId: string | null, eventHash: string | null, eventSpecMetadata: EventSpecMetadata, propertyResults: PropertyValidationResult[]): EventSchemaBody;
70
+ private encryptProperties;
45
71
  private createBaseCallBody;
46
72
  }
@@ -3,15 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AvoNetworkCallsHandler = void 0;
4
4
  const AvoGuid_1 = require("./AvoGuid");
5
5
  const AvoInspector_1 = require("./AvoInspector");
6
+ const AvoEncryption_1 = require("./AvoEncryption");
6
7
  const https_1 = require("https");
7
8
  class AvoNetworkCallsHandler {
8
- constructor(apiKey, envName, appName, appVersion, libVersion) {
9
+ constructor(apiKey, envName, appName, appVersion, libVersion, publicEncryptionKey) {
9
10
  this.samplingRate = 1.0;
10
11
  this.apiKey = apiKey;
11
12
  this.envName = envName;
12
13
  this.appName = appName;
13
14
  this.appVersion = appVersion;
14
15
  this.libVersion = libVersion;
16
+ this.publicEncryptionKey = publicEncryptionKey;
15
17
  }
16
18
  callInspectorWithBatchBody(inEvents) {
17
19
  const events = inEvents.filter((x) => x != null);
@@ -26,64 +28,87 @@ class AvoNetworkCallsHandler {
26
28
  }
27
29
  if (AvoInspector_1.AvoInspector.shouldLog) {
28
30
  events.forEach(function (event) {
29
- if (event.type === "sessionStarted") {
30
- console.log("Avo Inspector: sending session started event.");
31
- }
32
- else if (event.type === "event") {
33
- let schemaEvent = event;
34
- console.log("Avo Inspector: sending event " +
35
- schemaEvent.eventName +
36
- " with schema " +
37
- JSON.stringify(schemaEvent.eventProperties));
38
- }
31
+ const eventProps = event.eventProperties
32
+ .map(p => '\t"' + p.propertyName + '": "' + p.propertyType + '"')
33
+ .join(";\n");
34
+ const validated = event.eventSpecMetadata ? " (validated)" : "";
35
+ console.log("Avo Inspector: Sending event " +
36
+ event.eventName + validated +
37
+ " with schema {\n" + eventProps + "\n}");
39
38
  });
40
39
  }
41
- return new Promise(function (resolve, reject) {
40
+ return new Promise((resolve, reject) => {
42
41
  const data = JSON.stringify(events);
43
- var options = {
42
+ const options = {
44
43
  hostname: "api.avo.app",
45
44
  port: 443,
46
45
  path: AvoNetworkCallsHandler.trackingEndpoint,
47
46
  method: "POST",
48
47
  headers: {
49
- "Content-Type": "text/plain",
48
+ "Accept": "application/json",
49
+ "Content-Type": "application/json",
50
50
  "Content-Length": Buffer.byteLength(data),
51
51
  },
52
52
  };
53
- var req = (0, https_1.request)(options, (res) => {
53
+ if (AvoInspector_1.AvoInspector.shouldLog) {
54
+ console.log("Avo Inspector: [network] POST https://" + options.hostname + options.path);
55
+ console.log("Avo Inspector: [network] Request body (" + Buffer.byteLength(data) + " bytes)");
56
+ }
57
+ const req = (0, https_1.request)(options, (res) => {
58
+ if (AvoInspector_1.AvoInspector.shouldLog) {
59
+ console.log("Avo Inspector: [network] Response status: " + res.statusCode + " " + res.statusMessage);
60
+ }
54
61
  const chunks = [];
55
62
  res.on("data", (data) => chunks.push(data));
56
63
  res.on("end", () => {
57
- try {
58
- // @ts-ignore
59
- const data = JSON.parse(Buffer.concat(chunks).toString());
60
- // @ts-ignore
61
- this.samplingRate = data.samplingRate;
64
+ if (res.statusCode === 200) {
65
+ try {
66
+ const responseBody = Buffer.concat(chunks).toString();
67
+ const data = JSON.parse(responseBody);
68
+ if (typeof data.samplingRate === "number" && data.samplingRate >= 0 && data.samplingRate <= 1) {
69
+ this.samplingRate = data.samplingRate;
70
+ }
71
+ }
72
+ catch (e) {
73
+ if (AvoInspector_1.AvoInspector.shouldLog) {
74
+ console.warn("Avo Inspector: [network] Failed to parse response JSON: " + e);
75
+ }
76
+ }
77
+ }
78
+ else if (AvoInspector_1.AvoInspector.shouldLog) {
79
+ console.warn("Avo Inspector: [network] Non-200 response: " + res.statusCode);
62
80
  }
63
- catch (e) { }
64
81
  resolve();
65
82
  });
66
83
  });
67
84
  req.write(data);
68
- req.on("error", () => {
85
+ req.setTimeout(10000);
86
+ req.on("error", (err) => {
87
+ if (AvoInspector_1.AvoInspector.shouldLog) {
88
+ console.error("Avo Inspector: [network] Request error: " + err);
89
+ }
69
90
  reject("Request failed");
70
91
  });
71
92
  req.on("timeout", () => {
93
+ if (AvoInspector_1.AvoInspector.shouldLog) {
94
+ console.error("Avo Inspector: [network] Request timed out after 10s");
95
+ }
96
+ req.destroy();
72
97
  reject("Request timed out");
73
98
  });
74
99
  req.end();
75
100
  });
76
101
  }
77
- bodyForSessionStartedCall(sessionId) {
78
- let sessionBody = this.createBaseCallBody(sessionId);
79
- sessionBody.type = "sessionStarted";
80
- return sessionBody;
81
- }
82
- bodyForEventSchemaCall(sessionId, eventName, eventProperties, eventId, eventHash) {
83
- let eventSchemaBody = this.createBaseCallBody(sessionId);
102
+ bodyForEventSchemaCall(anonymousId, eventName, eventProperties, eventId, eventHash, rawEventProperties) {
103
+ let eventSchemaBody = this.createBaseCallBody(anonymousId);
84
104
  eventSchemaBody.type = "event";
85
105
  eventSchemaBody.eventName = eventName;
86
- eventSchemaBody.eventProperties = eventProperties;
106
+ if (AvoEncryption_1.AvoEncryption.shouldEncrypt(this.envName, this.publicEncryptionKey) && rawEventProperties) {
107
+ eventSchemaBody.eventProperties = this.encryptProperties(eventProperties, rawEventProperties);
108
+ }
109
+ else {
110
+ eventSchemaBody.eventProperties = eventProperties;
111
+ }
87
112
  if (eventId != null) {
88
113
  eventSchemaBody.avoFunction = true;
89
114
  eventSchemaBody.eventId = eventId;
@@ -96,8 +121,72 @@ class AvoNetworkCallsHandler {
96
121
  }
97
122
  return eventSchemaBody;
98
123
  }
99
- createBaseCallBody(sessionId) {
100
- return {
124
+ buildEventProperties(eventProperties, rawEventProperties) {
125
+ if (AvoEncryption_1.AvoEncryption.shouldEncrypt(this.envName, this.publicEncryptionKey) && rawEventProperties) {
126
+ return this.encryptProperties(eventProperties, rawEventProperties);
127
+ }
128
+ return eventProperties;
129
+ }
130
+ bodyForValidatedEventSchemaCall(anonymousId, eventName, eventProperties, eventId, eventHash, eventSpecMetadata, propertyResults) {
131
+ // Build a map of validation results by property name
132
+ const validationMap = new Map();
133
+ for (const result of propertyResults) {
134
+ validationMap.set(result.propertyName, result);
135
+ }
136
+ // Merge validation results into eventProperties (matching Android)
137
+ const mergedProperties = eventProperties.map((prop) => {
138
+ const validation = validationMap.get(prop.propertyName);
139
+ if (validation) {
140
+ const merged = Object.assign({}, prop);
141
+ if (validation.failedEventIds.length > 0) {
142
+ merged.failedEventIds = validation.failedEventIds;
143
+ }
144
+ if (validation.passedEventIds.length > 0) {
145
+ merged.passedEventIds = validation.passedEventIds;
146
+ }
147
+ return merged;
148
+ }
149
+ return prop;
150
+ });
151
+ let body = this.createBaseCallBody(anonymousId);
152
+ body.type = "event";
153
+ body.eventName = eventName;
154
+ body.eventProperties = mergedProperties;
155
+ body.streamId = anonymousId;
156
+ body.eventSpecMetadata = eventSpecMetadata;
157
+ if (eventId != null) {
158
+ body.avoFunction = true;
159
+ body.eventId = eventId;
160
+ body.eventHash = eventHash;
161
+ }
162
+ else {
163
+ body.avoFunction = false;
164
+ body.eventId = null;
165
+ body.eventHash = null;
166
+ }
167
+ return body;
168
+ }
169
+ encryptProperties(properties, rawEventProperties) {
170
+ var _a;
171
+ const result = [];
172
+ for (const prop of properties) {
173
+ // List-type properties: omit entirely
174
+ if (AvoEncryption_1.AvoEncryption.isListType(prop.propertyType)) {
175
+ continue;
176
+ }
177
+ const rawValue = rawEventProperties[prop.propertyName];
178
+ const jsonValue = (_a = JSON.stringify(rawValue)) !== null && _a !== void 0 ? _a : "null";
179
+ const encrypted = AvoEncryption_1.AvoEncryption.encryptValue(jsonValue, this.publicEncryptionKey);
180
+ if (encrypted === null) {
181
+ // Encryption failure: omit the property (warning already logged by encryptValue)
182
+ continue;
183
+ }
184
+ result.push(Object.assign({ propertyName: prop.propertyName, propertyType: prop.propertyType, encryptedPropertyValue: encrypted }, (prop.children !== undefined ? { children: prop.children } : {})));
185
+ }
186
+ return result;
187
+ }
188
+ createBaseCallBody(anonymousId) {
189
+ const body = {
101
190
  apiKey: this.apiKey,
102
191
  appName: this.appName,
103
192
  appVersion: this.appVersion,
@@ -106,10 +195,15 @@ class AvoNetworkCallsHandler {
106
195
  libPlatform: "node",
107
196
  messageId: AvoGuid_1.AvoGuid.newGuid(),
108
197
  trackingId: "",
198
+ sessionId: "",
199
+ anonymousId: anonymousId,
109
200
  createdAt: new Date().toISOString(),
110
- sessionId: sessionId,
111
201
  samplingRate: this.samplingRate,
112
202
  };
203
+ if (this.publicEncryptionKey && this.publicEncryptionKey.length > 0) {
204
+ body.publicEncryptionKey = this.publicEncryptionKey;
205
+ }
206
+ return body;
113
207
  }
114
208
  }
115
209
  exports.AvoNetworkCallsHandler = AvoNetworkCallsHandler;
@@ -0,0 +1,5 @@
1
+ export declare class AvoStreamId {
2
+ private _streamId;
3
+ constructor(streamId?: string);
4
+ get streamId(): string;
5
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AvoStreamId = void 0;
4
+ class AvoStreamId {
5
+ constructor(streamId) {
6
+ this._streamId = streamId !== null && streamId !== void 0 ? streamId : "";
7
+ if (this._streamId.includes(":")) {
8
+ console.warn("[Avo Inspector] Warning: streamId contains ':' which is not supported");
9
+ }
10
+ }
11
+ get streamId() {
12
+ return this._streamId;
13
+ }
14
+ }
15
+ exports.AvoStreamId = AvoStreamId;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,288 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const crypto = require("crypto");
4
+ const AvoEncryption_1 = require("../AvoEncryption");
5
+ const AvoNetworkCallsHandler_1 = require("../AvoNetworkCallsHandler");
6
+ const AvoInspectorEnv_1 = require("../AvoInspectorEnv");
7
+ const AvoGuid_1 = require("../AvoGuid");
8
+ const constants_1 = require("./constants");
9
+ // Generate a test key pair for encryption tests
10
+ function generateTestKeyPair() {
11
+ const ecdh = crypto.createECDH("prime256v1");
12
+ ecdh.generateKeys();
13
+ return {
14
+ publicKey: ecdh.getPublicKey("hex"),
15
+ privateKey: ecdh.getPrivateKey(),
16
+ ecdh,
17
+ };
18
+ }
19
+ function parseWireFormat(encryptedBase64) {
20
+ const wire = Buffer.from(encryptedBase64, "base64");
21
+ return {
22
+ version: wire[0],
23
+ ephemeralPubKey: wire.subarray(1, 66),
24
+ iv: wire.subarray(66, 82),
25
+ authTag: wire.subarray(82, 98),
26
+ ciphertext: wire.subarray(98),
27
+ wire,
28
+ };
29
+ }
30
+ describe("AvoEncryption", () => {
31
+ describe("shouldEncrypt", () => {
32
+ const dummyHexKey = "deadbeef";
33
+ test("dev + key => true", () => {
34
+ expect(AvoEncryption_1.AvoEncryption.shouldEncrypt(AvoInspectorEnv_1.AvoInspectorEnv.Dev, dummyHexKey)).toBe(true);
35
+ });
36
+ test("staging + key => true", () => {
37
+ expect(AvoEncryption_1.AvoEncryption.shouldEncrypt(AvoInspectorEnv_1.AvoInspectorEnv.Staging, dummyHexKey)).toBe(true);
38
+ });
39
+ test("prod + key => false", () => {
40
+ expect(AvoEncryption_1.AvoEncryption.shouldEncrypt(AvoInspectorEnv_1.AvoInspectorEnv.Prod, dummyHexKey)).toBe(false);
41
+ });
42
+ test("dev + null => false", () => {
43
+ expect(AvoEncryption_1.AvoEncryption.shouldEncrypt(AvoInspectorEnv_1.AvoInspectorEnv.Dev, undefined)).toBe(false);
44
+ });
45
+ test("dev + empty string => false", () => {
46
+ expect(AvoEncryption_1.AvoEncryption.shouldEncrypt(AvoInspectorEnv_1.AvoInspectorEnv.Dev, "")).toBe(false);
47
+ });
48
+ });
49
+ describe("wire format", () => {
50
+ const testKeyPair = generateTestKeyPair();
51
+ test("base64Decode(output).length >= 99", () => {
52
+ const encrypted = AvoEncryption_1.AvoEncryption.encryptValue("hello", testKeyPair.publicKey);
53
+ expect(encrypted).not.toBeNull();
54
+ const decoded = Buffer.from(encrypted, "base64");
55
+ // 1 (version) + 65 (ephemeral pubkey) + 16 (IV) + 16 (auth tag) + ciphertext >= 1
56
+ expect(decoded.length).toBeGreaterThanOrEqual(99);
57
+ });
58
+ test("output[0] == 0x00 (version byte)", () => {
59
+ const encrypted = AvoEncryption_1.AvoEncryption.encryptValue("hello", testKeyPair.publicKey);
60
+ const decoded = Buffer.from(encrypted, "base64");
61
+ expect(decoded[0]).toBe(0x00);
62
+ });
63
+ test("output[1] == 0x04 (uncompressed pubkey marker)", () => {
64
+ const encrypted = AvoEncryption_1.AvoEncryption.encryptValue("hello", testKeyPair.publicKey);
65
+ const decoded = Buffer.from(encrypted, "base64");
66
+ expect(decoded[1]).toBe(0x04);
67
+ });
68
+ test("ephemeral public key is 65 bytes (uncompressed)", () => {
69
+ const encrypted = AvoEncryption_1.AvoEncryption.encryptValue("test data", testKeyPair.publicKey);
70
+ const decoded = Buffer.from(encrypted, "base64");
71
+ // bytes 1..65 are the ephemeral public key
72
+ const ephemeralPubKey = decoded.subarray(1, 66);
73
+ expect(ephemeralPubKey.length).toBe(65);
74
+ expect(ephemeralPubKey[0]).toBe(0x04); // uncompressed marker
75
+ });
76
+ });
77
+ describe("round-trip encrypt/decrypt", () => {
78
+ test("can decrypt back to original plaintext", () => {
79
+ const ecdh = crypto.createECDH("prime256v1");
80
+ ecdh.generateKeys();
81
+ const recipientPubKeyHex = ecdh.getPublicKey("hex");
82
+ const plaintext = "Hello, encryption!";
83
+ const encrypted = AvoEncryption_1.AvoEncryption.encryptValue(plaintext, recipientPubKeyHex);
84
+ expect(encrypted).not.toBeNull();
85
+ // Decrypt: parse wire format
86
+ const { version, ephemeralPubKey, iv, authTag, ciphertext } = parseWireFormat(encrypted);
87
+ expect(version).toBe(0x00);
88
+ // Compute shared secret the same way
89
+ const sharedSecret = ecdh.computeSecret(ephemeralPubKey);
90
+ const aesKey = crypto.createHash("sha256").update(sharedSecret).digest();
91
+ const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv, {
92
+ authTagLength: 16,
93
+ });
94
+ decipher.setAuthTag(authTag);
95
+ let decrypted = decipher.update(ciphertext);
96
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
97
+ expect(decrypted.toString("utf8")).toBe(plaintext);
98
+ });
99
+ test("different encryptions of same value produce different ciphertext", () => {
100
+ const ecdh = crypto.createECDH("prime256v1");
101
+ ecdh.generateKeys();
102
+ const pubKey = ecdh.getPublicKey("hex");
103
+ const enc1 = AvoEncryption_1.AvoEncryption.encryptValue("same", pubKey);
104
+ const enc2 = AvoEncryption_1.AvoEncryption.encryptValue("same", pubKey);
105
+ // Ephemeral keys differ each time, so output differs
106
+ expect(enc1).not.toBe(enc2);
107
+ });
108
+ });
109
+ describe("encryption failure handling", () => {
110
+ test("returns null and warns on invalid key", () => {
111
+ const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
112
+ const result = AvoEncryption_1.AvoEncryption.encryptValue("hello", "not-a-valid-hex-key");
113
+ expect(result).toBeNull();
114
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("[Avo Inspector] Warning:"));
115
+ warnSpy.mockRestore();
116
+ });
117
+ });
118
+ describe("isListType", () => {
119
+ test("detects list types", () => {
120
+ expect(AvoEncryption_1.AvoEncryption.isListType("list(string)")).toBe(true);
121
+ expect(AvoEncryption_1.AvoEncryption.isListType("list(int)")).toBe(true);
122
+ expect(AvoEncryption_1.AvoEncryption.isListType("list(object)")).toBe(true);
123
+ });
124
+ test("non-list types return false", () => {
125
+ expect(AvoEncryption_1.AvoEncryption.isListType("string")).toBe(false);
126
+ expect(AvoEncryption_1.AvoEncryption.isListType("int")).toBe(false);
127
+ expect(AvoEncryption_1.AvoEncryption.isListType("object")).toBe(false);
128
+ });
129
+ });
130
+ });
131
+ describe("AvoNetworkCallsHandler encryption integration", () => {
132
+ const inspectorVersion = process.env.npm_package_version || "";
133
+ beforeEach(() => {
134
+ const now = new Date();
135
+ // @ts-ignore
136
+ jest.spyOn(global, "Date").mockImplementation(() => now);
137
+ jest
138
+ .spyOn(AvoGuid_1.AvoGuid, "newGuid")
139
+ .mockImplementation(() => constants_1.mockedReturns.GUID);
140
+ });
141
+ afterEach(() => {
142
+ jest.restoreAllMocks();
143
+ });
144
+ test("publicEncryptionKey included in base body when non-null and non-empty", () => {
145
+ const pubKey = "deadbeef";
146
+ const handler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler("api-key", AvoInspectorEnv_1.AvoInspectorEnv.Dev, "app", "1.0", inspectorVersion, pubKey);
147
+ const body = handler.bodyForEventSchemaCall("anon-id", "test-event", [{ propertyName: "prop1", propertyType: "string" }], null, null);
148
+ expect(body.publicEncryptionKey).toBe(pubKey);
149
+ });
150
+ test("publicEncryptionKey NOT included in base body when null", () => {
151
+ const handler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler("api-key", AvoInspectorEnv_1.AvoInspectorEnv.Dev, "app", "1.0", inspectorVersion, undefined);
152
+ const body = handler.bodyForEventSchemaCall("anon-id", "test-event", [{ propertyName: "prop1", propertyType: "string" }], null, null);
153
+ expect(body.publicEncryptionKey).toBeUndefined();
154
+ });
155
+ test("publicEncryptionKey NOT included in base body when empty string", () => {
156
+ const handler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler("api-key", AvoInspectorEnv_1.AvoInspectorEnv.Dev, "app", "1.0", inspectorVersion, "");
157
+ const body = handler.bodyForEventSchemaCall("anon-id", "test-event", [{ propertyName: "prop1", propertyType: "string" }], null, null);
158
+ expect(body.publicEncryptionKey).toBeUndefined();
159
+ });
160
+ test("dev env with key: properties are encrypted with encryptedPropertyValue", () => {
161
+ const ecdh = crypto.createECDH("prime256v1");
162
+ ecdh.generateKeys();
163
+ const pubKey = ecdh.getPublicKey("hex");
164
+ const handler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler("api-key", AvoInspectorEnv_1.AvoInspectorEnv.Dev, "app", "1.0", inspectorVersion, pubKey);
165
+ const body = handler.bodyForEventSchemaCall("anon-id", "test-event", [{ propertyName: "username", propertyType: "string" }], null, null, { username: "alice" });
166
+ // Should have encryptedPropertyValue alongside propertyType
167
+ expect(body.eventProperties.length).toBe(1);
168
+ expect(body.eventProperties[0].propertyName).toBe("username");
169
+ expect(body.eventProperties[0].encryptedPropertyValue).toBeDefined();
170
+ expect(body.eventProperties[0].propertyType).toBe("string");
171
+ });
172
+ test("prod env: no encryptedPropertyValue, propertyType kept as-is", () => {
173
+ const ecdh = crypto.createECDH("prime256v1");
174
+ ecdh.generateKeys();
175
+ const pubKey = ecdh.getPublicKey("hex");
176
+ const handler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler("api-key", AvoInspectorEnv_1.AvoInspectorEnv.Prod, "app", "1.0", inspectorVersion, pubKey);
177
+ const body = handler.bodyForEventSchemaCall("anon-id", "test-event", [{ propertyName: "username", propertyType: "string" }], null, null);
178
+ expect(body.eventProperties.length).toBe(1);
179
+ expect(body.eventProperties[0].propertyType).toBe("string");
180
+ expect(body.eventProperties[0].encryptedPropertyValue).toBeUndefined();
181
+ });
182
+ test("list-type properties are omitted entirely when encryption is active", () => {
183
+ const ecdh = crypto.createECDH("prime256v1");
184
+ ecdh.generateKeys();
185
+ const pubKey = ecdh.getPublicKey("hex");
186
+ const handler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler("api-key", AvoInspectorEnv_1.AvoInspectorEnv.Dev, "app", "1.0", inspectorVersion, pubKey);
187
+ const body = handler.bodyForEventSchemaCall("anon-id", "test-event", [
188
+ { propertyName: "name", propertyType: "string" },
189
+ { propertyName: "tags", propertyType: "list(string)" },
190
+ { propertyName: "scores", propertyType: "list(int)" },
191
+ ], null, null, { name: "Alice", tags: ["a", "b"], scores: [1, 2] });
192
+ // List types should be omitted
193
+ expect(body.eventProperties.length).toBe(1);
194
+ expect(body.eventProperties[0].propertyName).toBe("name");
195
+ });
196
+ test("encryption failure: property is omitted, warning logged", () => {
197
+ const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
198
+ const handler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler("api-key", AvoInspectorEnv_1.AvoInspectorEnv.Dev, "app", "1.0", inspectorVersion, "invalid-key-that-will-fail");
199
+ const body = handler.bodyForEventSchemaCall("anon-id", "test-event", [{ propertyName: "prop1", propertyType: "string" }], null, null, { prop1: "hello" });
200
+ // Property should be omitted on failure
201
+ expect(body.eventProperties.length).toBe(0);
202
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("[Avo Inspector] Warning:"));
203
+ warnSpy.mockRestore();
204
+ });
205
+ test("non-string value (number) is JSON.stringify'd before encryption", () => {
206
+ const ecdh = crypto.createECDH("prime256v1");
207
+ ecdh.generateKeys();
208
+ const pubKey = ecdh.getPublicKey("hex");
209
+ const handler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler("api-key", AvoInspectorEnv_1.AvoInspectorEnv.Dev, "app", "1.0", inspectorVersion, pubKey);
210
+ const body = handler.bodyForEventSchemaCall("anon-id", "test-event", [{ propertyName: "count", propertyType: "int" }], null, null, { count: 42 });
211
+ expect(body.eventProperties.length).toBe(1);
212
+ const encryptedValue = body.eventProperties[0].encryptedPropertyValue;
213
+ expect(encryptedValue).toBeDefined();
214
+ // Decrypt and verify the value is the JSON-stringified number "42"
215
+ const { ephemeralPubKey, iv, authTag, ciphertext } = parseWireFormat(encryptedValue);
216
+ const sharedSecret = ecdh.computeSecret(ephemeralPubKey);
217
+ const aesKey = crypto.createHash("sha256").update(sharedSecret).digest();
218
+ const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv, {
219
+ authTagLength: 16,
220
+ });
221
+ decipher.setAuthTag(authTag);
222
+ let decrypted = decipher.update(ciphertext);
223
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
224
+ expect(decrypted.toString("utf8")).toBe("42");
225
+ });
226
+ test("missing property in rawEventProperties encrypts 'null' instead of crashing", () => {
227
+ const ecdh = crypto.createECDH("prime256v1");
228
+ ecdh.generateKeys();
229
+ const pubKey = ecdh.getPublicKey("hex");
230
+ const handler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler("api-key", AvoInspectorEnv_1.AvoInspectorEnv.Dev, "app", "1.0", inspectorVersion, pubKey);
231
+ // Schema says "missing_prop" exists, but rawEventProperties does NOT have it
232
+ const body = handler.bodyForEventSchemaCall("anon-id", "test-event", [{ propertyName: "missing_prop", propertyType: "string" }], null, null, { other_prop: "value" });
233
+ expect(body.eventProperties.length).toBe(1);
234
+ const encryptedValue = body.eventProperties[0].encryptedPropertyValue;
235
+ expect(encryptedValue).toBeDefined();
236
+ // Decrypt and verify the value is "null"
237
+ const { ephemeralPubKey, iv, authTag, ciphertext } = parseWireFormat(encryptedValue);
238
+ const sharedSecret = ecdh.computeSecret(ephemeralPubKey);
239
+ const aesKey = crypto.createHash("sha256").update(sharedSecret).digest();
240
+ const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv, {
241
+ authTagLength: 16,
242
+ });
243
+ decipher.setAuthTag(authTag);
244
+ let decrypted = decipher.update(ciphertext);
245
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
246
+ expect(decrypted.toString("utf8")).toBe("null");
247
+ });
248
+ test("children are preserved in encrypted property output", () => {
249
+ const ecdh = crypto.createECDH("prime256v1");
250
+ ecdh.generateKeys();
251
+ const pubKey = ecdh.getPublicKey("hex");
252
+ const handler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler("api-key", AvoInspectorEnv_1.AvoInspectorEnv.Dev, "app", "1.0", inspectorVersion, pubKey);
253
+ const children = [
254
+ { propertyName: "street", propertyType: "string" },
255
+ { propertyName: "zip", propertyType: "int" },
256
+ ];
257
+ const body = handler.bodyForEventSchemaCall("anon-id", "test-event", [{ propertyName: "address", propertyType: "object", children }], null, null, { address: { street: "123 Main St", zip: 12345 } });
258
+ expect(body.eventProperties.length).toBe(1);
259
+ const prop = body.eventProperties[0];
260
+ expect(prop.propertyName).toBe("address");
261
+ expect(prop.encryptedPropertyValue).toBeDefined();
262
+ expect(prop.children).toEqual(children);
263
+ });
264
+ test("cross-SDK interop: encrypted wire format structure is correct", () => {
265
+ const ecdh = crypto.createECDH("prime256v1");
266
+ ecdh.generateKeys();
267
+ const pubKey = ecdh.getPublicKey("hex");
268
+ const handler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler("api-key", AvoInspectorEnv_1.AvoInspectorEnv.Dev, "app", "1.0", inspectorVersion, pubKey);
269
+ const body = handler.bodyForEventSchemaCall("anon-id", "test-event", [{ propertyName: "email", propertyType: "string" }], null, null, { email: "user@example.com" });
270
+ const encryptedValue = body.eventProperties[0].encryptedPropertyValue;
271
+ expect(encryptedValue).toBeDefined();
272
+ const { version, ephemeralPubKey, iv, authTag, ciphertext, wire } = parseWireFormat(encryptedValue);
273
+ // Verify wire format: [0x00][65-byte ephemeral pubkey][16-byte IV][16-byte auth tag][ciphertext]
274
+ expect(wire.length).toBeGreaterThanOrEqual(99);
275
+ expect(version).toBe(0x00); // version
276
+ expect(ephemeralPubKey[0]).toBe(0x04); // uncompressed pubkey marker
277
+ // Verify we can decrypt it
278
+ const sharedSecret = ecdh.computeSecret(ephemeralPubKey);
279
+ const aesKey = crypto.createHash("sha256").update(sharedSecret).digest();
280
+ const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv, {
281
+ authTagLength: 16,
282
+ });
283
+ decipher.setAuthTag(authTag);
284
+ let decrypted = decipher.update(ciphertext);
285
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
286
+ expect(decrypted.toString("utf8")).toBe('"user@example.com"');
287
+ });
288
+ });
@@ -0,0 +1 @@
1
+ export {};