avo-inspector 3.1.0-alpha.2 → 3.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.
- package/README.md +6 -13
- package/dist/AvoBatcher.js +52 -44
- package/dist/AvoDeduplicator.js +46 -45
- package/dist/AvoEncryption.js +147 -253
- package/dist/AvoInspector.js +295 -334
- package/dist/AvoNetworkCallsHandler.d.ts +6 -0
- package/dist/AvoNetworkCallsHandler.js +180 -136
- package/dist/AvoSchemaParser.js +34 -71
- package/dist/AvoStreamId.js +67 -129
- package/dist/__mocks__/xhr.js +14 -0
- package/dist/__mocks__/xhrError.js +14 -0
- package/dist/__tests__/AvoEncryption.d.ts +1 -0
- package/dist/__tests__/AvoEncryption.js +272 -0
- package/dist/__tests__/AvoEncryptionIntegration.d.ts +1 -0
- package/dist/__tests__/AvoEncryptionIntegration.js +191 -0
- package/dist/__tests__/AvoEventSpecFetcher.d.ts +1 -0
- package/dist/__tests__/AvoEventSpecFetcher.js +377 -0
- package/dist/__tests__/AvoInspector.d.ts +1 -0
- package/dist/__tests__/AvoInspector.js +335 -0
- package/dist/__tests__/Batcher.d.ts +1 -0
- package/dist/__tests__/Batcher.js +214 -0
- package/dist/__tests__/BatcherPublic.d.ts +1 -0
- package/dist/__tests__/BatcherPublic.js +139 -0
- package/dist/__tests__/Deduplicator.d.ts +1 -0
- package/dist/__tests__/Deduplicator.js +190 -0
- package/dist/__tests__/EventSpecCache.d.ts +1 -0
- package/dist/__tests__/EventSpecCache.js +143 -0
- package/dist/__tests__/EventValidator.d.ts +1 -0
- package/dist/__tests__/EventValidator.js +543 -0
- package/dist/__tests__/InstallationId.d.ts +1 -0
- package/dist/__tests__/InstallationId.js +60 -0
- package/dist/__tests__/NetworkCallsHandler.d.ts +1 -0
- package/dist/__tests__/NetworkCallsHandler.js +222 -0
- package/dist/__tests__/NetworkCallsHandlerGzip_test.d.ts +1 -0
- package/dist/__tests__/NetworkCallsHandlerModelA.d.ts +1 -0
- package/dist/__tests__/NetworkCallsHandlerModelA.js +206 -0
- package/dist/__tests__/Parsing.d.ts +1 -0
- package/dist/__tests__/Parsing.js +115 -0
- package/dist/__tests__/Sessions.d.ts +1 -0
- package/dist/__tests__/Sessions.js +10 -0
- package/dist/__tests__/Storage.d.ts +1 -0
- package/dist/__tests__/Storage.js +173 -0
- package/dist/__tests__/StreamId.d.ts +1 -0
- package/dist/__tests__/StreamId.js +153 -0
- package/dist/__tests__/constants.js +48 -0
- package/dist/eventSpec/AvoEventSpecCache.js +94 -82
- package/dist/eventSpec/AvoEventSpecFetcher.js +200 -117
- package/dist/eventSpec/EventValidator.js +460 -304
- package/dist/index.js +1 -1
- package/dist/lite/AvoNetworkCallsHandlerLite.d.ts +6 -0
- package/dist/lite/AvoNetworkCallsHandlerLite.js +110 -1
- package/package.json +2 -2
package/dist/AvoStreamId.js
CHANGED
|
@@ -1,144 +1,82 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AvoStreamId = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
* The stream ID is generated and stored persistently, but resets after:
|
|
10
|
-
* - 4 hours of total age AND
|
|
11
|
-
* - 2 hours of idle time (no activity)
|
|
12
|
-
*
|
|
13
|
-
* This ensures the ID is truly anonymous by not persisting indefinitely,
|
|
14
|
-
* while still maintaining session continuity during active usage.
|
|
15
|
-
*/
|
|
16
|
-
class AvoStreamId {
|
|
17
|
-
/**
|
|
18
|
-
* Get the stream ID. If it doesn't exist or has expired, generates a new one.
|
|
19
|
-
* Returns "unknown" if storage is not initialized.
|
|
20
|
-
*/
|
|
21
|
-
static get streamId() {
|
|
22
|
-
// Return cached value if available and not expired
|
|
23
|
-
if (AvoStreamId._streamId !== null && !AvoStreamId.shouldReset()) {
|
|
24
|
-
AvoStreamId.updateLastActivity();
|
|
25
|
-
return AvoStreamId._streamId;
|
|
26
|
-
}
|
|
27
|
-
if (!AvoInspector_1.AvoInspector.avoStorage.isInitialized()) {
|
|
28
|
-
return "unknown";
|
|
29
|
-
}
|
|
30
|
-
// Load from storage
|
|
31
|
-
AvoStreamId.loadFromStorage();
|
|
32
|
-
// Check if we should reset
|
|
33
|
-
if (AvoStreamId._streamId !== null && AvoStreamId.shouldReset()) {
|
|
34
|
-
return AvoStreamId.generateAndStoreNew();
|
|
35
|
-
}
|
|
36
|
-
// If no ID exists, generate new one
|
|
37
|
-
if (AvoStreamId._streamId === null) {
|
|
38
|
-
return AvoStreamId.generateAndStoreNew();
|
|
39
|
-
}
|
|
40
|
-
// Update last activity and return existing
|
|
41
|
-
AvoStreamId.updateLastActivity();
|
|
42
|
-
return AvoStreamId._streamId;
|
|
4
|
+
var AvoGuid_1 = require("./AvoGuid");
|
|
5
|
+
var AvoInspector_1 = require("./AvoInspector");
|
|
6
|
+
var AvoStreamId = /** @class */ (function () {
|
|
7
|
+
function AvoStreamId() {
|
|
43
8
|
}
|
|
44
9
|
/**
|
|
45
|
-
*
|
|
10
|
+
* Returns the persistent anonymous ID (Model A).
|
|
11
|
+
* Generates a UUID once and persists it via AsyncStorage.
|
|
12
|
+
* Never resets based on time.
|
|
13
|
+
* Concurrent calls before cache is populated all return the same UUID.
|
|
46
14
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
15
|
+
AvoStreamId.initialize = function () {
|
|
16
|
+
// Return cached value immediately if available
|
|
17
|
+
if (AvoStreamId._anonymousId !== null) {
|
|
18
|
+
return Promise.resolve(AvoStreamId._anonymousId);
|
|
50
19
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Reset if older than 4 hours AND idle for 2+ hours
|
|
55
|
-
return age > AvoStreamId.MAX_AGE_MS && idleTime > AvoStreamId.IDLE_THRESHOLD_MS;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Load stream ID and timestamps from storage into cache.
|
|
59
|
-
*/
|
|
60
|
-
static loadFromStorage() {
|
|
61
|
-
try {
|
|
62
|
-
const storedId = AvoInspector_1.AvoInspector.avoStorage.getItem(AvoStreamId.STREAM_ID_KEY);
|
|
63
|
-
const createdAt = AvoInspector_1.AvoInspector.avoStorage.getItem(AvoStreamId.CREATED_AT_KEY);
|
|
64
|
-
const lastActivityAt = AvoInspector_1.AvoInspector.avoStorage.getItem(AvoStreamId.LAST_ACTIVITY_KEY);
|
|
65
|
-
if (storedId !== null && storedId !== undefined) {
|
|
66
|
-
AvoStreamId._streamId = storedId;
|
|
67
|
-
AvoStreamId._createdAt = createdAt ?? Date.now();
|
|
68
|
-
AvoStreamId._lastActivityAt = lastActivityAt ?? Date.now();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
catch (e) {
|
|
72
|
-
console.error("Avo Inspector: Error reading stream ID from storage. Please report to support@avo.app.", e);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Generate a new stream ID and store it with timestamps.
|
|
77
|
-
*/
|
|
78
|
-
static generateAndStoreNew() {
|
|
79
|
-
const now = Date.now();
|
|
80
|
-
AvoStreamId._streamId = AvoGuid_1.default.newGuid();
|
|
81
|
-
AvoStreamId._createdAt = now;
|
|
82
|
-
AvoStreamId._lastActivityAt = now;
|
|
83
|
-
try {
|
|
84
|
-
AvoInspector_1.AvoInspector.avoStorage.setItem(AvoStreamId.STREAM_ID_KEY, AvoStreamId._streamId);
|
|
85
|
-
AvoInspector_1.AvoInspector.avoStorage.setItem(AvoStreamId.CREATED_AT_KEY, AvoStreamId._createdAt);
|
|
86
|
-
AvoInspector_1.AvoInspector.avoStorage.setItem(AvoStreamId.LAST_ACTIVITY_KEY, AvoStreamId._lastActivityAt);
|
|
20
|
+
// Return the in-flight promise if initialization is already underway
|
|
21
|
+
if (AvoStreamId._initializationPromise !== null) {
|
|
22
|
+
return AvoStreamId._initializationPromise;
|
|
87
23
|
}
|
|
88
|
-
|
|
89
|
-
|
|
24
|
+
// Guard: if storage isn't initialized yet, fall back to a fresh GUID
|
|
25
|
+
if (!AvoInspector_1.AvoInspector.avoStorage) {
|
|
26
|
+
var fallbackId = AvoGuid_1.default.newGuid();
|
|
27
|
+
AvoStreamId._anonymousId = fallbackId;
|
|
28
|
+
return Promise.resolve(fallbackId);
|
|
90
29
|
}
|
|
91
|
-
|
|
92
|
-
|
|
30
|
+
// Start initialization and cache the promise to handle concurrent calls
|
|
31
|
+
var storagePromise = AvoInspector_1.AvoInspector.avoStorage.getItemAsync(AvoStreamId.cacheKey);
|
|
32
|
+
var resolvedPromise = storagePromise && typeof storagePromise.then === "function"
|
|
33
|
+
? storagePromise
|
|
34
|
+
: Promise.resolve(null);
|
|
35
|
+
AvoStreamId._initializationPromise = resolvedPromise.then(function (maybeId) {
|
|
36
|
+
if (maybeId !== null && maybeId !== undefined) {
|
|
37
|
+
AvoStreamId._anonymousId = maybeId;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
AvoStreamId._anonymousId = AvoGuid_1.default.newGuid();
|
|
41
|
+
AvoInspector_1.AvoInspector.avoStorage.setItem(AvoStreamId.cacheKey, AvoStreamId._anonymousId);
|
|
42
|
+
}
|
|
43
|
+
return AvoStreamId._anonymousId;
|
|
44
|
+
}).catch(function (error) {
|
|
45
|
+
// Reset so callers can retry on next call
|
|
46
|
+
AvoStreamId._initializationPromise = null;
|
|
47
|
+
// Fall back to a fresh GUID
|
|
48
|
+
var fallbackId = AvoGuid_1.default.newGuid();
|
|
49
|
+
AvoStreamId._anonymousId = fallbackId;
|
|
50
|
+
return fallbackId;
|
|
51
|
+
});
|
|
52
|
+
return AvoStreamId._initializationPromise;
|
|
53
|
+
};
|
|
93
54
|
/**
|
|
94
|
-
*
|
|
55
|
+
* Sets a custom anonymous ID. Persists to storage.
|
|
95
56
|
*/
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
AvoStreamId.
|
|
99
|
-
|
|
100
|
-
AvoInspector_1.AvoInspector.avoStorage.setItem(AvoStreamId.
|
|
101
|
-
}
|
|
102
|
-
catch (e) {
|
|
103
|
-
// Silently fail - not critical
|
|
57
|
+
AvoStreamId.setAnonymousId = function (id) {
|
|
58
|
+
AvoStreamId._anonymousId = id;
|
|
59
|
+
AvoStreamId._initializationPromise = null;
|
|
60
|
+
if (AvoInspector_1.AvoInspector.avoStorage) {
|
|
61
|
+
AvoInspector_1.AvoInspector.avoStorage.setItem(AvoStreamId.cacheKey, id);
|
|
104
62
|
}
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* The storage key used to persist the stream ID.
|
|
108
|
-
*/
|
|
109
|
-
static get storageKey() {
|
|
110
|
-
return AvoStreamId.STREAM_ID_KEY;
|
|
111
|
-
}
|
|
63
|
+
};
|
|
112
64
|
/**
|
|
113
|
-
* The
|
|
65
|
+
* Clears the cached anonymous ID. The next access will reload from storage.
|
|
114
66
|
*/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
AvoStreamId._createdAt = null;
|
|
131
|
-
AvoStreamId._lastActivityAt = null;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
67
|
+
AvoStreamId.clearCache = function () {
|
|
68
|
+
AvoStreamId._anonymousId = null;
|
|
69
|
+
AvoStreamId._initializationPromise = null;
|
|
70
|
+
};
|
|
71
|
+
Object.defineProperty(AvoStreamId, "cacheKey", {
|
|
72
|
+
get: function () {
|
|
73
|
+
return "AvoInspectorAnonymousId";
|
|
74
|
+
},
|
|
75
|
+
enumerable: false,
|
|
76
|
+
configurable: true
|
|
77
|
+
});
|
|
78
|
+
AvoStreamId._anonymousId = null;
|
|
79
|
+
AvoStreamId._initializationPromise = null;
|
|
80
|
+
return AvoStreamId;
|
|
81
|
+
}());
|
|
134
82
|
exports.AvoStreamId = AvoStreamId;
|
|
135
|
-
AvoStreamId._streamId = null;
|
|
136
|
-
AvoStreamId._createdAt = null;
|
|
137
|
-
AvoStreamId._lastActivityAt = null;
|
|
138
|
-
// Ephemeral reset thresholds
|
|
139
|
-
AvoStreamId.MAX_AGE_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
140
|
-
AvoStreamId.IDLE_THRESHOLD_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
141
|
-
// Storage keys
|
|
142
|
-
AvoStreamId.STREAM_ID_KEY = "AvoInspectorStreamId";
|
|
143
|
-
AvoStreamId.CREATED_AT_KEY = "AvoInspectorStreamIdCreatedAt";
|
|
144
|
-
AvoStreamId.LAST_ACTIVITY_KEY = "AvoInspectorStreamIdLastActivityAt";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
var xhrMock = {
|
|
4
|
+
open: jest.fn(),
|
|
5
|
+
send: jest.fn(),
|
|
6
|
+
setRequestHeader: jest.fn(),
|
|
7
|
+
onload: jest.fn(),
|
|
8
|
+
onerror: jest.fn(),
|
|
9
|
+
ontimeout: jest.fn(),
|
|
10
|
+
status: 200,
|
|
11
|
+
response: "{}",
|
|
12
|
+
};
|
|
13
|
+
window.XMLHttpRequest = jest.fn(function () { return xhrMock; });
|
|
14
|
+
exports.default = xhrMock;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
var xhrErrorMock = {
|
|
4
|
+
open: jest.fn(),
|
|
5
|
+
send: jest.fn(),
|
|
6
|
+
setRequestHeader: jest.fn(),
|
|
7
|
+
onload: jest.fn(),
|
|
8
|
+
onerror: jest.fn(),
|
|
9
|
+
ontimeout: jest.fn(),
|
|
10
|
+
status: 400,
|
|
11
|
+
statusText: "Bad Request",
|
|
12
|
+
};
|
|
13
|
+
window.XMLHttpRequest = jest.fn(function () { return xhrErrorMock; });
|
|
14
|
+
exports.default = xhrErrorMock;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Tests for AvoEncryption (React Native - @noble/ciphers based, fully synchronous).
|
|
5
|
+
*
|
|
6
|
+
* Covers:
|
|
7
|
+
* - shouldEncrypt() truth table
|
|
8
|
+
* - Wire format structural tests
|
|
9
|
+
* - Cross-SDK interop (TypeScript mirror of Java EncryptionInteropTestUtil)
|
|
10
|
+
* - Encryption failure handling
|
|
11
|
+
* - List-type property omission
|
|
12
|
+
* - publicEncryptionKey in base body
|
|
13
|
+
* - Prod negative test
|
|
14
|
+
*/
|
|
15
|
+
var p256_1 = require("@noble/curves/p256");
|
|
16
|
+
var aes_1 = require("@noble/ciphers/aes");
|
|
17
|
+
var sha256_1 = require("@noble/hashes/sha256");
|
|
18
|
+
var AvoEncryption_1 = require("../AvoEncryption");
|
|
19
|
+
// =========================================================================
|
|
20
|
+
// Helper: TypeScript mirror of Java EncryptionInteropTestUtil.decrypt
|
|
21
|
+
// =========================================================================
|
|
22
|
+
function hexToBytes(hex) {
|
|
23
|
+
var bytes = new Uint8Array(hex.length / 2);
|
|
24
|
+
for (var i = 0; i < hex.length; i += 2) {
|
|
25
|
+
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
26
|
+
}
|
|
27
|
+
return bytes;
|
|
28
|
+
}
|
|
29
|
+
function bytesToHex(bytes) {
|
|
30
|
+
return Array.from(bytes)
|
|
31
|
+
.map(function (b) { return b.toString(16).padStart(2, "0"); })
|
|
32
|
+
.join("");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Reference decryptor mirroring Java EncryptionInteropTestUtil.decrypt.
|
|
36
|
+
* Uses @noble/curves + @noble/ciphers (same libs, but acts as independent verification).
|
|
37
|
+
*/
|
|
38
|
+
function referenceDecrypt(base64Encrypted, privateKeyHex) {
|
|
39
|
+
var data = Buffer.from(base64Encrypted, "base64");
|
|
40
|
+
// Minimum length: 1 (version) + 65 (ephemeral pub key) + 16 (IV) + 16 (auth tag) + 1 (min ciphertext)
|
|
41
|
+
if (data.length < 99) {
|
|
42
|
+
throw new Error("Encrypted data too short: expected at least 99 bytes, got ".concat(data.length));
|
|
43
|
+
}
|
|
44
|
+
// Step 2: Assert version byte
|
|
45
|
+
if (data[0] !== 0x00) {
|
|
46
|
+
throw new Error("Unsupported version byte: expected 0x00, got 0x".concat(data[0].toString(16).padStart(2, "0")));
|
|
47
|
+
}
|
|
48
|
+
// Step 3: Extract ephemeral public key bytes [1..65] (65 bytes, uncompressed EC point)
|
|
49
|
+
var ephemeralPubKeyBytes = new Uint8Array(data.slice(1, 66));
|
|
50
|
+
// Step 4: Extract IV [66..81] (16 bytes)
|
|
51
|
+
var iv = new Uint8Array(data.slice(66, 82));
|
|
52
|
+
// Step 5: Extract auth tag [82..97] (16 bytes)
|
|
53
|
+
var authTag = new Uint8Array(data.slice(82, 98));
|
|
54
|
+
// Step 6: Extract ciphertext [98..N]
|
|
55
|
+
var ciphertext = new Uint8Array(data.slice(98));
|
|
56
|
+
// Step 7-8: ECDH with recipient private key -> sharedSecret
|
|
57
|
+
var privateKeyBytes = hexToBytes(privateKeyHex);
|
|
58
|
+
var sharedSecretPoint = p256_1.p256.getSharedSecret(privateKeyBytes, ephemeralPubKeyBytes);
|
|
59
|
+
var sharedSecret = sharedSecretPoint.slice(-32); // X-coordinate
|
|
60
|
+
// Step 9: KDF: SHA-256(sharedSecret) -> aesKey
|
|
61
|
+
var aesKey = (0, sha256_1.sha256)(sharedSecret);
|
|
62
|
+
// Step 10: AES/GCM decrypt
|
|
63
|
+
// @noble/ciphers gcm expects ciphertext + authTag concatenated
|
|
64
|
+
var ciphertextWithTag = new Uint8Array(ciphertext.length + authTag.length);
|
|
65
|
+
ciphertextWithTag.set(ciphertext, 0);
|
|
66
|
+
ciphertextWithTag.set(authTag, ciphertext.length);
|
|
67
|
+
var aes = (0, aes_1.gcm)(aesKey, iv);
|
|
68
|
+
var plainBytes = aes.decrypt(ciphertextWithTag);
|
|
69
|
+
// Step 11: Return as UTF-8 string
|
|
70
|
+
return new TextDecoder().decode(plainBytes);
|
|
71
|
+
}
|
|
72
|
+
// =========================================================================
|
|
73
|
+
// Generate a test key pair
|
|
74
|
+
// =========================================================================
|
|
75
|
+
function generateTestKeyPair() {
|
|
76
|
+
var privateKeyBytes = p256_1.p256.utils.randomPrivateKey();
|
|
77
|
+
var publicKeyBytes = p256_1.p256.getPublicKey(privateKeyBytes, false);
|
|
78
|
+
return {
|
|
79
|
+
privateKey: bytesToHex(privateKeyBytes).padStart(64, "0"),
|
|
80
|
+
publicKey: bytesToHex(publicKeyBytes),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// =========================================================================
|
|
84
|
+
// Tests
|
|
85
|
+
// =========================================================================
|
|
86
|
+
describe("AvoEncryption (React Native - @noble/ciphers)", function () {
|
|
87
|
+
// -----------------------------------------------------------------------
|
|
88
|
+
// shouldEncrypt() truth table
|
|
89
|
+
// -----------------------------------------------------------------------
|
|
90
|
+
describe("shouldEncrypt()", function () {
|
|
91
|
+
test("dev + valid key = true", function () {
|
|
92
|
+
expect((0, AvoEncryption_1.shouldEncrypt)("dev", "04abcdef1234")).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
test("staging + valid key = true", function () {
|
|
95
|
+
expect((0, AvoEncryption_1.shouldEncrypt)("staging", "04abcdef1234")).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
test("prod + valid key = false", function () {
|
|
98
|
+
expect((0, AvoEncryption_1.shouldEncrypt)("prod", "04abcdef1234")).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
test("dev + null key = false", function () {
|
|
101
|
+
expect((0, AvoEncryption_1.shouldEncrypt)("dev", null)).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
test("dev + undefined key = false", function () {
|
|
104
|
+
expect((0, AvoEncryption_1.shouldEncrypt)("dev", undefined)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
test("dev + empty string key = false", function () {
|
|
107
|
+
expect((0, AvoEncryption_1.shouldEncrypt)("dev", "")).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
test("dev + whitespace-only key = false", function () {
|
|
110
|
+
expect((0, AvoEncryption_1.shouldEncrypt)("dev", " ")).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
// -----------------------------------------------------------------------
|
|
114
|
+
// Wire format structural test
|
|
115
|
+
// -----------------------------------------------------------------------
|
|
116
|
+
describe("Wire format", function () {
|
|
117
|
+
var keyPair = generateTestKeyPair();
|
|
118
|
+
test("base64Decode(output).length >= 99", function () {
|
|
119
|
+
var encrypted = (0, AvoEncryption_1.encryptValue)("hello world", keyPair.publicKey);
|
|
120
|
+
var decoded = Buffer.from(encrypted, "base64");
|
|
121
|
+
expect(decoded.length).toBeGreaterThanOrEqual(99);
|
|
122
|
+
});
|
|
123
|
+
test("output[0] == 0x00 (version byte)", function () {
|
|
124
|
+
var encrypted = (0, AvoEncryption_1.encryptValue)("hello world", keyPair.publicKey);
|
|
125
|
+
var decoded = Buffer.from(encrypted, "base64");
|
|
126
|
+
expect(decoded[0]).toBe(0x00);
|
|
127
|
+
});
|
|
128
|
+
test("output[1] == 0x04 (uncompressed point marker)", function () {
|
|
129
|
+
var encrypted = (0, AvoEncryption_1.encryptValue)("hello world", keyPair.publicKey);
|
|
130
|
+
var decoded = Buffer.from(encrypted, "base64");
|
|
131
|
+
expect(decoded[1]).toBe(0x04);
|
|
132
|
+
});
|
|
133
|
+
test("encryptValue is synchronous (returns string, not Promise)", function () {
|
|
134
|
+
var result = (0, AvoEncryption_1.encryptValue)("test", keyPair.publicKey);
|
|
135
|
+
// If it were async, result would be a Promise, not a string
|
|
136
|
+
expect(typeof result).toBe("string");
|
|
137
|
+
// Ensure it's NOT a Promise
|
|
138
|
+
expect(result).not.toBeInstanceOf(Promise);
|
|
139
|
+
});
|
|
140
|
+
test("different encryptions produce different output", function () {
|
|
141
|
+
var enc1 = (0, AvoEncryption_1.encryptValue)("same", keyPair.publicKey);
|
|
142
|
+
var enc2 = (0, AvoEncryption_1.encryptValue)("same", keyPair.publicKey);
|
|
143
|
+
expect(enc1).not.toBe(enc2);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
// -----------------------------------------------------------------------
|
|
147
|
+
// Cross-SDK interop: RN-encrypted ciphertext decryptable by reference decryptor
|
|
148
|
+
// -----------------------------------------------------------------------
|
|
149
|
+
describe("Cross-SDK interop", function () {
|
|
150
|
+
var keyPair = generateTestKeyPair();
|
|
151
|
+
var standardPlaintexts = [
|
|
152
|
+
'"hello world"',
|
|
153
|
+
"42",
|
|
154
|
+
"3.14",
|
|
155
|
+
"true",
|
|
156
|
+
'"test string value"',
|
|
157
|
+
];
|
|
158
|
+
standardPlaintexts.forEach(function (plaintext) {
|
|
159
|
+
test("RN encrypt -> reference decrypt: ".concat(plaintext), function () {
|
|
160
|
+
var encrypted = (0, AvoEncryption_1.encryptValue)(plaintext, keyPair.publicKey);
|
|
161
|
+
var decrypted = referenceDecrypt(encrypted, keyPair.privateKey);
|
|
162
|
+
expect(decrypted).toBe(plaintext);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
test("round-trip with JSON-stringified object", function () {
|
|
166
|
+
var obj = { key: "value", number: 123 };
|
|
167
|
+
var jsonStr = JSON.stringify(obj);
|
|
168
|
+
var encrypted = (0, AvoEncryption_1.encryptValue)(jsonStr, keyPair.publicKey);
|
|
169
|
+
var decrypted = referenceDecrypt(encrypted, keyPair.privateKey);
|
|
170
|
+
expect(decrypted).toBe(jsonStr);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// -----------------------------------------------------------------------
|
|
174
|
+
// encryptEventProperties
|
|
175
|
+
// -----------------------------------------------------------------------
|
|
176
|
+
describe("encryptEventProperties()", function () {
|
|
177
|
+
var keyPair = generateTestKeyPair();
|
|
178
|
+
test("encrypts string property values", function () {
|
|
179
|
+
var properties = [
|
|
180
|
+
{ propertyName: "name", propertyType: "string" },
|
|
181
|
+
];
|
|
182
|
+
var eventProps = { name: "Alice" };
|
|
183
|
+
var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, keyPair.publicKey);
|
|
184
|
+
expect(result).toHaveLength(1);
|
|
185
|
+
expect(result[0].propertyName).toBe("name");
|
|
186
|
+
expect(result[0].propertyType).toBe("string");
|
|
187
|
+
expect(result[0].encryptedPropertyValue).toBeDefined();
|
|
188
|
+
// Verify it decrypts correctly
|
|
189
|
+
var decrypted = referenceDecrypt(result[0].encryptedPropertyValue, keyPair.privateKey);
|
|
190
|
+
expect(JSON.parse(decrypted)).toBe("Alice");
|
|
191
|
+
});
|
|
192
|
+
test("encrypts int property values", function () {
|
|
193
|
+
var properties = [
|
|
194
|
+
{ propertyName: "age", propertyType: "int" },
|
|
195
|
+
];
|
|
196
|
+
var eventProps = { age: 42 };
|
|
197
|
+
var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, keyPair.publicKey);
|
|
198
|
+
expect(result[0].encryptedPropertyValue).toBeDefined();
|
|
199
|
+
var decrypted = referenceDecrypt(result[0].encryptedPropertyValue, keyPair.privateKey);
|
|
200
|
+
expect(JSON.parse(decrypted)).toBe(42);
|
|
201
|
+
});
|
|
202
|
+
test("encrypts boolean property values", function () {
|
|
203
|
+
var properties = [
|
|
204
|
+
{ propertyName: "active", propertyType: "boolean" },
|
|
205
|
+
];
|
|
206
|
+
var eventProps = { active: true };
|
|
207
|
+
var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, keyPair.publicKey);
|
|
208
|
+
expect(result[0].encryptedPropertyValue).toBeDefined();
|
|
209
|
+
var decrypted = referenceDecrypt(result[0].encryptedPropertyValue, keyPair.privateKey);
|
|
210
|
+
expect(JSON.parse(decrypted)).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
test("omits list-type properties entirely", function () {
|
|
213
|
+
var properties = [
|
|
214
|
+
{ propertyName: "tags", propertyType: "list", children: ["string"] },
|
|
215
|
+
{ propertyName: "name", propertyType: "string" },
|
|
216
|
+
];
|
|
217
|
+
var eventProps = { tags: ["a", "b"], name: "Alice" };
|
|
218
|
+
var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, keyPair.publicKey);
|
|
219
|
+
// list property should be omitted entirely
|
|
220
|
+
expect(result).toHaveLength(1);
|
|
221
|
+
expect(result[0].propertyName).toBe("name");
|
|
222
|
+
});
|
|
223
|
+
test("encryption failure: console.warn, omit property, continue", function () {
|
|
224
|
+
var warnSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
225
|
+
var properties = [
|
|
226
|
+
{ propertyName: "secret", propertyType: "string" },
|
|
227
|
+
{ propertyName: "name", propertyType: "string" },
|
|
228
|
+
];
|
|
229
|
+
var eventProps = { secret: "value1", name: "value2" };
|
|
230
|
+
// Use an invalid public key to trigger encryption failure for the first property
|
|
231
|
+
// We can't easily make only one fail, so let's test with a completely invalid key
|
|
232
|
+
var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, "invalid-key");
|
|
233
|
+
// Both should fail with invalid key, so both should be omitted
|
|
234
|
+
expect(result).toHaveLength(0);
|
|
235
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
236
|
+
// Check that warning message matches expected format
|
|
237
|
+
var calls = warnSpy.mock.calls;
|
|
238
|
+
expect(calls.some(function (call) {
|
|
239
|
+
return String(call[0]).includes("[Avo Inspector] Encryption failed:");
|
|
240
|
+
})).toBe(true);
|
|
241
|
+
warnSpy.mockRestore();
|
|
242
|
+
});
|
|
243
|
+
test("null property values are encrypted as null", function () {
|
|
244
|
+
var properties = [
|
|
245
|
+
{ propertyName: "nullProp", propertyType: "null" },
|
|
246
|
+
];
|
|
247
|
+
var eventProps = { nullProp: null };
|
|
248
|
+
var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, keyPair.publicKey);
|
|
249
|
+
expect(result).toHaveLength(1);
|
|
250
|
+
expect(result[0].encryptedPropertyValue).toBeDefined();
|
|
251
|
+
var decrypted = referenceDecrypt(result[0].encryptedPropertyValue, keyPair.privateKey);
|
|
252
|
+
expect(JSON.parse(decrypted)).toBeNull();
|
|
253
|
+
});
|
|
254
|
+
test("object property values are encrypted", function () {
|
|
255
|
+
var properties = [
|
|
256
|
+
{
|
|
257
|
+
propertyName: "address",
|
|
258
|
+
propertyType: "object",
|
|
259
|
+
children: [
|
|
260
|
+
{ propertyName: "street", propertyType: "string" },
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
var eventProps = { address: { street: "123 Main St" } };
|
|
265
|
+
var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, keyPair.publicKey);
|
|
266
|
+
expect(result).toHaveLength(1);
|
|
267
|
+
expect(result[0].encryptedPropertyValue).toBeDefined();
|
|
268
|
+
var decrypted = referenceDecrypt(result[0].encryptedPropertyValue, keyPair.privateKey);
|
|
269
|
+
expect(JSON.parse(decrypted)).toEqual({ street: "123 Main St" });
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|