node-avo-inspector 1.2.0 → 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.
- package/CHANGELOG.md +46 -0
- package/README.md +268 -4
- package/package.json +17 -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,55 +0,0 @@
|
|
|
1
|
-
name: Claude Code
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
issue_comment:
|
|
5
|
-
types: [created]
|
|
6
|
-
pull_request_review_comment:
|
|
7
|
-
types: [created]
|
|
8
|
-
pull_request_review:
|
|
9
|
-
types: [submitted]
|
|
10
|
-
issues:
|
|
11
|
-
types: [opened, assigned]
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
claude:
|
|
15
|
-
if: |
|
|
16
|
-
(
|
|
17
|
-
github.event_name == 'issue_comment' &&
|
|
18
|
-
contains(github.event.comment.body || '', '@claude') &&
|
|
19
|
-
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
|
|
20
|
-
) ||
|
|
21
|
-
(
|
|
22
|
-
github.event_name == 'pull_request_review_comment' &&
|
|
23
|
-
contains(github.event.comment.body || '', '@claude') &&
|
|
24
|
-
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
|
|
25
|
-
) ||
|
|
26
|
-
(
|
|
27
|
-
github.event_name == 'pull_request_review' &&
|
|
28
|
-
contains(github.event.review.body || '', '@claude') &&
|
|
29
|
-
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association)
|
|
30
|
-
) ||
|
|
31
|
-
(
|
|
32
|
-
github.event_name == 'issues' &&
|
|
33
|
-
contains(github.event.issue.body || '', '@claude') &&
|
|
34
|
-
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)
|
|
35
|
-
)
|
|
36
|
-
runs-on: ubuntu-latest
|
|
37
|
-
permissions:
|
|
38
|
-
contents: read
|
|
39
|
-
pull-requests: write
|
|
40
|
-
issues: write
|
|
41
|
-
id-token: write
|
|
42
|
-
steps:
|
|
43
|
-
- name: Checkout repository
|
|
44
|
-
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
45
|
-
with:
|
|
46
|
-
fetch-depth: 1
|
|
47
|
-
|
|
48
|
-
- name: Run Claude Code
|
|
49
|
-
uses: anthropics/claude-code-action@673eb13aa77026be5c507eda12322c1a58b80f0b # v1
|
|
50
|
-
with:
|
|
51
|
-
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
52
|
-
plugin_marketplaces: |
|
|
53
|
-
https://github.com/anthropics/claude-code.git
|
|
54
|
-
plugins: |
|
|
55
|
-
code-review@claude-code-plugins
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,288 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const AvoEventSpecCache_1 = require("../eventSpec/AvoEventSpecCache");
|
|
4
|
-
function makeSpec(eventName) {
|
|
5
|
-
return {
|
|
6
|
-
eventSpec: {
|
|
7
|
-
eventName,
|
|
8
|
-
properties: [{ propertyName: "prop1", propertyType: "string" }],
|
|
9
|
-
},
|
|
10
|
-
metadata: {
|
|
11
|
-
schemaId: "schema1",
|
|
12
|
-
branchId: "branch1",
|
|
13
|
-
latestActionId: "action1",
|
|
14
|
-
sourceId: "source1",
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
describe("AvoEventSpecCache", () => {
|
|
19
|
-
let cache;
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
cache = new AvoEventSpecCache_1.AvoEventSpecCache();
|
|
22
|
-
jest.useFakeTimers();
|
|
23
|
-
});
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
jest.useRealTimers();
|
|
26
|
-
});
|
|
27
|
-
test("returns undefined for cache miss", () => {
|
|
28
|
-
expect(cache.get("nonexistent")).toBeUndefined();
|
|
29
|
-
});
|
|
30
|
-
test("stores and retrieves a value", () => {
|
|
31
|
-
const spec = makeSpec("click");
|
|
32
|
-
cache.set("key1", spec);
|
|
33
|
-
expect(cache.get("key1")).toEqual(spec);
|
|
34
|
-
});
|
|
35
|
-
test("stores and retrieves null responses", () => {
|
|
36
|
-
const nullSpec = {
|
|
37
|
-
eventSpec: null,
|
|
38
|
-
metadata: {
|
|
39
|
-
schemaId: "s1",
|
|
40
|
-
branchId: "b1",
|
|
41
|
-
latestActionId: "a1",
|
|
42
|
-
sourceId: "src1",
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
cache.set("null-key", nullSpec);
|
|
46
|
-
expect(cache.get("null-key")).toEqual(nullSpec);
|
|
47
|
-
});
|
|
48
|
-
test("TTL eviction: entries older than 60s removed on next sweep", () => {
|
|
49
|
-
cache.set("old-key", makeSpec("old"));
|
|
50
|
-
// Advance time by 61 seconds
|
|
51
|
-
jest.advanceTimersByTime(61000);
|
|
52
|
-
// Trigger sweep by performing enough operations
|
|
53
|
-
for (let i = 0; i < 50; i++) {
|
|
54
|
-
cache.set(`sweep-${i}`, makeSpec(`sweep-${i}`));
|
|
55
|
-
}
|
|
56
|
-
expect(cache.get("old-key")).toBeUndefined();
|
|
57
|
-
});
|
|
58
|
-
test("per-entry eviction: entries retrieved 50+ times evicted on next get", () => {
|
|
59
|
-
cache.set("hot-key", makeSpec("hot"));
|
|
60
|
-
// Access 49 times - should still be present
|
|
61
|
-
for (let i = 0; i < 49; i++) {
|
|
62
|
-
expect(cache.get("hot-key")).toBeDefined();
|
|
63
|
-
}
|
|
64
|
-
// The 50th access should trigger eviction
|
|
65
|
-
expect(cache.get("hot-key")).toBeUndefined();
|
|
66
|
-
});
|
|
67
|
-
test("LRU eviction when size > 50", () => {
|
|
68
|
-
// Fill cache with 50 entries. Each set() creates an entry with
|
|
69
|
-
// lastAccessed = Date.now(). We advance the fake clock by 1ms between
|
|
70
|
-
// sets so every entry has a distinct timestamp.
|
|
71
|
-
// The 50th set triggers evictLRU(), which removes the entry with the
|
|
72
|
-
// oldest lastAccessed — that is entry-0 (set at t=0).
|
|
73
|
-
for (let i = 0; i < 50; i++) {
|
|
74
|
-
cache.set(`entry-${i}`, makeSpec(`event-${i}`));
|
|
75
|
-
jest.advanceTimersByTime(1);
|
|
76
|
-
}
|
|
77
|
-
// entry-0 was the oldest when evictLRU() fired on the 50th set,
|
|
78
|
-
// so it should have been removed.
|
|
79
|
-
expect(cache.get("entry-0")).toBeUndefined();
|
|
80
|
-
// Touch entry-1 via get() — this updates its lastAccessed to "now",
|
|
81
|
-
// making it the most-recently-accessed entry and protecting it from
|
|
82
|
-
// the next eviction.
|
|
83
|
-
expect(cache.get("entry-1")).toBeDefined();
|
|
84
|
-
jest.advanceTimersByTime(1);
|
|
85
|
-
// Add two more entries to push the cache over 50 again.
|
|
86
|
-
// Each set that crosses the 50-entry threshold triggers evictLRU().
|
|
87
|
-
cache.set("entry-50", makeSpec("event-50"));
|
|
88
|
-
jest.advanceTimersByTime(1);
|
|
89
|
-
cache.set("entry-51", makeSpec("event-51"));
|
|
90
|
-
// entry-2 was never touched after its initial set(), so it had the
|
|
91
|
-
// oldest lastAccessed among remaining entries — evictLRU() removes it.
|
|
92
|
-
expect(cache.get("entry-2")).toBeUndefined();
|
|
93
|
-
// entry-1 survives because get() above refreshed its lastAccessed,
|
|
94
|
-
// so it was NOT the oldest entry when eviction fired.
|
|
95
|
-
expect(cache.get("entry-1")).toBeDefined();
|
|
96
|
-
// entry-51 was just inserted, so it is definitely still present.
|
|
97
|
-
expect(cache.get("entry-51")).toBeDefined();
|
|
98
|
-
});
|
|
99
|
-
test("global rotation evicts LRU entry every 50 operations", () => {
|
|
100
|
-
// Add entries with distinct timestamps
|
|
101
|
-
for (let i = 0; i < 49; i++) {
|
|
102
|
-
cache.set(`entry-${i}`, makeSpec(`event-${i}`));
|
|
103
|
-
jest.advanceTimersByTime(1);
|
|
104
|
-
}
|
|
105
|
-
// The 50th set triggers global rotation, evicting the LRU entry (entry-0)
|
|
106
|
-
cache.set("entry-49", makeSpec("event-49"));
|
|
107
|
-
expect(cache.get("entry-0")).toBeUndefined();
|
|
108
|
-
expect(cache.get("entry-49")).toBeDefined();
|
|
109
|
-
});
|
|
110
|
-
test("flush on empty cache is a no-op", () => {
|
|
111
|
-
// Just verify no crash
|
|
112
|
-
cache.flush();
|
|
113
|
-
expect(cache.get("anything")).toBeUndefined();
|
|
114
|
-
});
|
|
115
|
-
test("flush clears all entries", () => {
|
|
116
|
-
cache.set("a", makeSpec("a"));
|
|
117
|
-
cache.set("b", makeSpec("b"));
|
|
118
|
-
cache.flush();
|
|
119
|
-
expect(cache.get("a")).toBeUndefined();
|
|
120
|
-
expect(cache.get("b")).toBeUndefined();
|
|
121
|
-
});
|
|
122
|
-
test("key format uses null-byte delimiter", () => {
|
|
123
|
-
const key = AvoEventSpecCache_1.AvoEventSpecCache.makeKey("myApi", "stream1", "click");
|
|
124
|
-
expect(key).toBe("myApi\0stream1\0click");
|
|
125
|
-
});
|
|
126
|
-
describe("contains()", () => {
|
|
127
|
-
test("returns true for a valid cached entry", () => {
|
|
128
|
-
cache.set("k1", makeSpec("click"));
|
|
129
|
-
expect(cache.contains("k1")).toBe(true);
|
|
130
|
-
});
|
|
131
|
-
test("returns false for a missing key", () => {
|
|
132
|
-
expect(cache.contains("nonexistent")).toBe(false);
|
|
133
|
-
});
|
|
134
|
-
test("returns false after TTL expiry", () => {
|
|
135
|
-
cache.set("ttl-key", makeSpec("click"));
|
|
136
|
-
jest.advanceTimersByTime(61000);
|
|
137
|
-
expect(cache.contains("ttl-key")).toBe(false);
|
|
138
|
-
});
|
|
139
|
-
test("returns false after access count exhaustion", () => {
|
|
140
|
-
cache.set("hot-key", makeSpec("hot"));
|
|
141
|
-
// Access 49 times via get to increment eventCount to 49
|
|
142
|
-
for (let i = 0; i < 49; i++) {
|
|
143
|
-
cache.get("hot-key");
|
|
144
|
-
}
|
|
145
|
-
// eventCount is now 49; contains() checks >= 50, so still false at 49
|
|
146
|
-
// but the 50th get() returned undefined and deleted the entry
|
|
147
|
-
// Actually the 50th get increments to 50 and evicts, so after 49 gets
|
|
148
|
-
// eventCount is 49. The next get (50th) increments to 50 and evicts.
|
|
149
|
-
// contains() checks >= MAX_EVENT_COUNT without incrementing, so at 49 it's true.
|
|
150
|
-
// We need one more get to push it to 50.
|
|
151
|
-
expect(cache.get("hot-key")).toBeUndefined(); // 50th access evicts
|
|
152
|
-
expect(cache.contains("hot-key")).toBe(false);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
describe("upsert/overwrite", () => {
|
|
156
|
-
test("setting the same key twice overwrites the previous value", () => {
|
|
157
|
-
const spec1 = makeSpec("first");
|
|
158
|
-
const spec2 = makeSpec("second");
|
|
159
|
-
cache.set("dup-key", spec1);
|
|
160
|
-
expect(cache.get("dup-key")).toEqual(spec1);
|
|
161
|
-
cache.set("dup-key", spec2);
|
|
162
|
-
expect(cache.get("dup-key")).toEqual(spec2);
|
|
163
|
-
});
|
|
164
|
-
test("overwrite resets timestamp", () => {
|
|
165
|
-
cache.set("ts-key", makeSpec("event"));
|
|
166
|
-
// Advance time close to TTL
|
|
167
|
-
jest.advanceTimersByTime(55000);
|
|
168
|
-
// Overwrite — should reset the timestamp
|
|
169
|
-
cache.set("ts-key", makeSpec("event-v2"));
|
|
170
|
-
// Advance another 10s (total 65s since first set, but only 10s since overwrite)
|
|
171
|
-
jest.advanceTimersByTime(10000);
|
|
172
|
-
expect(cache.get("ts-key")).toBeDefined();
|
|
173
|
-
});
|
|
174
|
-
test("overwrite resets eventCount", () => {
|
|
175
|
-
cache.set("count-key", makeSpec("event"));
|
|
176
|
-
// Access 48 times to bring eventCount close to limit
|
|
177
|
-
for (let i = 0; i < 48; i++) {
|
|
178
|
-
cache.get("count-key");
|
|
179
|
-
}
|
|
180
|
-
// Overwrite — should reset eventCount to 0
|
|
181
|
-
cache.set("count-key", makeSpec("event-v2"));
|
|
182
|
-
// Should be able to access again without hitting the limit
|
|
183
|
-
expect(cache.get("count-key")).toBeDefined();
|
|
184
|
-
expect(cache.get("count-key")).toBeDefined();
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
describe("makeKey delimiter collision", () => {
|
|
188
|
-
test("makeKey with null-byte delimiter prevents colon collisions", () => {
|
|
189
|
-
const key1 = AvoEventSpecCache_1.AvoEventSpecCache.makeKey("a:b", "c", "d");
|
|
190
|
-
const key2 = AvoEventSpecCache_1.AvoEventSpecCache.makeKey("a", "b:c", "d");
|
|
191
|
-
expect(key1).not.toBe(key2);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|