node-avo-inspector 1.1.1 → 1.2.0

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 (93) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/.claude/settings.local.json +11 -0
  3. package/.claude/worktrees/release-1.1.1/LICENSE +21 -0
  4. package/.claude/worktrees/release-1.1.1/README.md +69 -0
  5. package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/index.js +30 -0
  6. package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/package.json +15 -0
  7. package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/yarn.lock +6 -0
  8. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/dist/index.js +34 -0
  9. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/index.ts +30 -0
  10. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/package.json +18 -0
  11. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/tsconfig.json +15 -0
  12. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/yarn.lock +11 -0
  13. package/.claude/worktrees/release-1.1.1/package-lock.json +4039 -0
  14. package/.claude/worktrees/release-1.1.1/package.json +48 -0
  15. package/.claude/worktrees/release-1.1.1/reason/AvoInspector.rei +11 -0
  16. package/.claude/worktrees/release-1.1.1/reason/NodeAvoInspector.re +27 -0
  17. package/.claude/worktrees/release-1.1.1/src/AvoDeduplicator.ts +146 -0
  18. package/.claude/worktrees/release-1.1.1/src/AvoEncryption.ts +90 -0
  19. package/.claude/worktrees/release-1.1.1/src/AvoGuid.ts +9 -0
  20. package/.claude/worktrees/release-1.1.1/src/AvoInspector.ts +383 -0
  21. package/.claude/worktrees/release-1.1.1/src/AvoInspectorEnv.ts +8 -0
  22. package/.claude/worktrees/release-1.1.1/src/AvoNetworkCallsHandler.ts +267 -0
  23. package/.claude/worktrees/release-1.1.1/src/AvoSchemaParser.ts +115 -0
  24. package/.claude/worktrees/release-1.1.1/src/AvoStreamId.ts +16 -0
  25. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEncryption.ts +515 -0
  26. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEventSpecCache.ts +242 -0
  27. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEventSpecFetcher.ts +241 -0
  28. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoInspector.ts +360 -0
  29. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoStreamId.ts +72 -0
  30. package/.claude/worktrees/release-1.1.1/src/__tests__/TestDeduplicator.ts +191 -0
  31. package/.claude/worktrees/release-1.1.1/src/__tests__/TestEventValidator.ts +231 -0
  32. package/.claude/worktrees/release-1.1.1/src/__tests__/TestNetworkCallsHandler.ts +179 -0
  33. package/.claude/worktrees/release-1.1.1/src/__tests__/TestNetworkCallsHandlerPublic.ts +208 -0
  34. package/.claude/worktrees/release-1.1.1/src/__tests__/TestParsing.ts +153 -0
  35. package/.claude/worktrees/release-1.1.1/src/__tests__/constants.ts +63 -0
  36. package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecCache.ts +109 -0
  37. package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecFetchTypes.ts +47 -0
  38. package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecFetcher.ts +106 -0
  39. package/.claude/worktrees/release-1.1.1/src/eventSpec/EventValidator.ts +74 -0
  40. package/.claude/worktrees/release-1.1.1/src/index.ts +2 -0
  41. package/.claude/worktrees/release-1.1.1/src/utils.ts +49 -0
  42. package/.claude/worktrees/release-1.1.1/tsconfig.json +13 -0
  43. package/.claude/worktrees/release-1.1.1/yarn.lock +2310 -0
  44. package/.github/workflows/claude.yml +55 -0
  45. package/dist/AvoDeduplicator.d.ts +4 -3
  46. package/dist/AvoDeduplicator.js +25 -24
  47. package/dist/AvoEncryption.d.ts +26 -0
  48. package/dist/AvoEncryption.js +77 -0
  49. package/dist/AvoInspector.d.ts +34 -3
  50. package/dist/AvoInspector.js +182 -36
  51. package/dist/AvoNetworkCallsHandler.d.ts +39 -13
  52. package/dist/AvoNetworkCallsHandler.js +127 -33
  53. package/dist/AvoStreamId.d.ts +5 -0
  54. package/dist/AvoStreamId.js +15 -0
  55. package/dist/__tests__/TestAvoEncryption.d.ts +1 -0
  56. package/dist/__tests__/TestAvoEncryption.js +288 -0
  57. package/dist/__tests__/TestAvoEventSpecCache.d.ts +1 -0
  58. package/dist/__tests__/TestAvoEventSpecCache.js +194 -0
  59. package/dist/__tests__/TestAvoEventSpecFetcher.d.ts +1 -0
  60. package/dist/__tests__/TestAvoEventSpecFetcher.js +219 -0
  61. package/dist/__tests__/TestAvoInspector.js +76 -2
  62. package/dist/__tests__/TestAvoStreamId.d.ts +1 -0
  63. package/dist/__tests__/TestAvoStreamId.js +55 -0
  64. package/dist/__tests__/TestEventValidator.d.ts +1 -0
  65. package/dist/__tests__/TestEventValidator.js +196 -0
  66. package/dist/__tests__/TestNetworkCallsHandler.js +37 -10
  67. package/dist/__tests__/TestNetworkCallsHandlerPublic.js +74 -41
  68. package/dist/eventSpec/AvoEventSpecCache.d.ts +19 -0
  69. package/dist/eventSpec/AvoEventSpecCache.js +100 -0
  70. package/dist/eventSpec/AvoEventSpecFetchTypes.d.ts +41 -0
  71. package/dist/eventSpec/AvoEventSpecFetchTypes.js +5 -0
  72. package/dist/eventSpec/AvoEventSpecFetcher.d.ts +25 -0
  73. package/dist/eventSpec/AvoEventSpecFetcher.js +159 -0
  74. package/dist/eventSpec/EventValidator.d.ts +15 -0
  75. package/dist/eventSpec/EventValidator.js +53 -0
  76. package/package.json +4 -2
  77. package/src/AvoDeduplicator.ts +31 -27
  78. package/src/AvoEncryption.ts +90 -0
  79. package/src/AvoInspector.ts +275 -49
  80. package/src/AvoNetworkCallsHandler.ts +205 -45
  81. package/src/AvoStreamId.ts +16 -0
  82. package/src/__tests__/TestAvoEncryption.ts +515 -0
  83. package/src/__tests__/TestAvoEventSpecCache.ts +242 -0
  84. package/src/__tests__/TestAvoEventSpecFetcher.ts +279 -0
  85. package/src/__tests__/TestAvoInspector.ts +114 -2
  86. package/src/__tests__/TestAvoStreamId.ts +72 -0
  87. package/src/__tests__/TestEventValidator.ts +231 -0
  88. package/src/__tests__/TestNetworkCallsHandler.ts +82 -15
  89. package/src/__tests__/TestNetworkCallsHandlerPublic.ts +110 -44
  90. package/src/eventSpec/AvoEventSpecCache.ts +119 -0
  91. package/src/eventSpec/AvoEventSpecFetchTypes.ts +47 -0
  92. package/src/eventSpec/AvoEventSpecFetcher.ts +186 -0
  93. package/src/eventSpec/EventValidator.ts +74 -0
