node-avo-inspector 1.2.0-alpha.1 → 2.0.0-beta.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 (105) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +268 -4
  3. package/package.json +17 -1
  4. package/.claude/scheduled_tasks.lock +0 -1
  5. package/.claude/settings.local.json +0 -11
  6. package/.claude/worktrees/release-1.1.1/LICENSE +0 -21
  7. package/.claude/worktrees/release-1.1.1/README.md +0 -69
  8. package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/index.js +0 -30
  9. package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/package.json +0 -15
  10. package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/yarn.lock +0 -6
  11. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/dist/index.js +0 -34
  12. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/index.ts +0 -30
  13. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/package.json +0 -18
  14. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/tsconfig.json +0 -15
  15. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/yarn.lock +0 -11
  16. package/.claude/worktrees/release-1.1.1/package-lock.json +0 -4039
  17. package/.claude/worktrees/release-1.1.1/package.json +0 -48
  18. package/.claude/worktrees/release-1.1.1/reason/AvoInspector.rei +0 -11
  19. package/.claude/worktrees/release-1.1.1/reason/NodeAvoInspector.re +0 -27
  20. package/.claude/worktrees/release-1.1.1/src/AvoDeduplicator.ts +0 -146
  21. package/.claude/worktrees/release-1.1.1/src/AvoEncryption.ts +0 -90
  22. package/.claude/worktrees/release-1.1.1/src/AvoGuid.ts +0 -9
  23. package/.claude/worktrees/release-1.1.1/src/AvoInspector.ts +0 -383
  24. package/.claude/worktrees/release-1.1.1/src/AvoInspectorEnv.ts +0 -8
  25. package/.claude/worktrees/release-1.1.1/src/AvoNetworkCallsHandler.ts +0 -267
  26. package/.claude/worktrees/release-1.1.1/src/AvoSchemaParser.ts +0 -115
  27. package/.claude/worktrees/release-1.1.1/src/AvoStreamId.ts +0 -16
  28. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEncryption.ts +0 -515
  29. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEventSpecCache.ts +0 -242
  30. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEventSpecFetcher.ts +0 -241
  31. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoInspector.ts +0 -360
  32. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoStreamId.ts +0 -72
  33. package/.claude/worktrees/release-1.1.1/src/__tests__/TestDeduplicator.ts +0 -191
  34. package/.claude/worktrees/release-1.1.1/src/__tests__/TestEventValidator.ts +0 -231
  35. package/.claude/worktrees/release-1.1.1/src/__tests__/TestNetworkCallsHandler.ts +0 -179
  36. package/.claude/worktrees/release-1.1.1/src/__tests__/TestNetworkCallsHandlerPublic.ts +0 -208
  37. package/.claude/worktrees/release-1.1.1/src/__tests__/TestParsing.ts +0 -153
  38. package/.claude/worktrees/release-1.1.1/src/__tests__/constants.ts +0 -63
  39. package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecCache.ts +0 -109
  40. package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecFetchTypes.ts +0 -47
  41. package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecFetcher.ts +0 -106
  42. package/.claude/worktrees/release-1.1.1/src/eventSpec/EventValidator.ts +0 -74
  43. package/.claude/worktrees/release-1.1.1/src/index.ts +0 -2
  44. package/.claude/worktrees/release-1.1.1/src/utils.ts +0 -49
  45. package/.claude/worktrees/release-1.1.1/tsconfig.json +0 -13
  46. package/.claude/worktrees/release-1.1.1/yarn.lock +0 -2310
  47. package/.github/workflows/claude.yml +0 -55
  48. package/dist/__tests__/TestAvoEncryption.d.ts +0 -1
  49. package/dist/__tests__/TestAvoEncryption.js +0 -288
  50. package/dist/__tests__/TestAvoEventSpecCache.d.ts +0 -1
  51. package/dist/__tests__/TestAvoEventSpecCache.js +0 -194
  52. package/dist/__tests__/TestAvoEventSpecFetcher.d.ts +0 -1
  53. package/dist/__tests__/TestAvoEventSpecFetcher.js +0 -219
  54. package/dist/__tests__/TestAvoInspector.d.ts +0 -1
  55. package/dist/__tests__/TestAvoInspector.js +0 -291
  56. package/dist/__tests__/TestAvoStreamId.d.ts +0 -1
  57. package/dist/__tests__/TestAvoStreamId.js +0 -55
  58. package/dist/__tests__/TestDeduplicator.d.ts +0 -1
  59. package/dist/__tests__/TestDeduplicator.js +0 -102
  60. package/dist/__tests__/TestEventValidator.d.ts +0 -1
  61. package/dist/__tests__/TestEventValidator.js +0 -196
  62. package/dist/__tests__/TestNetworkCallsHandler.d.ts +0 -1
  63. package/dist/__tests__/TestNetworkCallsHandler.js +0 -86
  64. package/dist/__tests__/TestNetworkCallsHandlerPublic.d.ts +0 -1
  65. package/dist/__tests__/TestNetworkCallsHandlerPublic.js +0 -160
  66. package/dist/__tests__/TestParsing.d.ts +0 -1
  67. package/dist/__tests__/TestParsing.js +0 -128
  68. package/dist/__tests__/constants.d.ts +0 -42
  69. package/dist/__tests__/constants.js +0 -53
  70. package/examples/node-inspector-js/index.js +0 -30
  71. package/examples/node-inspector-js/package.json +0 -15
  72. package/examples/node-inspector-js/yarn.lock +0 -6
  73. package/examples/node-inspector-ts/dist/index.js +0 -34
  74. package/examples/node-inspector-ts/index.ts +0 -30
  75. package/examples/node-inspector-ts/package.json +0 -18
  76. package/examples/node-inspector-ts/tsconfig.json +0 -15
  77. package/examples/node-inspector-ts/yarn.lock +0 -11
  78. package/reason/AvoInspector.rei +0 -11
  79. package/reason/NodeAvoInspector.re +0 -27
  80. package/src/AvoDeduplicator.ts +0 -150
  81. package/src/AvoEncryption.ts +0 -90
  82. package/src/AvoGuid.ts +0 -9
  83. package/src/AvoInspector.ts +0 -498
  84. package/src/AvoInspectorEnv.ts +0 -8
  85. package/src/AvoNetworkCallsHandler.ts +0 -340
  86. package/src/AvoSchemaParser.ts +0 -115
  87. package/src/AvoStreamId.ts +0 -16
  88. package/src/__tests__/TestAvoEncryption.ts +0 -515
  89. package/src/__tests__/TestAvoEventSpecCache.ts +0 -242
  90. package/src/__tests__/TestAvoEventSpecFetcher.ts +0 -279
  91. package/src/__tests__/TestAvoInspector.ts +0 -376
  92. package/src/__tests__/TestAvoStreamId.ts +0 -72
  93. package/src/__tests__/TestDeduplicator.ts +0 -191
  94. package/src/__tests__/TestEventValidator.ts +0 -231
  95. package/src/__tests__/TestNetworkCallsHandler.ts +0 -181
  96. package/src/__tests__/TestNetworkCallsHandlerPublic.ts +0 -208
  97. package/src/__tests__/TestParsing.ts +0 -153
  98. package/src/__tests__/constants.ts +0 -63
  99. package/src/eventSpec/AvoEventSpecCache.ts +0 -119
  100. package/src/eventSpec/AvoEventSpecFetchTypes.ts +0 -47
  101. package/src/eventSpec/AvoEventSpecFetcher.ts +0 -186
  102. package/src/eventSpec/EventValidator.ts +0 -74
  103. package/src/index.ts +0 -2
  104. package/src/utils.ts +0 -49
  105. package/tsconfig.json +0 -13
