@anonympins/fingerprint 0.7.4 → 0.8.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 (64) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/public/anonympins-bot-mitigation-pow.zip +0 -0
  5. package/src/js/dynamic-wasm.js +218 -218
  6. package/src/js/fingerprint.client.js +1784 -1738
  7. package/src/js/fingerprint.client.obfuscated.js +1 -1
  8. package/src/js/fingerprint.js +489 -99
  9. package/src/js/fingerprint.utils.js +252 -2
  10. package/src/js/pow.worker.js +2 -2
  11. package/src/js/tests/cross_parity.test.js +3 -3
  12. package/src/js/tests/dns-circuit-breaker.test.js +6 -6
  13. package/src/js/tests/dynamic-wasm.test.js +77 -77
  14. package/src/js/tests/fingerprint.builder.test.js +76 -78
  15. package/src/js/tests/fingerprint.client.init.test.js +78 -0
  16. package/src/js/tests/fingerprint.client.test.js +16 -17
  17. package/src/js/tests/fingerprint.test.js +37 -12
  18. package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
  19. package/src/js/tests/library.test.js +94 -95
  20. package/src/js/tests/obfuscation.test.js +8 -8
  21. package/src/js/tests/quicFingerprint.test.js +47 -0
  22. package/src/js/tests/tcpFingerprint.test.js +4 -4
  23. package/src/js/tests/threatIntel.test.js +40 -1
  24. package/src/js/upow-model-task.js +155 -112
  25. package/src/php/Challenge/ChallengeUtils.php +392 -86
  26. package/src/php/Config/SecurityProfiles.php +79 -61
  27. package/src/php/DirectFingerprint.php +55 -8
  28. package/src/php/FingerprintBuilder.php +13 -13
  29. package/src/php/FingerprintClient.php +6 -6
  30. package/src/php/FingerprintEngine.php +2122 -1690
  31. package/src/php/Ja3AnomalyDetector.php +22 -22
  32. package/src/php/Optimization/FunctionRegistry.php +4 -4
  33. package/src/php/Optimization/Optimization.php +6 -6
  34. package/src/php/RequestContext.php +11 -11
  35. package/src/php/Store/InMemoryStore.php +96 -66
  36. package/src/php/Store/MongoDbStore.php +2 -2
  37. package/src/php/Store/RedisStore.php +1 -1
  38. package/src/php/Store/StoreManager.php +48 -35
  39. package/src/php/Tests/ChallengeUtilsTest.php +31 -0
  40. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  41. package/src/php/Tests/FingerprintClientTest.php +70 -70
  42. package/src/php/Tests/PowTest.php +37 -39
  43. package/src/php/Tests/QuicFingerprintTest.php +115 -53
  44. package/src/php/Tests/ThreatIntelTest.php +34 -0
  45. package/src/php/Utils/BigInt.php +202 -202
  46. package/src/php/Utils/BlockList.php +13 -13
  47. package/src/php/Utils/Logger.php +19 -1
  48. package/src/php/Utils/MaliciousPatterns.php +6 -6
  49. package/src/php/Utils/MetricsManager.php +209 -209
  50. package/src/php/Utils/RequestUtils.php +362 -1
  51. package/src/php/Utils/TLSClientHelloParser.php +385 -369
  52. package/src/php/WordPress/WpDbStore.php +7 -7
  53. package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
  54. package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
  55. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
  56. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
  57. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  58. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
  59. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  60. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
  61. package/src/php/WordPress/package.php +80 -101
  62. package/src/php/WordPress/readme.txt +21 -5
  63. package/src/php/bin/auto-tune.php +1 -1
  64. package/public/fingerprint-anti-bot.zip +0 -0
@@ -1,5 +1,18 @@
1
1
  import {cyrb53} from "./fingerprint.builder.js";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
2
5
 