@@ -0,0 +1,194 @@
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
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const AvoEventSpecFetcher_1 = require("../eventSpec/AvoEventSpecFetcher");
4
+ const https = require("https");
5
+ const events_1 = require("events");
6
+ jest.mock("https");
7
+ const mockedHttps = https;
8
+ class MockIncomingMessage extends events_1.EventEmitter {
9
+ constructor(statusCode) {
10
+ super();
11
+ this.statusCode = statusCode;
12
+ }
13
+ }
14
+ class MockClientRequest extends events_1.EventEmitter {
15
+ constructor() {
16
+ super(...arguments);
17
+ this.end = jest.fn();
18
+ this.destroy = jest.fn();
19
+ this.setTimeout = jest.fn();
20
+ }
21
+ }
22
+ /** Build a wire-format response body matching the /trackingPlan/eventSpec API */
23
+ function wireResponse(eventName, properties, metadata) {
24
+ const p = {};
25
+ for (const [name, constraint] of Object.entries(properties)) {
26
+ p[name] = { t: constraint.t, r: true };
27
+ }
28
+ return {
29
+ events: [{ b: metadata.branchId, id: "evt1", vids: [], p }],
30
+ metadata,
31
+ };
32
+ }
33
+ function setupMockRequest(responseStatusCode, responseBody) {
34
+ const mockReq = new MockClientRequest();
35
+ const mockRes = new MockIncomingMessage(responseStatusCode);
36
+ mockedHttps.request.mockImplementation((_options, callback) => {
37
+ callback(mockRes);
38
+ const data = JSON.stringify(responseBody);
39
+ mockRes.emit("data", Buffer.from(data));
40
+ mockRes.emit("end");
41
+ return mockReq;
42
+ });
43
+ return mockReq;
44
+ }
45
+ function setupMockRequestError() {
46
+ const mockReq = new MockClientRequest();
47
+ mockedHttps.request.mockImplementation((_options, _callback) => {
48
+ process.nextTick(() => mockReq.emit("error", new Error("network failure")));
49
+ return mockReq;
50
+ });
51
+ return mockReq;
52
+ }
53
+ describe("AvoEventSpecFetcher", () => {
54
+ let fetcher;
55
+ beforeEach(() => {
56
+ jest.clearAllMocks();
57
+ fetcher = new AvoEventSpecFetcher_1.AvoEventSpecFetcher("test-api-key");
58
+ });
59
+ afterEach(() => {
60
+ fetcher.destroy();
61
+ });
62
+ test("fetches event spec via HTTPS GET", (done) => {
63
+ const metadata = { schemaId: "s1", branchId: "b1", latestActionId: "a1", sourceId: "src1" };
64
+ const wire = wireResponse("click", { target: { t: "string" } }, metadata);
65
+ const expectedParsed = {
66
+ eventSpec: {
67
+ eventName: "click",
68
+ properties: [{ propertyName: "target", propertyType: "string" }],
69
+ },
70
+ metadata,
71
+ };
72
+ setupMockRequest(200, wire);
73
+ fetcher.fetch("click", "stream1", (result) => {
74
+ expect(result).toEqual(expectedParsed);
75
+ expect(mockedHttps.request).toHaveBeenCalledTimes(1);
76
+ const callArgs = mockedHttps.request.mock.calls[0][0];
77
+ expect(callArgs.method).toBe("GET");
78
+ expect(callArgs.hostname).toBe("api.avo.app");
79
+ done();
80
+ });
81
+ });
82
+ test("in-flight dedup: exactly ONE https.request for concurrent same-key events", (done) => {
83
+ const metadata = { schemaId: "s1", branchId: "b1", latestActionId: "a1", sourceId: "src1" };
84
+ const wire = wireResponse("purchase", {}, metadata);
85
+ const expectedParsed = {
86
+ eventSpec: { eventName: "purchase", properties: [] },
87
+ metadata,
88
+ };
89
+ // Delay the response so both calls are in-flight
90
+ const mockReq = new MockClientRequest();
91
+ const mockRes = new MockIncomingMessage(200);
92
+ mockedHttps.request.mockImplementation((_options, callback) => {
93
+ // Defer the response
94
+ setTimeout(() => {
95
+ callback(mockRes);
96
+ const data = JSON.stringify(wire);
97
+ mockRes.emit("data", Buffer.from(data));
98
+ mockRes.emit("end");
99
+ }, 10);
100
+ return mockReq;
101
+ });
102
+ let callCount = 0;
103
+ const checkDone = (result) => {
104
+ callCount++;
105
+ expect(result).toEqual(expectedParsed);
106
+ if (callCount === 2) {
107
+ // Only ONE actual https.request
108
+ expect(mockedHttps.request).toHaveBeenCalledTimes(1);
109
+ done();
110
+ }
111
+ };
112
+ fetcher.fetch("purchase", "stream1", checkDone);
113
+ fetcher.fetch("purchase", "stream1", checkDone);
114
+ });
115
+ test("in-flight failure: all queued callbacks resolved with null", (done) => {
116
+ setupMockRequestError();
117
+ let callCount = 0;
118
+ const checkDone = (result) => {
119
+ callCount++;
120
+ expect(result).toBeNull();
121
+ if (callCount === 2) {
122
+ expect(mockedHttps.request).toHaveBeenCalledTimes(1);
123
+ done();
124
+ }
125
+ };
126
+ fetcher.fetch("fail-event", "stream1", checkDone);
127
+ fetcher.fetch("fail-event", "stream1", checkDone);
128
+ });
129
+ test("non-200 status code resolves callback with null", (done) => {
130
+ setupMockRequest(500, { error: "Internal Server Error" });
131
+ fetcher.fetch("click", "stream1", (result) => {
132
+ expect(result).toBeNull();
133
+ expect(mockedHttps.request).toHaveBeenCalledTimes(1);
134
+ done();
135
+ });
136
+ });
137
+ test("malformed JSON body resolves callback with null", (done) => {
138
+ const mockReq = new MockClientRequest();
139
+ const mockRes = new MockIncomingMessage(200);
140
+ mockedHttps.request.mockImplementation((_options, callback) => {
141
+ callback(mockRes);
142
+ mockRes.emit("data", Buffer.from("not valid json {{{"));
143
+ mockRes.emit("end");
144
+ return mockReq;
145
+ });
146
+ fetcher.fetch("click", "stream1", (result) => {
147
+ expect(result).toBeNull();
148
+ expect(mockedHttps.request).toHaveBeenCalledTimes(1);
149
+ done();
150
+ });
151
+ });
152
+ test("timeout resolves callback with null", (done) => {
153
+ const mockReq = new MockClientRequest();
154
+ mockedHttps.request.mockImplementation((_options, _callback) => {
155
+ // Simulate timeout: never call the response callback,
156
+ // instead emit "timeout" on next tick
157
+ process.nextTick(() => mockReq.emit("timeout"));
158
+ return mockReq;
159
+ });
160
+ fetcher.fetch("click", "stream1", (result) => {
161
+ expect(result).toBeNull();
162
+ expect(mockReq.destroy).toHaveBeenCalled();
163
+ expect(mockedHttps.request).toHaveBeenCalledTimes(1);
164
+ done();
165
+ });
166
+ });
167
+ test("separate keys make separate requests", (done) => {
168
+ const metadata = { schemaId: "s1", branchId: "b1", latestActionId: "a1", sourceId: "src1" };
169
+ const wire = wireResponse("click", {}, metadata);
170
+ const expectedParsed = {
171
+ eventSpec: { eventName: "click", properties: [] },
172
+ metadata,
173
+ };
174
+ setupMockRequest(200, wire);
175
+ let callCount = 0;
176
+ const checkDone = () => {
177
+ callCount++;
178
+ if (callCount === 2) {
179
+ expect(mockedHttps.request).toHaveBeenCalledTimes(2);
180
+ done();
181
+ }
182
+ };
183
+ fetcher.fetch("click", "stream1", (result) => {
184
+ expect(result).toEqual(expectedParsed);
185
+ checkDone();
186
+ });
187
+ fetcher.fetch("purchase", "stream1", (result) => {
188
+ // Both resolve with the same mock but that's fine for this test
189
+ checkDone();
190
+ });
191
+ });
192
+ test("parseWireResponse transforms wire format correctly", () => {
193
+ const wire = {
194
+ events: [{
195
+ b: "main",
196
+ id: "evt1",
197
+ vids: [],
198
+ p: {
199
+ "Cli Action": { t: "string", r: true, v: { '["Status","Pull"]': ["evt1"] } },
200
+ "Client": { t: "string", r: true },
201
+ },
202
+ }],
203
+ metadata: { schemaId: "s1", branchId: "main", latestActionId: "a1", sourceId: "src1" },
204
+ };
205
+ const result = AvoEventSpecFetcher_1.AvoEventSpecFetcher.parseWireResponse(wire, "Cli Invoked");
206
+ expect(result.metadata).toEqual({ schemaId: "s1", branchId: "main", latestActionId: "a1", sourceId: "src1" });
207
+ expect(result.eventSpec).not.toBeNull();
208
+ expect(result.eventSpec.eventName).toBe("Cli Invoked");
209
+ expect(result.eventSpec.properties).toEqual([
210
+ { propertyName: "Cli Action", propertyType: "string" },
211
+ { propertyName: "Client", propertyType: "string" },
212
+ ]);
213
+ });
214
+ test("parseWireResponse returns null eventSpec for empty events array", () => {
215
+ const wire = { events: [], metadata: { schemaId: "s1", branchId: "b1", latestActionId: "a1", sourceId: "src1" } };
216
+ const result = AvoEventSpecFetcher_1.AvoEventSpecFetcher.parseWireResponse(wire, "test");
217
+ expect(result.eventSpec).toBeNull();
218
+ });
219
+ });
@@ -12,7 +12,7 @@ describe("Initialization", () => {
12
12
  let inspector = new AvoInspector_1.AvoInspector(constants_1.defaultOptions);
13
13
  inspector.enableLogging(true);
14
14
  expect(AvoNetworkCallsHandler_1.AvoNetworkCallsHandler).toHaveBeenCalledTimes(1);
15
- expect(AvoNetworkCallsHandler_1.AvoNetworkCallsHandler).toHaveBeenCalledWith(apiKey, env, "my-test-app", version, inspectorVersion);
15
+ expect(AvoNetworkCallsHandler_1.AvoNetworkCallsHandler).toHaveBeenCalledWith(apiKey, env, "my-test-app", version, inspectorVersion, undefined);
16
16
  });
