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,55 @@
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
@@ -7,18 +7,19 @@ export declare class AvoDeduplicator {
7
7
  };
8
8
  private msToConsiderOld;
9
9
  avoFunctionsEventsParams: {
10
- [eventName: string]: {
10
+ [key: string]: {
11
11
  [propName: string]: any;
12
12
  };
13
13
  };
14
14
  manualEventsParams: {
15
- [eventName: string]: {
15
+ [key: string]: {
16
16
  [propName: string]: any;
17
17
  };
18
18
  };
19
+ private static dedupKey;
19
20
  shouldRegisterEvent(eventName: string, params: {
20
21
  [propName: string]: any;
21
- }, fromAvoFunction: boolean): boolean;
22
+ }, fromAvoFunction: boolean, streamId?: string): boolean;
22
23
  private hasSameEventAs;
23
24
  private lookForEventIn;
24
25
  hasSeenEventParams(params: {
@@ -7,49 +7,50 @@ class AvoDeduplicator {
7
7
  this.avoFunctionsEvents = {};
8
8
  this.manualEvents = {};
9
9
  this.msToConsiderOld = 500;
10
+ // Keyed by streamId\0eventName to prevent cross-stream suppression on server
10
11
  this.avoFunctionsEventsParams = {};
11
12
  this.manualEventsParams = {};
12
13
  }
13
- shouldRegisterEvent(eventName, params, fromAvoFunction) {
14
+ static dedupKey(eventName, streamId) {
15
+ return streamId + "\0" + eventName;
16
+ }
17
+ shouldRegisterEvent(eventName, params, fromAvoFunction, streamId = "") {
14
18
  this.clearOldEvents();
19
+ const key = AvoDeduplicator.dedupKey(eventName, streamId);
15
20
  if (fromAvoFunction) {
16
- this.avoFunctionsEvents[Date.now()] = eventName;
17
- this.avoFunctionsEventsParams[eventName] = params;
21
+ this.avoFunctionsEvents[Date.now()] = key;
22
+ this.avoFunctionsEventsParams[key] = params;
18
23
  }
19
24
  else {
20
- this.manualEvents[Date.now()] = eventName;
21
- this.manualEventsParams[eventName] = params;
25
+ this.manualEvents[Date.now()] = key;
26
+ this.manualEventsParams[key] = params;
22
27
  }
23
28
  let checkInAvoFunctions = !fromAvoFunction;
24
- return !this.hasSameEventAs(eventName, params, checkInAvoFunctions);
29
+ return !this.hasSameEventAs(key, params, checkInAvoFunctions);
25
30
  }
26
- hasSameEventAs(eventName, params, checkInAvoFunctions) {
31
+ hasSameEventAs(key, params, checkInAvoFunctions) {
27
32
  let result = false;
28
33
  if (checkInAvoFunctions) {
29
- if (this.lookForEventIn(eventName, params, this.avoFunctionsEventsParams)) {
34
+ if (this.lookForEventIn(key, params, this.avoFunctionsEventsParams)) {
30
35
  result = true;
31
36
  }
32
37
  }
33
38
  else {
34
- if (this.lookForEventIn(eventName, params, this.manualEventsParams)) {
39
+ if (this.lookForEventIn(key, params, this.manualEventsParams)) {
35
40
  result = true;
36
41
  }
37
42
  }
38
43
  if (result) {
39
- delete this.avoFunctionsEventsParams[eventName];
40
- delete this.manualEventsParams[eventName];
44
+ delete this.avoFunctionsEventsParams[key];
45
+ delete this.manualEventsParams[key];
41
46
  }
42
47
  return result;
43
48
  }
44
- lookForEventIn(eventName, params, eventsStorage) {
45
- for (const otherEventName in eventsStorage) {
46
- if (eventsStorage.hasOwnProperty(otherEventName)) {
47
- if (otherEventName === eventName) {
48
- const otherParams = eventsStorage[eventName];
49
- if (otherParams && (0, utils_1.deepEquals)(params, otherParams)) {
50
- return true;
51
- }
52
- }
49
+ lookForEventIn(key, params, eventsStorage) {
50
+ if (eventsStorage.hasOwnProperty(key)) {
51
+ const otherParams = eventsStorage[key];
52
+ if (otherParams && (0, utils_1.deepEquals)(params, otherParams)) {
53
+ return true;
53
54
  }
54
55
  }
55
56
  return false;
@@ -85,9 +86,9 @@ class AvoDeduplicator {
85
86
  if (this.avoFunctionsEvents.hasOwnProperty(time)) {
86
87
  const timestamp = Number(time) || 0;
87
88
  if (now - timestamp > this.msToConsiderOld) {
88
- const eventName = this.avoFunctionsEvents[time];
89
+ const key = this.avoFunctionsEvents[time];
89
90
  delete this.avoFunctionsEvents[time];
90
- delete this.avoFunctionsEventsParams[eventName];
91
+ delete this.avoFunctionsEventsParams[key];
91
92
  }
92
93
  }
93
94
  }
@@ -95,9 +96,9 @@ class AvoDeduplicator {
95
96
  if (this.manualEvents.hasOwnProperty(time)) {
96
97
  const timestamp = Number(time) || 0;
97
98
  if (now - timestamp > this.msToConsiderOld) {
98
- const eventName = this.manualEvents[time];
99
+ const key = this.manualEvents[time];
99
100
  delete this.manualEvents[time];
100
- delete this.manualEventsParams[eventName];
101
+ delete this.manualEventsParams[key];
101
102
  }
102
103
  }
103
104
  }
@@ -0,0 +1,26 @@
1
+ export declare class AvoEncryption {
2
+ /**
3
+ * Determines whether encryption should be applied.
4
+ *
5
+ * Truth table:
6
+ * dev + key => true
7
+ * staging + key => true
8
+ * prod + key => false
9
+ * dev + null => false
10
+ * dev + "" => false
11
+ */
12
+ static shouldEncrypt(env: string, publicEncryptionKey: string | undefined | null): boolean;
13
+ /**
14
+ * Returns true if the propertyType represents a list type (e.g. "list(string)").
15
+ */
16
+ static isListType(propertyType: string): boolean;
17
+ /**
18
+ * Encrypts a string value using ECIES with prime256v1.
19
+ *
20
+ * Wire format: [0x00][65-byte ephemeral pubkey][16-byte IV][16-byte auth tag][ciphertext]
21
+ * Output is base64-encoded.
22
+ *
23
+ * Returns null on failure, logging a warning.
24
+ */
25
+ static encryptValue(value: string, recipientPubKeyHex: string): string | null;
26
+ }
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AvoEncryption = void 0;
4
+ const crypto = require("crypto");
5
+ class AvoEncryption {
6
+ /**
7
+ * Determines whether encryption should be applied.
8
+ *
9
+ * Truth table:
10
+ * dev + key => true
11
+ * staging + key => true
12
+ * prod + key => false
13
+ * dev + null => false
14
+ * dev + "" => false
15
+ */
16
+ static shouldEncrypt(env, publicEncryptionKey) {
17
+ if (!publicEncryptionKey || publicEncryptionKey.length === 0) {
18
+ return false;
19
+ }
20
+ if (env === "prod") {
21
+ return false;
22
+ }
23
+ return true;
24
+ }
25
+ /**
26
+ * Returns true if the propertyType represents a list type (e.g. "list(string)").
27
+ */
28
+ static isListType(propertyType) {
29
+ return propertyType.startsWith("list(");
30
+ }
31
+ /**
32
+ * Encrypts a string value using ECIES with prime256v1.
33
+ *
34
+ * Wire format: [0x00][65-byte ephemeral pubkey][16-byte IV][16-byte auth tag][ciphertext]
35
+ * Output is base64-encoded.
36
+ *
37
+ * Returns null on failure, logging a warning.
38
+ */
39
+ static encryptValue(value, recipientPubKeyHex) {
40
+ try {
41
+ const recipientPubKey = Buffer.from(recipientPubKeyHex, "hex");
42
+ // Generate ephemeral key pair
43
+ const ecdh = crypto.createECDH("prime256v1");
44
+ ecdh.generateKeys();
45
+ const ephemeralPubKey = ecdh.getPublicKey(); // 65 bytes, uncompressed
46
+ // Compute shared secret (raw 32-byte X-coordinate)
47
+ const sharedSecret = ecdh.computeSecret(recipientPubKey);
48
+ // Derive AES key via SHA-256
49
+ const aesKey = crypto.createHash("sha256").update(sharedSecret).digest();
50
+ // Generate random 16-byte IV
51
+ const iv = crypto.randomBytes(16);
52
+ // Encrypt with AES-256-GCM
53
+ const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv, {
54
+ authTagLength: 16,
55
+ });
56
+ const encrypted = Buffer.concat([
57
+ cipher.update(value, "utf8"),
58
+ cipher.final(),
59
+ ]);
60
+ const authTag = cipher.getAuthTag(); // 16 bytes
61
+ // Assemble wire format: [version][ephemeralPubKey][IV][authTag][ciphertext]
62
+ const wire = Buffer.concat([
63
+ Buffer.from([0x00]), // version byte
64
+ ephemeralPubKey, // 65 bytes
65
+ iv, // 16 bytes
66
+ authTag, // 16 bytes
67
+ encrypted, // variable length
68
+ ]);
69
+ return wire.toString("base64");
70
+ }
71
+ catch (e) {
72
+ console.warn(`[Avo Inspector] Warning: encryption failed for value, omitting property. ${e}`);
73
+ return null;
74
+ }
75
+ }
76
+ }
77
+ exports.AvoEncryption = AvoEncryption;
@@ -7,6 +7,14 @@ export declare class AvoInspector {
7
7
  avoDeduplicator: AvoDeduplicator;
8
8
  apiKey: string;
9
9
  version: string;
10
+ private publicEncryptionKey?;
11
+ private eventSpecFetcher;
12
+ private eventSpecCache;
13
+ private eventValidator;
14
+ private generatedAnonymousId;
15
+ private pendingCount;
16
+ private keepAliveTimer;
17
+ private trackPending;
10
18
  private static _shouldLog;
11
19
  static get shouldLog(): boolean;
12
20
  static set shouldLog(enable: boolean);
@@ -15,16 +23,33 @@ export declare class AvoInspector {
15
23
  env: AvoInspectorEnvValueType;
16
24
  version: string;
17
25
  appName?: string;
26
+ publicEncryptionKey?: string;
18
27
  });
19
28
  trackSchemaFromEvent(eventName: string, eventProperties: {
20
29
  [propName: string]: any;
21
- }): Promise<Array<{
30
+ }, streamId?: string): Promise<Array<{
22
31
  propertyName: string;
23
32
  propertyType: string;
24
33
  children?: any;
25
34
  }>>;
26
- private _avoFunctionTrackSchemaFromEvent;
27
- private trackSchemaInternal;
35
+ _avoFunctionTrackSchemaFromEvent(eventName: string, eventProperties: {
36
+ [propName: string]: any;
37
+ }, eventId: string, eventHash: string, streamId?: string): Promise<Array<{
38
+ propertyName: string;
39
+ propertyType: string;
40
+ children?: any;
41
+ }>>;
42
+ /**
43
+ * Try to fetch event spec and validate, then send a single event call.
44
+ * If validation succeeds, sends a validated event (with results merged in).
45
+ * If unavailable, sends a plain event.
46
+ *
47
+ * Uses trackPending() to keep the Node process alive until the work completes,
48
+ * so short-lived processes (CLIs) don't exit before the send finishes — even
49
+ * when the caller doesn't await the returned promise.
50
+ */
51
+ private sendEventWithOptionalValidation;
52
+ private doSendEventWithOptionalValidation;
28
53
  enableLogging(enable: boolean): void;
29
54
  extractSchema(eventProperties: {
30
55
  [propName: string]: any;
@@ -33,4 +58,10 @@ export declare class AvoInspector {
33
58
  propertyType: string;
34
59
  children?: any;
35
60
  }>;
61
+ /**
62
+ * Fetch event spec and validate. Returns validation results + metadata if
63
+ * validation succeeds, or null if unavailable (prod, no spec, fetch error).
64
+ */
65
+ private fetchAndValidate;
66
+ destroy(): void;
36
67
  }
@@ -14,10 +14,27 @@ const AvoInspectorEnv_1 = require("./AvoInspectorEnv");
14
14
  const AvoSchemaParser_1 = require("./AvoSchemaParser");
15
15
  const AvoNetworkCallsHandler_1 = require("./AvoNetworkCallsHandler");
16
16
  const AvoDeduplicator_1 = require("./AvoDeduplicator");
17
+ const AvoStreamId_1 = require("./AvoStreamId");
18
+ const AvoEventSpecFetcher_1 = require("./eventSpec/AvoEventSpecFetcher");
19
+ const AvoEventSpecCache_1 = require("./eventSpec/AvoEventSpecCache");
20
+ const EventValidator_1 = require("./eventSpec/EventValidator");
17
21
  const utils_1 = require("./utils");
18
- const AvoGuid_1 = require("./AvoGuid");
19
22
  const libVersion = require("../package.json").version;
20
23
  class AvoInspector {
24
+ trackPending(promise) {
25
+ this.pendingCount++;
26
+ if (this.keepAliveTimer === null) {
27
+ this.keepAliveTimer = setInterval(() => { }, 60000);
28
+ }
29
+ const done = () => {
30
+ this.pendingCount--;
31
+ if (this.pendingCount === 0 && this.keepAliveTimer !== null) {
32
+ clearInterval(this.keepAliveTimer);
33
+ this.keepAliveTimer = null;
34
+ }
35
+ };
36
+ promise.then(done, done);
37
+ }
21
38
  static get shouldLog() {
22
39
  return this._shouldLog;
23
40
  }
@@ -25,6 +42,15 @@ class AvoInspector {
25
42
  this._shouldLog = enable;
26
43
  }
27
44
  constructor(options) {
45
+ this.eventSpecFetcher = null;
46
+ this.eventSpecCache = null;
47
+ this.eventValidator = null;
48
+ this.generatedAnonymousId = "";
49
+ // Keep the Node process alive while there are pending inspector operations.
50
+ // Without this, short-lived processes (e.g. CLIs) can exit before async
51
+ // sends complete, since the caller typically doesn't await the inspector promise.
52
+ this.pendingCount = 0;
53
+ this.keepAliveTimer = null;
28
54
  // the constructor does aggressive null/undefined checking because same code paths will be accessible from JS
29
55
  if ((0, utils_1.isValueEmpty)(options.env)) {
30
56
  this.environment = AvoInspectorEnv_1.AvoInspectorEnv.Dev;
@@ -49,32 +75,51 @@ class AvoInspector {
49
75
  else {
50
76
  this.version = options.version;
51
77
  }
78
+ this.publicEncryptionKey = options.publicEncryptionKey;
79
+ if (this.publicEncryptionKey &&
80
+ this.environment !== AvoInspectorEnv_1.AvoInspectorEnv.Prod) {
81
+ const hexPattern = /^[0-9a-fA-F]+$/;
82
+ const len = this.publicEncryptionKey.length;
83
+ // Accept both compressed (66 hex chars, prefix 02/03) and uncompressed (130 hex chars, prefix 04) P-256 keys
84
+ const isValidLength = len === 66 || len === 130;
85
+ if (!hexPattern.test(this.publicEncryptionKey) || !isValidLength) {
86
+ console.warn("[Avo Inspector] Warning: publicEncryptionKey does not look like a valid P-256 public key (expected 66 or 130 hex characters). Encryption may fail.");
87
+ }
88
+ }
52
89
  if (this.environment === AvoInspectorEnv_1.AvoInspectorEnv.Dev) {
53
90
  AvoInspector._shouldLog = true;
54
91
  }
55
92
  else {
56
93
  AvoInspector._shouldLog = false;
57
94
  }
58
- this.avoNetworkCallsHandler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler(this.apiKey, this.environment.toString(), options.appName || "", this.version, libVersion);
95
+ this.avoNetworkCallsHandler = new AvoNetworkCallsHandler_1.AvoNetworkCallsHandler(this.apiKey, this.environment.toString(), options.appName || "", this.version, libVersion, this.publicEncryptionKey);
59
96
  this.avoDeduplicator = new AvoDeduplicator_1.AvoDeduplicator();
97
+ // Initialize event spec validation for dev/staging only
98
+ if (this.environment !== AvoInspectorEnv_1.AvoInspectorEnv.Prod) {
99
+ this.eventSpecFetcher = new AvoEventSpecFetcher_1.AvoEventSpecFetcher(this.apiKey);
100
+ this.eventSpecCache = new AvoEventSpecCache_1.AvoEventSpecCache();
101
+ this.eventValidator = new EventValidator_1.EventValidator();
102
+ }
60
103
  }
61
- trackSchemaFromEvent(eventName, eventProperties) {
104
+ trackSchemaFromEvent(eventName, eventProperties, streamId) {
62
105
  try {
63
- if (this.avoDeduplicator.shouldRegisterEvent(eventName, eventProperties, false)) {
106
+ const avoStreamId = new AvoStreamId_1.AvoStreamId(streamId);
107
+ const anonymousId = avoStreamId.streamId || this.generatedAnonymousId;
108
+ if (this.avoDeduplicator.shouldRegisterEvent(eventName, eventProperties, false, anonymousId)) {
64
109
  if (AvoInspector.shouldLog) {
65
- console.log("Avo Inspector: supplied event " +
110
+ console.log("Avo Inspector: Supplied event " +
66
111
  eventName +
67
- " with params " +
112
+ " with params \n" +
68
113
  JSON.stringify(eventProperties));
69
114
  }
70
115
  let eventSchema = this.extractSchema(eventProperties, false);
71
- return this.trackSchemaInternal(eventName, eventSchema, null, null).then(() => {
116
+ return this.sendEventWithOptionalValidation(eventName, eventSchema, null, null, anonymousId, eventProperties).then(() => {
72
117
  return eventSchema;
73
118
  });
74
119
  }
75
120
  else {
76
121
  if (AvoInspector.shouldLog) {
77
- console.log("Avo Inspector: Deduplicated event: " + eventName);
122
+ console.log("Avo Inspector: Deduplicated event " + eventName);
78
123
  }
79
124
  return Promise.resolve([]);
80
125
  }
@@ -84,23 +129,24 @@ class AvoInspector {
84
129
  return Promise.reject("Avo Inspector: something went wrong. Please report to support@avo.app.");
85
130
  }
86
131
  }
87
- _avoFunctionTrackSchemaFromEvent(eventName, eventProperties, eventId, eventHash) {
132
+ _avoFunctionTrackSchemaFromEvent(eventName, eventProperties, eventId, eventHash, streamId) {
88
133
  try {
89
- if (this.avoDeduplicator.shouldRegisterEvent(eventName, eventProperties, true)) {
134
+ const anonymousId = (streamId && streamId.length > 0) ? streamId : this.generatedAnonymousId;
135
+ if (this.avoDeduplicator.shouldRegisterEvent(eventName, eventProperties, true, anonymousId)) {
90
136
  if (AvoInspector.shouldLog) {
91
- console.log("Avo Inspector: supplied event " +
137
+ console.log("Avo Inspector: Supplied event " +
92
138
  eventName +
93
- " with params " +
139
+ " with params \n" +
94
140
  JSON.stringify(eventProperties));
95
141
  }
96
142
  let eventSchema = this.extractSchema(eventProperties, false);
97
- return this.trackSchemaInternal(eventName, eventSchema, eventId, eventHash).then(() => {
143
+ return this.sendEventWithOptionalValidation(eventName, eventSchema, eventId, eventHash, anonymousId, eventProperties).then(() => {
98
144
  return eventSchema;
99
145
  });
100
146
  }
101
147
  else {
102
148
  if (AvoInspector.shouldLog) {
103
- console.log("Avo Inspector: Deduplicated event: " + eventName);
149
+ console.log("Avo Inspector: Deduplicated event " + eventName);
104
150
  }
105
151
  return Promise.resolve([]);
106
152
  }
@@ -110,34 +156,43 @@ class AvoInspector {
110
156
  return Promise.reject("Avo Inspector: something went wrong. Please report to support@avo.app.");
111
157
  }
112
158
  }
113
- trackSchemaInternal(eventName, eventSchema, eventId, eventHash) {
159
+ /**
160
+ * Try to fetch event spec and validate, then send a single event call.
161
+ * If validation succeeds, sends a validated event (with results merged in).
162
+ * If unavailable, sends a plain event.
163
+ *
164
+ * Uses trackPending() to keep the Node process alive until the work completes,
165
+ * so short-lived processes (CLIs) don't exit before the send finishes — even
166
+ * when the caller doesn't await the returned promise.
167
+ */
168
+ sendEventWithOptionalValidation(eventName, eventSchema, eventId, eventHash, anonymousId, rawEventProperties) {
169
+ const work = this.doSendEventWithOptionalValidation(eventName, eventSchema, eventId, eventHash, anonymousId, rawEventProperties);
170
+ this.trackPending(work);
171
+ return work;
172
+ }
173
+ doSendEventWithOptionalValidation(eventName, eventSchema, eventId, eventHash, anonymousId, rawEventProperties) {
114
174
  return __awaiter(this, void 0, void 0, function* () {
175
+ const validationResult = yield this.fetchAndValidate(eventName, eventSchema, anonymousId, rawEventProperties, eventId);
115
176
  try {
116
- const seesionId = AvoGuid_1.AvoGuid.newGuid();
117
- yield this.avoNetworkCallsHandler
118
- .callInspectorWithBatchBody([
119
- this.avoNetworkCallsHandler.bodyForSessionStartedCall(seesionId),
120
- ]);
121
- if (AvoInspector.shouldLog) {
122
- console.log("Avo Inspector: session started sent successfully.");
123
- }
124
- try {
125
- yield this.avoNetworkCallsHandler.callInspectorWithBatchBody([
126
- this.avoNetworkCallsHandler.bodyForEventSchemaCall(seesionId, eventName, eventSchema, eventId, eventHash)
127
- ]);
177
+ let body;
178
+ if (validationResult) {
128
179
  if (AvoInspector.shouldLog) {
129
- console.log("Avo Inspector: schema sent successfully.");
180
+ console.log("Avo Inspector: Sending validated event " + eventName);
130
181
  }
182
+ const eventProps = this.avoNetworkCallsHandler.buildEventProperties(eventSchema, rawEventProperties);
183
+ body = this.avoNetworkCallsHandler.bodyForValidatedEventSchemaCall(anonymousId, eventName, eventProps, eventId, eventHash, validationResult.metadata, validationResult.propertyResults);
131
184
  }
132
- catch (err) {
133
- if (AvoInspector.shouldLog) {
134
- console.log("Avo Inspector: schema sending failed: " + err + ".");
135
- }
185
+ else {
186
+ body = this.avoNetworkCallsHandler.bodyForEventSchemaCall(anonymousId, eventName, eventSchema, eventId, eventHash, rawEventProperties);
187
+ }
188
+ yield this.avoNetworkCallsHandler.callInspectorWithBatchBody([body]);
189
+ if (AvoInspector.shouldLog) {
190
+ const schemaString = eventSchema.map(p => '\t"' + p.propertyName + '": "' + p.propertyType + '"').join(";\n");
191
+ console.log("Avo Inspector: Saved event " + eventName + " with schema {\n" + schemaString + "\n}");
136
192
  }
137
193
  }
138
- catch (e) {
139
- console.error("Avo Inspector: something went wrong. Please report to support@avo.app.", e);
140
- return Promise.reject();
194
+ catch (err) {
195
+ console.error("Avo Inspector: schema sending failed: " + err + ".");
141
196
  }
142
197
  });
143
198
  }
@@ -157,13 +212,104 @@ class AvoInspector {
157
212
  console.log("Avo Inspector: extracting schema from " +
158
213
  JSON.stringify(eventProperties));
159
214
  }
160
- return AvoSchemaParser_1.AvoSchemaParser.extractSchema(eventProperties);
215
+ const schema = AvoSchemaParser_1.AvoSchemaParser.extractSchema(eventProperties);
216
+ if (AvoInspector.shouldLog) {
217
+ const schemaString = schema.map(p => '\t"' + p.propertyName + '": "' + p.propertyType + '"').join(";\n");
218
+ console.log("Avo Inspector: Parsed schema {\n" + schemaString + "\n}");
219
+ }
220
+ return schema;
161
221
  }
162
222
  catch (e) {
163
223
  console.error("Avo Inspector: something went wrong. Please report to support@avo.app.", e);
164
224
  return [];
165
225
  }
166
226
  }
227
+ /**
228
+ * Fetch event spec and validate. Returns validation results + metadata if
229
+ * validation succeeds, or null if unavailable (prod, no spec, fetch error).
230
+ */
231
+ fetchAndValidate(eventName, eventSchema, anonymousId, rawEventProperties, eventId) {
232
+ return __awaiter(this, void 0, void 0, function* () {
233
+ if (!this.eventSpecFetcher || !this.eventSpecCache || !this.eventValidator) {
234
+ if (AvoInspector.shouldLog) {
235
+ console.log("Avo Inspector: Skipping event spec validation for event: " + eventName
236
+ + " (fetcher=" + (this.eventSpecFetcher != null)
237
+ + ", cache=" + (this.eventSpecCache != null)
238
+ + ", env=" + this.environment + ")");
239
+ }
240
+ return null;
241
+ }
242
+ const cacheKey = AvoEventSpecCache_1.AvoEventSpecCache.makeKey(this.apiKey, anonymousId, eventName);
243
+ const cache = this.eventSpecCache;
244
+ const validator = this.eventValidator;
245
+ const doValidate = (specResponse) => {
246
+ if (specResponse.eventSpec === null) {
247
+ if (AvoInspector.shouldLog) {
248
+ console.log("Avo Inspector: Event spec fetch returned null for event: " + eventName + ". Sending without validation.");
249
+ }
250
+ return null;
251
+ }
252
+ const eventProperties = eventSchema.map((prop) => (Object.assign({ propertyName: prop.propertyName, propertyType: prop.propertyType }, (rawEventProperties && rawEventProperties[prop.propertyName] !== undefined
253
+ ? { propertyValue: String(rawEventProperties[prop.propertyName]) }
254
+ : {}))));
255
+ if (AvoInspector.shouldLog) {
256
+ console.log("Avo Inspector: Validating event: " + eventName
257
+ + " with " + eventProperties.length + " properties"
258
+ + " against " + specResponse.eventSpec.properties.length + " spec properties");
259
+ }
260
+ const validationId = eventId || eventName;
261
+ const results = validator.validate(specResponse.eventSpec, eventProperties, validationId);
262
+ if (AvoInspector.shouldLog) {
263
+ console.log("Avo Inspector: Validation complete for event: " + eventName
264
+ + " with " + results.length + " property results");
265
+ }
266
+ return { metadata: specResponse.metadata, propertyResults: results };
267
+ };
268
+ // Check cache first
269
+ const cached = cache.get(cacheKey);
270
+ if (cached !== undefined) {
271
+ if (AvoInspector.shouldLog) {
272
+ console.log("Avo Inspector: Event spec cache hit for event: " + eventName);
273
+ }
274
+ return doValidate(cached);
275
+ }
276
+ // Cache miss — fetch spec (process stays alive via trackPending)
277
+ if (AvoInspector.shouldLog) {
278
+ console.log("Avo Inspector: Event spec cache miss for event: " + eventName + ". Fetching before sending.");
279
+ }
280
+ const fetcher = this.eventSpecFetcher;
281
+ return new Promise((resolve) => {
282
+ fetcher.fetch(eventName, anonymousId, (result) => {
283
+ if (result !== null) {
284
+ cache.set(cacheKey, result);
285
+ resolve(doValidate(result));
286
+ }
287
+ else {
288
+ if (AvoInspector.shouldLog) {
289
+ console.log("Avo Inspector: Event spec fetch returned null for event: " + eventName + ". Cached empty response. Sending without validation.");
290
+ }
291
+ resolve(null);
292
+ }
293
+ });
294
+ });
295
+ });
296
+ }
297
+ destroy() {
298
+ if (this.eventSpecFetcher) {
299
+ this.eventSpecFetcher.destroy();
300
+ this.eventSpecFetcher = null;
301
+ }
302
+ if (this.eventSpecCache) {
303
+ this.eventSpecCache.flush();
304
+ this.eventSpecCache = null;
305
+ }
306
+ this.eventValidator = null;
307
+ if (this.keepAliveTimer !== null) {
308
+ clearInterval(this.keepAliveTimer);
309
+ this.keepAliveTimer = null;
310
+ }
311
+ this.pendingCount = 0;
312
+ }
167
313
  }
168
314
  exports.AvoInspector = AvoInspector;
169
315
  AvoInspector._shouldLog = false;