6
+ /**
7
+ * Verifies a Zero-Knowledge Proof (ZKP) of the Schnorr type.
8
+ * The verification checks that g^s ≡ t * y^c (mod p),
9
+ * where c is a challenge computed by SHA-256 hashing of (g, y, t).
10
+ *
11
+ * @param {string} yStr - The public key y, hex-encoded (without the 0x prefix).
12
+ * @param {string} tStr - The commitment t, hex-encoded (without the 0x prefix).
13
+ * @param {string} sStr - The response s, hex-encoded (without the 0x prefix).
14
+ * @returns {boolean} True if the proof is valid, false otherwise (or on error).
15
+ */
3
16
  export function verifyZkpProof(yStr, tStr, sStr) {
4
17
  try {
5
18
  const y = BigInt('0x' + yStr);
@@ -19,6 +32,14 @@ export function verifyZkpProof(yStr, tStr, sStr) {
19
32
  }
20
33
  }
21
34
 
35
+ /**
36
+ * Serializes a value to JSON while escaping dangerous characters
37
+ * for safe inclusion in HTML/JS (XSS prevention).
38
+ * Escapes <, >, U+2028, and U+2029.
39
+ *
40
+ * @param {*} val - The value to serialize.
41
+ * @returns {string} The escaped JSON string.
42
+ */
22
43
  export function safeJsonStringify(val) {
23
44
  return JSON.stringify(val)
24
45
  .replace(/</g, '\\u003c')
@@ -27,6 +48,16 @@ export function safeJsonStringify(val) {
27
48
  .replace(/\u2029/g, '\\u2029');
28
49
  }
29
50
 
51
+ /**
52
+ * Sanitizes and validates a redirect path to prevent open redirects.
53
+ * - Strips out disallowed characters.
54
+ * - Prevents protocol-relative redirects (//evil.com).
55
+ * - Prevents absolute redirects (http://evil.com).
56
+ * - Ensures the path starts with a single '/'.
57
+ *
58
+ * @param {string} p - The raw redirect path.
59
+ * @returns {string} The sanitized path (or '/' by default).
60
+ */
30
61
  export function sanitizeRedirectPath(p) {
31
62
  if (typeof p !== 'string') return '/';
32
63
  let sanitized = p.replace(/[^a-zA-Z0-9\/.\-_~%?&=:@+,;]/g, '');
@@ -49,6 +80,16 @@ export function sanitizeRedirectPath(p) {
49
80
  }
50
81
  return sanitized.replace(/^\/+/g, '/');
51
82
  }
83
+
84
+ /**
85
+ * Decodes a polymorphic fingerprint by restoring the original keys
86
+ * from a mapping of randomized keys.
87
+ * Expected format: "randKey: value|randKey: value|...".
88
+ *
89
+ * @param {string} fpString - The encoded fingerprint.
90
+ * @param {object} mapping - The mapping object containing `keys` (orig -> rand).
91
+ * @returns {string} The decoded fingerprint with original keys.
92
+ */
52
93
  export function decodePolymorphicFingerprint(fpString, mapping) {
53
94
  if (!fpString || !mapping || !mapping.keys) return fpString;
54
95
  const reverseKeys = {};
@@ -67,6 +108,130 @@ export function decodePolymorphicFingerprint(fpString, mapping) {
67
108
  return mappedParts.join('|');
68
109
  }
69
110
 
111
+ /**
112
+ * Extracts and parses a Private Access Token (PAT / Privacy Pass / RFC 9578).
113
+ * Supports 'Authorization: PrivateToken token="..."' and 'Sec-Private-State-Token'.
114
+ *
115
+ * @param {object} headers - HTTP request headers object.
116
+ * @returns {Array<Uint8Array>} Array of raw binary tokens.
117
+ */
118
+ export function extractPrivateAccessTokens(headers) {
119
+ if (!headers) return [];
120
+ const tokens = [];
121
+
122
+ const authHeader = headers['authorization'] || headers['Authorization'];
123
+ if (typeof authHeader === 'string' && authHeader.toLowerCase().startsWith('privatetoken ')) {
124
+ const match = authHeader.match(/token=(?:"([^"]+)"|([a-zA-Z0-9_\-+/=]+))/i);
125
+ if (match) {
126
+ const rawB64 = match[1] || match[2];
127
+ try {
128
+ tokens.push(Buffer.from(rawB64.replace(/-/g, '+').replace(/_/g, '/'), 'base64'));
129
+ } catch (e) {}
130
+ }
131
+ }
132
+
133
+ const pstHeader = headers['sec-private-state-token'] || headers['Sec-Private-State-Token'];
134
+ if (typeof pstHeader === 'string') {
135
+ const parts = pstHeader.split(',');
136
+ for (const part of parts) {
137
+ const trimmed = part.trim();
138
+ if (trimmed) {
139
+ try {
140
+ tokens.push(Buffer.from(trimmed.replace(/-/g, '+').replace(/_/g, '/'), 'base64'));
141
+ } catch (e) {}
142
+ }
143
+ }
144
+ }
145
+
146
+ return tokens;
147
+ }
148
+
149
+ /**
150
+ * Decodes an RFC 9578 Token structure:
151
+ * uint16_t token_type (2 bytes)
152
+ * uint8_t nonce[32] (32 bytes)
153
+ * uint8_t challenge_digest[32] (32 bytes)
154
+ * uint8_t token_key_id[32] (32 bytes)
155
+ * uint8_t authenticator[Nk] (>= 32 bytes)
156
+ *
157
+ * @param {Uint8Array|Buffer} buffer
158
+ * @returns {object|null}
159
+ */
160
+ export function parsePrivateAccessToken(buffer) {
161
+ if (!buffer || buffer.length < 98) return null; // 2 + 32 + 32 + 32 = 98 min
162
+
163
+ const view = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
164
+ const tokenType = view.readUInt16BE(0);
165
+ const nonce = view.subarray(2, 34);
166
+ const challengeDigest = view.subarray(34, 66);
167
+ const tokenKeyId = view.subarray(66, 98);
168
+ const authenticator = view.subarray(98);
169
+
170
+ if (authenticator.length === 0) return null;
171
+
172
+ return {
173
+ tokenType,
174
+ nonce: nonce.toString('hex'),
175
+ challengeDigest: challengeDigest.toString('hex'),
176
+ tokenKeyId: tokenKeyId.toString('hex'),
177
+ authenticator,
178
+ signedData: view.subarray(0, 98)
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Validates a decoded Private Access Token signature against configured public keys.
184
+ *
185
+ * @param {object} parsedToken - Output from parsePrivateAccessToken.
186
+ * @param {object} patConfig - Configuration with trustedKeys mapping.
187
+ * @returns {boolean} True if signature is cryptographically valid.
188
+ */
189
+ export function verifyPatSignature(parsedToken, patConfig = {}) {
190
+ if (!parsedToken) return false;
191
+ const trustedKeys = patConfig.trustedKeys || {};
192
+ const keyId = parsedToken.tokenKeyId;
193
+ const publicKey = trustedKeys[keyId] || trustedKeys[keyId.toLowerCase()] || patConfig.defaultPublicKey;
194
+
195
+ if (!publicKey) {
196
+ return false;
197
+ }
198
+
199
+ const { tokenType, signedData, authenticator } = parsedToken;
200
+
201
+ try {
202
+ // Type 0x0001 (Blind RSA 2048) / 0x0002 (Rate-Limited Blind RSA)
203
+ if (tokenType === 0x0001 || tokenType === 0x0002 || tokenType === 1 || tokenType === 2) {
204
+ // Try RSA-PSS with SHA-384 (RFC 9577) first, fallback to SHA-256 and PKCS#1 v1.5
205
+ const algorithms = [
206
+ { hash: 'sha384', padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 48 },
207
+ { hash: 'sha256', padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 32 },
208
+ { hash: 'sha384', padding: crypto.constants.RSA_PKCS1_PADDING },
209
+ { hash: 'sha256', padding: crypto.constants.RSA_PKCS1_PADDING }
210
+ ];
211
+
212
+ for (const alg of algorithms) {
213
+ try {
214
+ const verifier = crypto.createVerify(alg.hash);
215
+ verifier.update(signedData);
216
+ const isValid = verifier.verify({
217
+ key: publicKey,
218
+ padding: alg.padding,
219
+ saltLength: alg.saltLength
220
+ }, authenticator);
221
+ if (isValid) return true;
222
+ } catch (verifyErr) {}
223
+ }
224
+ } else if (tokenType === 0x0003 || tokenType === 3) {
225
+ // VOPRF / Ed25519 authentication verification
226
+ try {
227
+ return crypto.verify(null, signedData, publicKey, authenticator);
228
+ } catch (voprfErr) {}
229
+ }
230
+ } catch (e) {
231
+ return false;
232
+ }
233
+ return false;
234
+ }
70
235
 
71
236
  /**
72
237
  * @private
@@ -127,6 +292,15 @@ export function parseJa3(ja3String) {
127
292
  };
128
293
  }
129
294
 
295
+ /**
296
+ * Computes modular exponentiation (base^exponent mod modulus) efficiently
297
+ * using the binary exponentiation method (square-and-multiply).
298
+ *
299
+ * @param {bigint} base - The base.
300
+ * @param {bigint} exponent - The exponent.
301
+ * @param {bigint} modulus - The modulus.
302
+ * @returns {bigint} The result of base^exponent mod modulus.
303
+ */
130
304
  export function modPow(base, exponent, modulus) {
131
305
  if (modulus === 1n) return 0n;
132
306
  let result = 1n;
@@ -142,6 +316,14 @@ export function modPow(base, exponent, modulus) {
142
316
  }
143
317
 
144
318
 
319
+ /**
320
+ * Computes a simple hash of the IP network by applying a mask (CIDR prefix).
321
+ * For example, with a prefix of 24, only the first 3 octets are kept.
322
+ *
323
+ * @param {string} ip - The IP address in IPv4 format (e.g. "192.168.1.42").
324
+ * @param {number} [prefix=24] - The network prefix length (in bits).
325
+ * @returns {string|null} A hexadecimal hash of the network, or null if the IP is invalid.
326
+ */
145
327
  export function hashNetwork(ip, prefix = 24) {
146
328
  // Network hash (/24 or /16 mask)
147
329
  const parts = ip.split('.');
@@ -157,6 +339,14 @@ export function hashNetwork(ip, prefix = 24) {
157
339
  }
158
340
  return hash.toString(16);
159
341
  }
342
+
343
+ /**
344
+ * Checks whether an IP address is a loopback address (localhost).
345
+ * Handles IPv4, IPv6, and the IPv4-mapped prefix (::ffff:).
346
+ *
347
+ * @param {string} ip - The IP address to test.
348
+ * @returns {boolean} True if the IP is a loopback address, false otherwise.
349
+ */
160
350
  export function isLoopbackIp(ip) {
161
351
  if (!ip || typeof ip !== 'string') return true;
162
352
  let cleanIp = ip.trim().toLowerCase();
@@ -164,9 +354,16 @@ export function isLoopbackIp(ip) {
164
354
  cleanIp = cleanIp.substring(7);
165
355
  }
166
356
  return cleanIp === '127.0.0.1' || cleanIp === '::1' || cleanIp === 'localhost' ||
167
- cleanIp.startsWith('127.') || cleanIp === '0.0.0.0' || cleanIp === '::';
357
+ cleanIp.startsWith('127.') || cleanIp === '0.0.0.0' || cleanIp === '::';
168
358
  }
169
359
 
360
+ /**
361
+ * Normalizes a referer URL by keeping only the protocol and hostname
362
+ * (e.g. "https://example.com").
363
+ *
364
+ * @param {string} referer - The raw referer URL.
365
+ * @returns {string} The normalized URL, or the original value if invalid.
366
+ */
170
367
  export function normalizeReferer(referer) {
171
368
  try {
172
369
  const url = new URL(referer);
@@ -176,6 +373,13 @@ export function normalizeReferer(referer) {
176
373
  }
177
374
  }
178
375
 
376
+ /**
377
+ * Checks whether an IP address belongs to a private range (RFC 1918, loopback,
378
+ * link-local, CGNAT, etc.). Handles both IPv4 and IPv6 formats.
379
+ *
380
+ * @param {string} ip - The IP address to test.
381
+ * @returns {boolean} True if the IP is private, false otherwise.
382
+ */
179
383
  export function isPrivateIp(ip) {
180
384
  if (!ip || typeof ip !== 'string') return false;
181
385
  let cleanIp = ip.trim().toLowerCase();
@@ -200,7 +404,16 @@ export function isPrivateIp(ip) {
200
404
  }
201
405
  return false;
202
406
  }
203
- // Utility parsing functions
407
+
408
+ /**
409
+ * Basic User-Agent parsing to extract:
410
+ * - the browser (with major version),
411
+ * - the operating system,
412
+ * - the device type (mobile, tablet, desktop).
413
+ *
414
+ * @param {string} ua - The User-Agent string.
415
+ * @returns {object} An object { browser?, os?, device }.
416
+ */
204
417
  export function parseUserAgent(ua) {
205
418
  // Basic User-Agent parser
206
419
  const result = {};
@@ -238,4 +451,41 @@ export function parseUserAgent(ua) {
238
451
  else result.device = 'desktop';
239
452
 
240
453
  return result;
454
+ }
455
+
456
+ /**
457
+ * Programmatically generates an Ed25519 key pair in PEM format
458
+ * (PKCS#8 for private key, SPKI for public key) matching OpenSSL CLI:
459
+ * `openssl genpkey -algorithm ed25519 -out issuer-private.pem`
460
+ * `openssl pkey -in issuer-private.pem -pubout -out issuer-public.pem`
461
+ *
462
+ * @param {string} [outDir='./config'] Target directory to store the PEM files.
463
+ * @param {object} [options={}] Optional configuration (e.g. custom file names).
464
+ * @returns {{privateKeyPath: string, publicKeyPath: string, privateKey: string, publicKey: string}}
465
+ */
466
+ export function generateIssuerPemKeys(outDir = './config', options = {}) {
467
+ const privateKeyName = options.privateKeyName || 'issuer-private.pem';
468
+ const publicKeyName = options.publicKeyName || 'issuer-public.pem';
469
+
470
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
471
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
472
+ publicKeyEncoding: { type: 'spki', format: 'pem' }
473
+ });
474
+
475
+ if (!fs.existsSync(outDir)) {
476
+ fs.mkdirSync(outDir, { recursive: true });
477
+ }
478
+
479
+ const privateKeyPath = path.join(outDir, privateKeyName);
480
+ const publicKeyPath = path.join(outDir, publicKeyName);
481
+
482
+ fs.writeFileSync(privateKeyPath, privateKey, { mode: 0o600 });
483
+ fs.writeFileSync(publicKeyPath, publicKey, { mode: 0o644 });
484
+
485
+ return {
486
+ privateKeyPath,
487
+ publicKeyPath,
488
+ privateKey,
489
+ publicKey
490
+ };
241
491
  }
@@ -10,7 +10,7 @@ self.onmessage = async (event) => {
10
10
  let solution = 0;
11
11
  const encoder = new TextEncoder();
12
12
  if (baseBlock) {
13
- // Mode solveCpuTargetInline (Uint8Array base block)
13
+ // solveCpuTargetInline mode (Uint8Array base block)
14
14
  const blockArray = new Uint8Array(baseBlock);
15
15
  while (true) {
16
16
  const solutionBytes = encoder.encode(String(solution));
@@ -31,7 +31,7 @@ self.onmessage = async (event) => {
31
31
  }
32
32
  }
33
33
  } else {
34
- // Mode solveCpuTarget standard/legacy (Message string)
34
+ // Standard/legacy solveCpuTarget mode (Message string)
35
35
  while (true) {
36
36
  const currentMessage = `${message}:${solution}`;
37
37
  const data = encoder.encode(currentMessage);
@@ -12,14 +12,14 @@ describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', ()
12
12
  let wasmModule = null;
13
13
 
14
14
  beforeAll(async () => {
15
- // Essai de compilation WASM à la volée s'il y a em++
15
+ // Attempt on-the-fly WASM compilation if em++ toolchain is available
16
16
  try {
17
17
  execSync('node src/js/build-client.js', { stdio: 'ignore' });
18
18
  } catch (e) {
19
- // Ignorer si Emscripten n'est pas présent dans l'env
19
+ // Ignore if Emscripten is missing in environment
20
20
  }
21
21
 
22
- // Instancier le WASM réel si compilé
22
+ // Instantiate compiled WASM module if available
23
23
  if (fs.existsSync(fpJsPath) && fs.existsSync(fpWasmPath)) {
24
24
  try {
25
25
  const module = await import(fpJsPath);
@@ -9,15 +9,15 @@ describe('DNS Circuit Breaker', () => {
9
9
  dnsCircuitBreaker.failureCount = 0;
10
10
  dnsCircuitBreaker.lastStateChange = 0;
11
11
  dnsCircuitBreaker.threshold = 5;
12
- dnsCircuitBreaker.cooldownMs = 1000; // 1s de cooldown pour accélérer le test unitaire
12
+ dnsCircuitBreaker.cooldownMs = 1000; // 1s cooldown to speed up test execution
13
13
  });
14
14
 
15
- it('devrait démarrer à l\'état CLOSED et autoriser les résolutions', () => {
15
+ it('should start in CLOSED state and allow lookups', () => {
16
16
  expect(canAttemptDns()).toBe(true);
17
17
  expect(dnsCircuitBreaker.state).toBe('CLOSED');
18
18
  });
19
19
 
20
- it('devrait s\'ouvrir après avoir atteint le seuil d\'échecs (5)', () => {
20
+ it('should transition to OPEN after reaching failure threshold (5)', () => {
21
21
  for (let i = 0; i < 4; i++) {
22
22
  recordDnsFailure();
23
23
  expect(canAttemptDns()).toBe(true);
@@ -29,7 +29,7 @@ describe('DNS Circuit Breaker', () => {
29
29
  expect(dnsCircuitBreaker.state).toBe('OPEN');
30
30
  });
31
31
 
32
- it('devrait passer en HALF-OPEN après expiration du cooldown', async () => {
32
+ it('should transition to HALF-OPEN after cooldown expires', async () => {
33
33
  for (let i = 0; i < 5; i++) {
34
34
  recordDnsFailure();
35
35
  }
@@ -41,12 +41,12 @@ describe('DNS Circuit Breaker', () => {
41
41
  expect(dnsCircuitBreaker.state).toBe('HALF-OPEN');
42
42
  });
43
43
 
44
- it('devrait se refermer (CLOSED) après un succès en HALF-OPEN', async () => {
44
+ it('should reset to CLOSED after a success while in HALF-OPEN', async () => {
45
45
  for (let i = 0; i < 5; i++) {
46
46
  recordDnsFailure();
47
47
  }
48
48
  await new Promise((resolve) => setTimeout(resolve, 1100));
49
- expect(canAttemptDns()).toBe(true); // Passage en HALF-OPEN
49
+ expect(canAttemptDns()).toBe(true); // Transition to HALF-OPEN
50
50
 
51
51
  recordDnsSuccess();
52
52
  expect(dnsCircuitBreaker.state).toBe('CLOSED');
@@ -1,78 +1,78 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { DynamicWasmGenerator } from '../dynamic-wasm.js';
3
-
4
- // Helper function to decode ULEB128 (for verification)
5
- function decodeULEB128(bytes) {
6
- let result = 0;
7
- let shift = 0;
8
- for (const byte of bytes) {
9
- result |= (byte & 0x7f) << shift;
10
- if (!(byte & 0x80)) {
11
- break;
12
- }
13
- shift += 7;
14
- }
15
- return result;
16
- }
17
-
18
- describe('DynamicWasmGenerator', () => {
19
- it('should generate a valid WASM module buffer', () => {
20
- const constants = { seed: 123, multiplier: 33, adder: 7 };
21
- const wasmBuffer = DynamicWasmGenerator.generate(constants);
22
-
23
- // Basic check: ensure it's a Buffer and starts with WASM magic number
24
- expect(wasmBuffer).toBeInstanceOf(Buffer);
25
- expect(wasmBuffer.slice(0, 4).toString('hex')).toBe('0061736d'); // \0asm
26
- expect(wasmBuffer.slice(4, 8).toString('hex')).toBe('01000000'); // Version 1
27
-
28
- // Attempt to compile and instantiate the module to ensure validity
29
- let instance;
30
- try {
31
- const module = new WebAssembly.Module(wasmBuffer);
32
- instance = new WebAssembly.Instance(module, {});
33
- } catch (e) {
34
- // If compilation/instantiation fails, it's an invalid WASM module
35
- expect.fail(`Generated WASM module is invalid: ${e.message}`);
36
- }
37
-
38
- // Verify the exported hash function exists
39
- expect(instance.exports.hash).toBeInstanceOf(Function);
40
- expect(instance.exports.memory).toBeInstanceOf(WebAssembly.Memory);
41
- });
42
-
43
- it('should produce a consistent hash result with the JS fallback', () => {
44
- const constants = { seed: 42, multiplier: 1597334677, adder: 12345 };
45
- const testString = "hello world";
46
-
47
- const wasmBuffer = DynamicWasmGenerator.generate(constants);
48
- const module = new WebAssembly.Module(wasmBuffer);
49
- const instance = new WebAssembly.Instance(module, {});
50
- const exports = instance.exports;
51
- const memory = exports.memory;
52
-
53
- // Write string to WASM memory
54
- const encoder = new TextEncoder();
55
- const bytes = encoder.encode(testString);
56
- const view = new Uint8Array(memory.buffer);
57
- view.set(bytes, 0); // Assuming hash function expects string at address 0
58
-
59
- // Calculate hash using WASM
60
- const wasmHash = exports.hash(0, bytes.length);
61
-
62
- // Calculate hash using JS fallback
63
- const jsHash = DynamicWasmGenerator.hashJs(testString, constants);
64
-
65
- // The WASM and JS implementations should yield the same result
66
- expect(wasmHash).toBe(jsHash);
67
- });
68
-
69
- it('should generate different WASM modules for different constants', () => {
70
- const constants1 = { seed: 1, multiplier: 2, adder: 3 };
71
- const constants2 = { seed: 4, multiplier: 5, adder: 6 };
72
-
73
- const wasmBuffer1 = DynamicWasmGenerator.generate(constants1);
74
- const wasmBuffer2 = DynamicWasmGenerator.generate(constants2);
75
-
76
- expect(wasmBuffer1.toString('hex')).not.toBe(wasmBuffer2.toString('hex'));
77
- });
1
+ import { describe, expect, it } from 'vitest';
2
+ import { DynamicWasmGenerator } from '../dynamic-wasm.js';
3
+
4
+ // Helper function to decode ULEB128 (for verification)
5
+ function decodeULEB128(bytes) {
6
+ let result = 0;
7
+ let shift = 0;
8
+ for (const byte of bytes) {
9
+ result |= (byte & 0x7f) << shift;
10
+ if (!(byte & 0x80)) {
11
+ break;
12
+ }
13
+ shift += 7;
14
+ }
15
+ return result;
16
+ }
17
+
18
+ describe('DynamicWasmGenerator', () => {
19
+ it('should generate a valid WASM module buffer', () => {
20
+ const constants = { seed: 123, multiplier: 33, adder: 7 };
21
+ const wasmBuffer = DynamicWasmGenerator.generate(constants);
22
+
23
+ // Basic check: ensure it's a Buffer and starts with WASM magic number
24
+ expect(wasmBuffer).toBeInstanceOf(Buffer);
25
+ expect(wasmBuffer.slice(0, 4).toString('hex')).toBe('0061736d'); // \0asm
26
+ expect(wasmBuffer.slice(4, 8).toString('hex')).toBe('01000000'); // Version 1
27
+
28
+ // Attempt to compile and instantiate the module to ensure validity
29
+ let instance;
30
+ try {
31
+ const module = new WebAssembly.Module(wasmBuffer);
32
+ instance = new WebAssembly.Instance(module, {});
33
+ } catch (e) {
34
+ // If compilation/instantiation fails, it's an invalid WASM module
35
+ expect.fail(`Generated WASM module is invalid: ${e.message}`);
36
+ }
37
+
38
+ // Verify the exported hash function exists
39
+ expect(instance.exports.hash).toBeInstanceOf(Function);
40
+ expect(instance.exports.memory).toBeInstanceOf(WebAssembly.Memory);
41
+ });
42
+
43
+ it('should produce a consistent hash result with the JS fallback', () => {
44
+ const constants = { seed: 42, multiplier: 1597334677, adder: 12345 };
45
+ const testString = "hello world";
46
+
47
+ const wasmBuffer = DynamicWasmGenerator.generate(constants);
48
+ const module = new WebAssembly.Module(wasmBuffer);
49
+ const instance = new WebAssembly.Instance(module, {});
50
+ const exports = instance.exports;
51
+ const memory = exports.memory;
52
+
53
+ // Write string to WASM memory
54
+ const encoder = new TextEncoder();
55
+ const bytes = encoder.encode(testString);
56
+ const view = new Uint8Array(memory.buffer);
57
+ view.set(bytes, 0); // Assuming hash function expects string at address 0
58
+
59
+ // Calculate hash using WASM
60
+ const wasmHash = exports.hash(0, bytes.length);
61
+
62
+ // Calculate hash using JS fallback
63
+ const jsHash = DynamicWasmGenerator.hashJs(testString, constants);
64
+
65
+ // The WASM and JS implementations should yield the same result
66
+ expect(wasmHash).toBe(jsHash);
67
+ });
68
+
69
+ it('should generate different WASM modules for different constants', () => {
70
+ const constants1 = { seed: 1, multiplier: 2, adder: 3 };
71
+ const constants2 = { seed: 4, multiplier: 5, adder: 6 };
72
+
73
+ const wasmBuffer1 = DynamicWasmGenerator.generate(constants1);
74
+ const wasmBuffer2 = DynamicWasmGenerator.generate(constants2);
75
+
76
+ expect(wasmBuffer1.toString('hex')).not.toBe(wasmBuffer2.toString('hex'));
77
+ });
78
78
  });