17
17
  test("Api Key is set", () => {
18
18
  // Given
@@ -40,7 +40,7 @@ describe("Initialization", () => {
40
40
  });
41
41
  }).toThrow(constants_1.error.API_KEY);
42
42
  });
43
- test("Error is thrown when empty Api Key is used", () => {
43
+ test("Error is thrown when whitespace Api Key is used", () => {
44
44
  // Given
45
45
  const apiKey = " ";
46
46
  // Then
@@ -52,6 +52,15 @@ describe("Initialization", () => {
52
52
  });
53
53
  }).toThrow(constants_1.error.API_KEY);
54
54
  });
55
+ test("Error is thrown when empty string Api Key is used", () => {
56
+ expect(() => {
57
+ new AvoInspector_1.AvoInspector({
58
+ env: AvoInspectorEnv_1.AvoInspectorEnv.Prod,
59
+ version: "0",
60
+ apiKey: "",
61
+ });
62
+ }).toThrow(constants_1.error.API_KEY);
63
+ });
55
64
  test("Error is thrown when Api Key is set to null", () => {
56
65
  // Given
57
66
  const apiKey = null;
@@ -214,4 +223,69 @@ describe("Initialization", () => {
214
223
  });
215
224
  }).toThrow(constants_1.error.VERSION);
