node-avo-inspector 1.2.0 → 2.0.0-beta.2
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.
- package/CHANGELOG.md +46 -0
- package/README.md +270 -4
- package/dist/AvoBatchQueue.d.ts +73 -0
- package/dist/AvoBatchQueue.js +239 -0
- package/dist/AvoDeduplicator.d.ts +15 -6
- package/dist/AvoDeduplicator.js +46 -24
- package/dist/AvoInspector.d.ts +136 -23
- package/dist/AvoInspector.js +777 -109
- package/dist/AvoInspectorVersion.d.ts +3 -0
- package/dist/AvoInspectorVersion.js +10 -0
- package/dist/AvoLog.d.ts +52 -0
- package/dist/AvoLog.js +246 -0
- package/dist/AvoNetworkCallsHandler.d.ts +55 -8
- package/dist/AvoNetworkCallsHandler.js +266 -92
- package/dist/AvoSchemaParser.d.ts +6 -0
- package/dist/AvoSchemaParser.js +138 -33
- package/dist/AvoStreamId.d.ts +2 -1
- package/dist/AvoStreamId.js +21 -2
- package/dist/eventSpec/AvoEventSpecCache.d.ts +28 -5
- package/dist/eventSpec/AvoEventSpecCache.js +66 -11
- package/dist/eventSpec/AvoEventSpecFetchTypes.d.ts +50 -18
- package/dist/eventSpec/AvoEventSpecFetcher.d.ts +29 -6
- package/dist/eventSpec/AvoEventSpecFetcher.js +279 -69
- package/dist/eventSpec/EventValidator.d.ts +16 -10
- package/dist/eventSpec/EventValidator.js +351 -38
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/utils.d.ts +29 -1
- package/dist/utils.js +86 -10
- package/package.json +18 -1
- package/.claude/scheduled_tasks.lock +0 -1
- package/.claude/settings.local.json +0 -11
- package/.claude/worktrees/release-1.1.1/LICENSE +0 -21
- package/.claude/worktrees/release-1.1.1/README.md +0 -69
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/index.js +0 -30
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/package.json +0 -15
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/yarn.lock +0 -6
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/dist/index.js +0 -34
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/index.ts +0 -30
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/package.json +0 -18
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/tsconfig.json +0 -15
- package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/yarn.lock +0 -11
- package/.claude/worktrees/release-1.1.1/package-lock.json +0 -4039
- package/.claude/worktrees/release-1.1.1/package.json +0 -48
- package/.claude/worktrees/release-1.1.1/reason/AvoInspector.rei +0 -11
- package/.claude/worktrees/release-1.1.1/reason/NodeAvoInspector.re +0 -27
- package/.claude/worktrees/release-1.1.1/src/AvoDeduplicator.ts +0 -146
- package/.claude/worktrees/release-1.1.1/src/AvoEncryption.ts +0 -90
- package/.claude/worktrees/release-1.1.1/src/AvoGuid.ts +0 -9
- package/.claude/worktrees/release-1.1.1/src/AvoInspector.ts +0 -383
- package/.claude/worktrees/release-1.1.1/src/AvoInspectorEnv.ts +0 -8
- package/.claude/worktrees/release-1.1.1/src/AvoNetworkCallsHandler.ts +0 -267
- package/.claude/worktrees/release-1.1.1/src/AvoSchemaParser.ts +0 -115
- package/.claude/worktrees/release-1.1.1/src/AvoStreamId.ts +0 -16
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEncryption.ts +0 -515
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEventSpecCache.ts +0 -242
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEventSpecFetcher.ts +0 -241
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoInspector.ts +0 -360
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoStreamId.ts +0 -72
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestDeduplicator.ts +0 -191
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestEventValidator.ts +0 -231
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestNetworkCallsHandler.ts +0 -179
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestNetworkCallsHandlerPublic.ts +0 -208
- package/.claude/worktrees/release-1.1.1/src/__tests__/TestParsing.ts +0 -153
- package/.claude/worktrees/release-1.1.1/src/__tests__/constants.ts +0 -63
- package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecCache.ts +0 -109
- package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecFetchTypes.ts +0 -47
- package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecFetcher.ts +0 -106
- package/.claude/worktrees/release-1.1.1/src/eventSpec/EventValidator.ts +0 -74
- package/.claude/worktrees/release-1.1.1/src/index.ts +0 -2
- package/.claude/worktrees/release-1.1.1/src/utils.ts +0 -49
- package/.claude/worktrees/release-1.1.1/tsconfig.json +0 -13
- package/.claude/worktrees/release-1.1.1/yarn.lock +0 -2310
- package/.github/workflows/claude.yml +0 -55
- package/dist/__tests__/TestAvoEncryption.d.ts +0 -1
- package/dist/__tests__/TestAvoEncryption.js +0 -288
- package/dist/__tests__/TestAvoEventSpecCache.d.ts +0 -1
- package/dist/__tests__/TestAvoEventSpecCache.js +0 -194
- package/dist/__tests__/TestAvoEventSpecFetcher.d.ts +0 -1
- package/dist/__tests__/TestAvoEventSpecFetcher.js +0 -219
- package/dist/__tests__/TestAvoInspector.d.ts +0 -1
- package/dist/__tests__/TestAvoInspector.js +0 -291
- package/dist/__tests__/TestAvoStreamId.d.ts +0 -1
- package/dist/__tests__/TestAvoStreamId.js +0 -55
- package/dist/__tests__/TestDeduplicator.d.ts +0 -1
- package/dist/__tests__/TestDeduplicator.js +0 -102
- package/dist/__tests__/TestEventValidator.d.ts +0 -1
- package/dist/__tests__/TestEventValidator.js +0 -196
- package/dist/__tests__/TestNetworkCallsHandler.d.ts +0 -1
- package/dist/__tests__/TestNetworkCallsHandler.js +0 -86
- package/dist/__tests__/TestNetworkCallsHandlerPublic.d.ts +0 -1
- package/dist/__tests__/TestNetworkCallsHandlerPublic.js +0 -160
- package/dist/__tests__/TestParsing.d.ts +0 -1
- package/dist/__tests__/TestParsing.js +0 -128
- package/dist/__tests__/constants.d.ts +0 -42
- package/dist/__tests__/constants.js +0 -53
- package/examples/node-inspector-js/index.js +0 -30
- package/examples/node-inspector-js/package.json +0 -15
- package/examples/node-inspector-js/yarn.lock +0 -6
- package/examples/node-inspector-ts/dist/index.js +0 -34
- package/examples/node-inspector-ts/index.ts +0 -30
- package/examples/node-inspector-ts/package.json +0 -18
- package/examples/node-inspector-ts/tsconfig.json +0 -15
- package/examples/node-inspector-ts/yarn.lock +0 -11
- package/reason/AvoInspector.rei +0 -11
- package/reason/NodeAvoInspector.re +0 -27
- package/src/AvoDeduplicator.ts +0 -150
- package/src/AvoEncryption.ts +0 -90
- package/src/AvoGuid.ts +0 -9
- package/src/AvoInspector.ts +0 -498
- package/src/AvoInspectorEnv.ts +0 -8
- package/src/AvoNetworkCallsHandler.ts +0 -340
- package/src/AvoSchemaParser.ts +0 -115
- package/src/AvoStreamId.ts +0 -16
- package/src/__tests__/TestAvoEncryption.ts +0 -515
- package/src/__tests__/TestAvoEventSpecCache.ts +0 -242
- package/src/__tests__/TestAvoEventSpecFetcher.ts +0 -279
- package/src/__tests__/TestAvoInspector.ts +0 -376
- package/src/__tests__/TestAvoStreamId.ts +0 -72
- package/src/__tests__/TestDeduplicator.ts +0 -191
- package/src/__tests__/TestEventValidator.ts +0 -231
- package/src/__tests__/TestNetworkCallsHandler.ts +0 -181
- package/src/__tests__/TestNetworkCallsHandlerPublic.ts +0 -208
- package/src/__tests__/TestParsing.ts +0 -153
- package/src/__tests__/constants.ts +0 -63
- package/src/eventSpec/AvoEventSpecCache.ts +0 -119
- package/src/eventSpec/AvoEventSpecFetchTypes.ts +0 -47
- package/src/eventSpec/AvoEventSpecFetcher.ts +0 -186
- package/src/eventSpec/EventValidator.ts +0 -74
- package/src/index.ts +0 -2
- package/src/utils.ts +0 -49
- package/tsconfig.json +0 -13
|
@@ -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
|
-
});
|