@@ -1,115 +0,0 @@
1
- let isArray = (obj: any): boolean => {
2
- return Object.prototype.toString.call(obj) === "[object Array]";
3
- };
4
-
5
- export class AvoSchemaParser {
6
- static extractSchema(eventProperties: {
7
- [propName: string]: any;
8
- }): Array<{
9
- propertyName: string;
10
- propertyType: string;
11
- children?: any;
12
- }> {
13
- if (eventProperties === null || eventProperties === undefined) {
14
- return [];
15
- }
16
-
17
- let mapping = (object: any) => {
18
- if (isArray(object)) {
19
- let list = object.map((x: any) => {
20
- return mapping(x);
21
- });
22
- return this.removeDuplicates(list);
23
- } else if (typeof object === "object") {
24
- let mappedResult: any = [];
25
- for (var key in object) {
26
- if (object.hasOwnProperty(key)) {
27
- let val = object[key];
28
-
29
- let mappedEntry: {
30
- propertyName: string;
31
- propertyType: string;
32
- children?: any;
33
- } = {
34
- propertyName: key,
35
- propertyType: this.getPropValueType(val),
36
- };
37
-
38
- if (typeof val === "object" && val != null) {
39
- mappedEntry["children"] = mapping(val);
40
- }
41
-
42
- mappedResult.push(mappedEntry);
43
- }
44
- }
45
-
46
- return mappedResult;
47
- } else {
48
- return this.getPropValueType(object);
49
- }
50
- };
51
-
52
- var mappedEventProps = mapping(eventProperties);
53
-
54
- return mappedEventProps;
55
- }
56
-
57
- private static removeDuplicates(array: Array<any>): Array<any> {
58
- // XXX TODO fix any types
59
- var primitives: any = { boolean: {}, number: {}, string: {} };
60
- var objects: Array<any> = [];
61
-
62
- return array.filter((item: any) => {
63
- var type: string = typeof item;
64
- if (type in primitives) {
65
- return primitives[type].hasOwnProperty(item)
66
- ? false
67
- : (primitives[type][item] = true);
68
- } else {
69
- return objects.indexOf(item) >= 0 ? false : objects.push(item);
70
- }
71
- });
72
- }
73
-
74
-
75
- private static getBasicPropType(propValue: any): string {
76
- let propType = typeof propValue;
77
- if (propValue == null) {
78
- return "null";
79
- } else if (propType === "string") {
80
- return "string";
81
- } else if (propType === "number" || propType === "bigint") {
82
- if ((propValue + "").indexOf(".") >= 0) {
83
- return "float";
84
- } else {
85
- return "int";
86
- }
87
- } else if (propType === "boolean") {
88
- return "boolean";
89
- } else if (propType === "object") {
90
- return "object"
91
- }
92
- else {
93
- return "unknown";
94
- }
95
- }
96
-
97
- private static getPropValueType(propValue: any): string {
98
- if (isArray(propValue)){
99
-
100
- //we now know that propValue is an array. get first element in propValue array
101
- let propElement = propValue[0];
102
-
103
- if (propElement == null) {
104
- return "list(string)"; // Default to list(string) if the list is empty.
105
- }
106
- else {
107
- let propElementType = this.getBasicPropType(propElement);
108
- return `list(${propElementType})`
109
- }
110
- }
111
- else {
112
- return this.getBasicPropType(propValue);
113
- }
114
- }
115
- }
@@ -1,16 +0,0 @@
1
- export class AvoStreamId {
2
- private _streamId: string;
3
-
4
- constructor(streamId?: string) {
5
- this._streamId = streamId ?? "";
6
- if (this._streamId.includes(":")) {
7
- console.warn(
8
- "[Avo Inspector] Warning: streamId contains ':' which is not supported"
9
- );
10
- }
11
- }
12
-
13
- get streamId(): string {
14
- return this._streamId;
15
- }
16
- }
@@ -1,515 +0,0 @@
1
- import * as crypto from "crypto";
2
- import { AvoEncryption } from "../AvoEncryption";
3
- import { AvoNetworkCallsHandler } from "../AvoNetworkCallsHandler";
4
- import { AvoInspectorEnv } from "../AvoInspectorEnv";
5
- import { AvoGuid } from "../AvoGuid";
6
- import { mockedReturns } from "./constants";
7
-
8
- // Generate a test key pair for encryption tests
9
- function generateTestKeyPair() {
10
- const ecdh = crypto.createECDH("prime256v1");
11
- ecdh.generateKeys();
12
- return {
13
- publicKey: ecdh.getPublicKey("hex"),
14
- privateKey: ecdh.getPrivateKey(),
15
- ecdh,
16
- };
17
- }
18
-
19
- function parseWireFormat(encryptedBase64: string) {
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
-
31
- describe("AvoEncryption", () => {
32
- describe("shouldEncrypt", () => {
33
- const dummyHexKey = "deadbeef";
34
-
35
- test("dev + key => true", () => {
36
- expect(AvoEncryption.shouldEncrypt(AvoInspectorEnv.Dev, dummyHexKey)).toBe(true);
37
- });
38
-
39
- test("staging + key => true", () => {
40
- expect(AvoEncryption.shouldEncrypt(AvoInspectorEnv.Staging, dummyHexKey)).toBe(true);
41
- });
42
-
43
- test("prod + key => false", () => {
44
- expect(AvoEncryption.shouldEncrypt(AvoInspectorEnv.Prod, dummyHexKey)).toBe(false);
45
- });
46
-
47
- test("dev + null => false", () => {
48
- expect(AvoEncryption.shouldEncrypt(AvoInspectorEnv.Dev, undefined)).toBe(false);
49
- });
50
-
51
- test("dev + empty string => false", () => {
52
- expect(AvoEncryption.shouldEncrypt(AvoInspectorEnv.Dev, "")).toBe(false);
53
- });
54
- });
55
-
56
- describe("wire format", () => {
57
- const testKeyPair = generateTestKeyPair();
58
-
59
- test("base64Decode(output).length >= 99", () => {
60
- const encrypted = AvoEncryption.encryptValue("hello", testKeyPair.publicKey);
61
- expect(encrypted).not.toBeNull();
62
- const decoded = Buffer.from(encrypted!, "base64");
63
- // 1 (version) + 65 (ephemeral pubkey) + 16 (IV) + 16 (auth tag) + ciphertext >= 1
64
- expect(decoded.length).toBeGreaterThanOrEqual(99);
65
- });
66
-
67
- test("output[0] == 0x00 (version byte)", () => {
68
- const encrypted = AvoEncryption.encryptValue("hello", testKeyPair.publicKey);
69
- const decoded = Buffer.from(encrypted!, "base64");
70
- expect(decoded[0]).toBe(0x00);
71
- });
72
-
73
- test("output[1] == 0x04 (uncompressed pubkey marker)", () => {
74
- const encrypted = AvoEncryption.encryptValue("hello", testKeyPair.publicKey);
75
- const decoded = Buffer.from(encrypted!, "base64");
76
- expect(decoded[1]).toBe(0x04);
77
- });
78
-
79
- test("ephemeral public key is 65 bytes (uncompressed)", () => {
80
- const encrypted = AvoEncryption.encryptValue("test data", testKeyPair.publicKey);
81
- const decoded = Buffer.from(encrypted!, "base64");
82
- // bytes 1..65 are the ephemeral public key
83
- const ephemeralPubKey = decoded.subarray(1, 66);
84
- expect(ephemeralPubKey.length).toBe(65);
85
- expect(ephemeralPubKey[0]).toBe(0x04); // uncompressed marker
86
- });
87
- });
88
-
89
- describe("round-trip encrypt/decrypt", () => {
90
- test("can decrypt back to original plaintext", () => {
91
- const ecdh = crypto.createECDH("prime256v1");
92
- ecdh.generateKeys();
93
- const recipientPubKeyHex = ecdh.getPublicKey("hex");
94
-
95
- const plaintext = "Hello, encryption!";
96
- const encrypted = AvoEncryption.encryptValue(plaintext, recipientPubKeyHex);
97
- expect(encrypted).not.toBeNull();
98
-
99
- // Decrypt: parse wire format
100
- const { version, ephemeralPubKey, iv, authTag, ciphertext } = parseWireFormat(encrypted!);
101
- expect(version).toBe(0x00);
102
-
103
- // Compute shared secret the same way
104
- const sharedSecret = ecdh.computeSecret(ephemeralPubKey);
105
- const aesKey = crypto.createHash("sha256").update(sharedSecret).digest();
106
-
107
- const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv, {
108
- authTagLength: 16,
109
- });
110
- decipher.setAuthTag(authTag);
111
- let decrypted = decipher.update(ciphertext);
112
- decrypted = Buffer.concat([decrypted, decipher.final()]);
113
-
114
- expect(decrypted.toString("utf8")).toBe(plaintext);
115
- });
116
-
117
- test("different encryptions of same value produce different ciphertext", () => {
118
- const ecdh = crypto.createECDH("prime256v1");
119
- ecdh.generateKeys();
120
- const pubKey = ecdh.getPublicKey("hex");
121
-
122
- const enc1 = AvoEncryption.encryptValue("same", pubKey);
123
- const enc2 = AvoEncryption.encryptValue("same", pubKey);
124
- // Ephemeral keys differ each time, so output differs
125
- expect(enc1).not.toBe(enc2);
126
- });
127
- });
128
-
129
- describe("encryption failure handling", () => {
130
- test("returns null and warns on invalid key", () => {
131
- const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
132
- const result = AvoEncryption.encryptValue("hello", "not-a-valid-hex-key");
133
- expect(result).toBeNull();
134
- expect(warnSpy).toHaveBeenCalledWith(
135
- expect.stringContaining("[Avo Inspector] Warning:")
136
- );
137
- warnSpy.mockRestore();
138
- });
139
- });
140
-
141
- describe("isListType", () => {
142
- test("detects list types", () => {
143
- expect(AvoEncryption.isListType("list(string)")).toBe(true);
144
- expect(AvoEncryption.isListType("list(int)")).toBe(true);
145
- expect(AvoEncryption.isListType("list(object)")).toBe(true);
146
- });
147
-
148
- test("non-list types return false", () => {
149
- expect(AvoEncryption.isListType("string")).toBe(false);
150
- expect(AvoEncryption.isListType("int")).toBe(false);
151
- expect(AvoEncryption.isListType("object")).toBe(false);
152
- });
153
- });
154
- });
155
-
156
- describe("AvoNetworkCallsHandler encryption integration", () => {
157
- const inspectorVersion = process.env.npm_package_version || "";
158
-
159
- beforeEach(() => {
160
- const now = new Date();
161
- // @ts-ignore
162
- jest.spyOn(global, "Date").mockImplementation(() => now);
163
- jest
164
- .spyOn(AvoGuid as any, "newGuid")
165
- .mockImplementation(() => mockedReturns.GUID);
166
- });
167
-
168
- afterEach(() => {
169
- jest.restoreAllMocks();
170
- });
171
-
172
- test("publicEncryptionKey included in base body when non-null and non-empty", () => {
173
- const pubKey = "deadbeef";
174
- const handler = new AvoNetworkCallsHandler(
175
- "api-key",
176
- AvoInspectorEnv.Dev,
177
- "app",
178
- "1.0",
179
- inspectorVersion,
180
- pubKey
181
- );
182
-
183
- const body = handler.bodyForEventSchemaCall(
184
- "anon-id",
185
- "test-event",
186
- [{ propertyName: "prop1", propertyType: "string" }],
187
- null,
188
- null
189
- );
190
-
191
- expect((body as any).publicEncryptionKey).toBe(pubKey);
192
- });
193
-
194
- test("publicEncryptionKey NOT included in base body when null", () => {
195
- const handler = new AvoNetworkCallsHandler(
196
- "api-key",
197
- AvoInspectorEnv.Dev,
198
- "app",
199
- "1.0",
200
- inspectorVersion,
201
- undefined
202
- );
203
-
204
- const body = handler.bodyForEventSchemaCall(
205
- "anon-id",
206
- "test-event",
207
- [{ propertyName: "prop1", propertyType: "string" }],
208
- null,
209
- null
210
- );
211
-
212
- expect((body as any).publicEncryptionKey).toBeUndefined();
213
- });
214
-
215
- test("publicEncryptionKey NOT included in base body when empty string", () => {
216
- const handler = new AvoNetworkCallsHandler(
217
- "api-key",
218
- AvoInspectorEnv.Dev,
219
- "app",
220
- "1.0",
221
- inspectorVersion,
222
- ""
223
- );
224
-
225
- const body = handler.bodyForEventSchemaCall(
226
- "anon-id",
227
- "test-event",
228
- [{ propertyName: "prop1", propertyType: "string" }],
229
- null,
230
- null
231
- );
232
-
233
- expect((body as any).publicEncryptionKey).toBeUndefined();
234
- });
235
-
236
- test("dev env with key: properties are encrypted with encryptedPropertyValue", () => {
237
- const ecdh = crypto.createECDH("prime256v1");
238
- ecdh.generateKeys();
239
- const pubKey = ecdh.getPublicKey("hex");
240
-
241
- const handler = new AvoNetworkCallsHandler(
242
- "api-key",
243
- AvoInspectorEnv.Dev,
244
- "app",
245
- "1.0",
246
- inspectorVersion,
247
- pubKey
248
- );
249
-
250
- const body = handler.bodyForEventSchemaCall(
251
- "anon-id",
252
- "test-event",
253
- [{ propertyName: "username", propertyType: "string" }],
254
- null,
255
- null,
256
- { username: "alice" }
257
- );
258
-
259
- // Should have encryptedPropertyValue alongside propertyType
260
- expect(body.eventProperties.length).toBe(1);
261
- expect(body.eventProperties[0].propertyName).toBe("username");
262
- expect((body.eventProperties[0] as any).encryptedPropertyValue).toBeDefined();
263
- expect((body.eventProperties[0] as any).propertyType).toBe("string");
264
- });
265
-
266
- test("prod env: no encryptedPropertyValue, propertyType kept as-is", () => {
267
- const ecdh = crypto.createECDH("prime256v1");
268
- ecdh.generateKeys();
269
- const pubKey = ecdh.getPublicKey("hex");
270
-
271
- const handler = new AvoNetworkCallsHandler(
272
- "api-key",
273
- AvoInspectorEnv.Prod,
274
- "app",
275
- "1.0",
276
- inspectorVersion,
277
- pubKey
278
- );
279
-
280
- const body = handler.bodyForEventSchemaCall(
281
- "anon-id",
282
- "test-event",
283
- [{ propertyName: "username", propertyType: "string" }],
284
- null,
285
- null
286
- );
287
-
288
- expect(body.eventProperties.length).toBe(1);
289
- expect((body.eventProperties[0] as any).propertyType).toBe("string");
290
- expect((body.eventProperties[0] as any).encryptedPropertyValue).toBeUndefined();
291
- });
292
-
293
- test("list-type properties are omitted entirely when encryption is active", () => {
294
- const ecdh = crypto.createECDH("prime256v1");
295
- ecdh.generateKeys();
296
- const pubKey = ecdh.getPublicKey("hex");
297
-
298
- const handler = new AvoNetworkCallsHandler(
299
- "api-key",
300
- AvoInspectorEnv.Dev,
301
- "app",
302
- "1.0",
303
- inspectorVersion,
304
- pubKey
305
- );
306
-
307
- const body = handler.bodyForEventSchemaCall(
308
- "anon-id",
309
- "test-event",
310
- [
311
- { propertyName: "name", propertyType: "string" },
312
- { propertyName: "tags", propertyType: "list(string)" },
313
- { propertyName: "scores", propertyType: "list(int)" },
314
- ],
315
- null,
316
- null,
317
- { name: "Alice", tags: ["a", "b"], scores: [1, 2] }
318
- );
319
-
320
- // List types should be omitted
321
- expect(body.eventProperties.length).toBe(1);
322
- expect(body.eventProperties[0].propertyName).toBe("name");
323
- });
324
-
325
- test("encryption failure: property is omitted, warning logged", () => {
326
- const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
327
-
328
- const handler = new AvoNetworkCallsHandler(
329
- "api-key",
330
- AvoInspectorEnv.Dev,
331
- "app",
332
- "1.0",
333
- inspectorVersion,
334
- "invalid-key-that-will-fail"
335
- );
336
-
337
- const body = handler.bodyForEventSchemaCall(
338
- "anon-id",
339
- "test-event",
340
- [{ propertyName: "prop1", propertyType: "string" }],
341
- null,
342
- null,
343
- { prop1: "hello" }
344
- );
345
-
346
- // Property should be omitted on failure
347
- expect(body.eventProperties.length).toBe(0);
348
- expect(warnSpy).toHaveBeenCalledWith(
349
- expect.stringContaining("[Avo Inspector] Warning:")
350
- );
351
- warnSpy.mockRestore();
352
- });
353
-
354
- test("non-string value (number) is JSON.stringify'd before encryption", () => {
355
- const ecdh = crypto.createECDH("prime256v1");
356
- ecdh.generateKeys();
357
- const pubKey = ecdh.getPublicKey("hex");
358
-
359
- const handler = new AvoNetworkCallsHandler(
360
- "api-key",
361
- AvoInspectorEnv.Dev,
362
- "app",
363
- "1.0",
364
- inspectorVersion,
365
- pubKey
366
- );
367
-
368
- const body = handler.bodyForEventSchemaCall(
369
- "anon-id",
370
- "test-event",
371
- [{ propertyName: "count", propertyType: "int" }],
372
- null,
373
- null,
374
- { count: 42 }
375
- );
376
-
377
- expect(body.eventProperties.length).toBe(1);
378
- const encryptedValue = (body.eventProperties[0] as any).encryptedPropertyValue;
379
- expect(encryptedValue).toBeDefined();
380
-
381
- // Decrypt and verify the value is the JSON-stringified number "42"
382
- const { ephemeralPubKey, iv, authTag, ciphertext } = parseWireFormat(encryptedValue);
383
-
384
- const sharedSecret = ecdh.computeSecret(ephemeralPubKey);
385
- const aesKey = crypto.createHash("sha256").update(sharedSecret).digest();
386
- const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv, {
387
- authTagLength: 16,
388
- });
389
- decipher.setAuthTag(authTag);
390
- let decrypted = decipher.update(ciphertext);
391
- decrypted = Buffer.concat([decrypted, decipher.final()]);
392
- expect(decrypted.toString("utf8")).toBe("42");
393
- });
394
-
395
- test("missing property in rawEventProperties encrypts 'null' instead of crashing", () => {
396
- const ecdh = crypto.createECDH("prime256v1");
397
- ecdh.generateKeys();
398
- const pubKey = ecdh.getPublicKey("hex");
399
-
400
- const handler = new AvoNetworkCallsHandler(
401
- "api-key",
402
- AvoInspectorEnv.Dev,
403
- "app",
404
- "1.0",
405
- inspectorVersion,
406
- pubKey
407
- );
408
-
409
- // Schema says "missing_prop" exists, but rawEventProperties does NOT have it
410
- const body = handler.bodyForEventSchemaCall(
411
- "anon-id",
412
- "test-event",
413
- [{ propertyName: "missing_prop", propertyType: "string" }],
414
- null,
415
- null,
416
- { other_prop: "value" }
417
- );
418
-
419
- expect(body.eventProperties.length).toBe(1);
420
- const encryptedValue = (body.eventProperties[0] as any).encryptedPropertyValue;
421
- expect(encryptedValue).toBeDefined();
422
-
423
- // Decrypt and verify the value is "null"
424
- const { ephemeralPubKey, iv, authTag, ciphertext } = parseWireFormat(encryptedValue);
425
-
426
- const sharedSecret = ecdh.computeSecret(ephemeralPubKey);
427
- const aesKey = crypto.createHash("sha256").update(sharedSecret).digest();
428
- const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv, {
429
- authTagLength: 16,
430
- });
431
- decipher.setAuthTag(authTag);
432
- let decrypted = decipher.update(ciphertext);
433
- decrypted = Buffer.concat([decrypted, decipher.final()]);
434
- expect(decrypted.toString("utf8")).toBe("null");
435
- });
436
-
437
- test("children are preserved in encrypted property output", () => {
438
- const ecdh = crypto.createECDH("prime256v1");
439
- ecdh.generateKeys();
440
- const pubKey = ecdh.getPublicKey("hex");
441
-
442
- const handler = new AvoNetworkCallsHandler(
443
- "api-key",
444
- AvoInspectorEnv.Dev,
445
- "app",
446
- "1.0",
447
- inspectorVersion,
448
- pubKey
449
- );
450
-
451
- const children = [
452
- { propertyName: "street", propertyType: "string" },
453
- { propertyName: "zip", propertyType: "int" },
454
- ];
455
-
456
- const body = handler.bodyForEventSchemaCall(
457
- "anon-id",
458
- "test-event",
459
- [{ propertyName: "address", propertyType: "object", children }],
460
- null,
461
- null,
462
- { address: { street: "123 Main St", zip: 12345 } }
463
- );
464
-
465
- expect(body.eventProperties.length).toBe(1);
466
- const prop = body.eventProperties[0] as any;
467
- expect(prop.propertyName).toBe("address");
468
- expect(prop.encryptedPropertyValue).toBeDefined();
469
- expect(prop.children).toEqual(children);
470
- });
471
-
472
- test("cross-SDK interop: encrypted wire format structure is correct", () => {
473
- const ecdh = crypto.createECDH("prime256v1");
474
- ecdh.generateKeys();
475
- const pubKey = ecdh.getPublicKey("hex");
476
-
477
- const handler = new AvoNetworkCallsHandler(
478
- "api-key",
479
- AvoInspectorEnv.Dev,
480
- "app",
481
- "1.0",
482
- inspectorVersion,
483
- pubKey
484
- );
485
-
486
- const body = handler.bodyForEventSchemaCall(
487
- "anon-id",
488
- "test-event",
489
- [{ propertyName: "email", propertyType: "string" }],
490
- null,
491
- null,
492
- { email: "user@example.com" }
493
- );
494
-
495
- const encryptedValue = (body.eventProperties[0] as any).encryptedPropertyValue;
496
- expect(encryptedValue).toBeDefined();
497
-
498
- const { version, ephemeralPubKey, iv, authTag, ciphertext, wire } = parseWireFormat(encryptedValue);
499
- // Verify wire format: [0x00][65-byte ephemeral pubkey][16-byte IV][16-byte auth tag][ciphertext]
500
- expect(wire.length).toBeGreaterThanOrEqual(99);
501
- expect(version).toBe(0x00); // version
502
- expect(ephemeralPubKey[0]).toBe(0x04); // uncompressed pubkey marker
503
-
504
- // Verify we can decrypt it
505
- const sharedSecret = ecdh.computeSecret(ephemeralPubKey);
506
- const aesKey = crypto.createHash("sha256").update(sharedSecret).digest();
507
- const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv, {
508
- authTagLength: 16,
509
- });
510
- decipher.setAuthTag(authTag);
511
- let decrypted = decipher.update(ciphertext);
512
- decrypted = Buffer.concat([decrypted, decipher.final()]);
513
- expect(decrypted.toString("utf8")).toBe('"user@example.com"');
514
- });
515
- });