216
225
  });
226
+ test("publicEncryptionKey is forwarded to AvoNetworkCallsHandler when provided", () => {
227
+ const inspectorVersion = process.env.npm_package_version || "";
228
+ AvoNetworkCallsHandler_1.AvoNetworkCallsHandler.mockClear();
229
+ let inspector = new AvoInspector_1.AvoInspector({
230
+ apiKey: "api-key-xxx",
231
+ env: AvoInspectorEnv_1.AvoInspectorEnv.Dev,
232
+ version: "1",
233
+ publicEncryptionKey: "my-public-key-123",
234
+ });
235
+ expect(AvoNetworkCallsHandler_1.AvoNetworkCallsHandler).toHaveBeenCalledWith("api-key-xxx", AvoInspectorEnv_1.AvoInspectorEnv.Dev, "", "1", inspectorVersion, "my-public-key-123");
236
+ });
237
+ test("publicEncryptionKey is forwarded as undefined to AvoNetworkCallsHandler when not provided", () => {
238
+ const inspectorVersion = process.env.npm_package_version || "";
239
+ AvoNetworkCallsHandler_1.AvoNetworkCallsHandler.mockClear();
240
+ let inspector = new AvoInspector_1.AvoInspector({
241
+ apiKey: "api-key-xxx",
242
+ env: AvoInspectorEnv_1.AvoInspectorEnv.Dev,
243
+ version: "1",
244
+ });
245
+ expect(AvoNetworkCallsHandler_1.AvoNetworkCallsHandler).toHaveBeenCalledWith("api-key-xxx", AvoInspectorEnv_1.AvoInspectorEnv.Dev, "", "1", inspectorVersion, undefined);
246
+ });
247
+ test("warns when publicEncryptionKey is malformed in dev/staging", () => {
248
+ const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
249
+ new AvoInspector_1.AvoInspector({
250
+ apiKey: "api-key-xxx",
251
+ env: AvoInspectorEnv_1.AvoInspectorEnv.Dev,
252
+ version: "1",
253
+ publicEncryptionKey: "not-valid-hex",
254
+ });
255
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("publicEncryptionKey does not look like a valid"));
256
+ consoleWarnSpy.mockRestore();
257
+ });
258
+ test("does not warn when publicEncryptionKey is a valid 130-char uncompressed hex key in dev/staging", () => {
259
+ const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
260
+ const validKey = "04" + "ab".repeat(64); // 2 + 128 = 130 hex chars
261
+ new AvoInspector_1.AvoInspector({
262
+ apiKey: "api-key-xxx",
263
+ env: AvoInspectorEnv_1.AvoInspectorEnv.Dev,
264
+ version: "1",
265
+ publicEncryptionKey: validKey,
266
+ });
267
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
268
+ consoleWarnSpy.mockRestore();
269
+ });
270
+ test("does not warn when publicEncryptionKey is a valid 66-char compressed hex key in dev/staging", () => {
271
+ const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
272
+ const validKey = "02" + "4e".repeat(32); // 2 + 64 = 66 hex chars
273
+ new AvoInspector_1.AvoInspector({
274
+ apiKey: "api-key-xxx",
275
+ env: AvoInspectorEnv_1.AvoInspectorEnv.Dev,
276
+ version: "1",
277
+ publicEncryptionKey: validKey,
278
+ });
279
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
280
+ consoleWarnSpy.mockRestore();
281
+ });
282
+ test("constructor works without publicEncryptionKey (backwards compatible)", () => {
283
+ expect(() => {
284
+ new AvoInspector_1.AvoInspector({
285
+ apiKey: "api-key-xxx",
286
+ env: AvoInspectorEnv_1.AvoInspectorEnv.Dev,
287
+ version: "1",
288
+ });
289
+ }).not.toThrow();
290
+ });
217
291
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const AvoStreamId_1 = require("../AvoStreamId");
4
+ describe("AvoStreamId", () => {
5
+ let consoleWarnSpy;
6
+ beforeEach(() => {
7
+ consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
8
+ });
9
+ afterEach(() => {
10
+ jest.restoreAllMocks();
11
+ });
12
+ test("streamId getter returns empty string by default", () => {
13
+ const avoStreamId = new AvoStreamId_1.AvoStreamId();
14
+ expect(avoStreamId.streamId).toBe("");
15
+ });
16
+ test("streamId getter returns the value passed to constructor", () => {
17
+ const avoStreamId = new AvoStreamId_1.AvoStreamId("my-stream-id");
18
+ expect(avoStreamId.streamId).toBe("my-stream-id");
19
+ });
20
+ test("streamId containing ':' logs a warning", () => {
21
+ const avoStreamId = new AvoStreamId_1.AvoStreamId("invalid:stream:id");
22
+ const id = avoStreamId.streamId;
23
+ expect(consoleWarnSpy).toHaveBeenCalledWith("[Avo Inspector] Warning: streamId contains ':' which is not supported");
24
+ expect(id).toBe("invalid:stream:id");
25
+ });
26
+ test("streamId without ':' does not log a warning", () => {
27
+ const avoStreamId = new AvoStreamId_1.AvoStreamId("valid-stream-id");
28
+ const id = avoStreamId.streamId;
29
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
30
+ expect(id).toBe("valid-stream-id");
31
+ });
32
+ test("empty streamId does not log a warning", () => {
33
+ const avoStreamId = new AvoStreamId_1.AvoStreamId("");
34
+ const id = avoStreamId.streamId;
35
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
36
+ expect(id).toBe("");
37
+ });
38
+ test("explicit undefined arg returns empty string and does not warn", () => {
39
+ const avoStreamId = new AvoStreamId_1.AvoStreamId(undefined);
40
+ const id = avoStreamId.streamId;
41
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
42
+ expect(id).toBe("");
43
+ });
44
+ test("warning is only logged once per instance, not on every .streamId access", () => {
45
+ const avoStreamId = new AvoStreamId_1.AvoStreamId("bad:id");
46
+ // The warning is logged in the constructor, so it's already been called once
47
+ expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
48
+ // Access streamId multiple times
49
+ avoStreamId.streamId;
50
+ avoStreamId.streamId;
51
+ avoStreamId.streamId;
52
+ // Should still be only one warning total
53
+ expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
54
+ });
55
+ });
@@ -0,0 +1 @@
1
+ export {};