@anonympins/fingerprint 0.5.1 → 0.5.4

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 (42) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +118 -115
  3. package/package.json +1 -1
  4. package/src/js/dynamic-wasm.js +52 -13
  5. package/src/js/fingerprint.builder.js +169 -168
  6. package/src/js/fingerprint.client.js +1271 -1189
  7. package/src/js/fingerprint.client.obfuscated.js +1 -1
  8. package/src/js/fingerprint.js +436 -162
  9. package/src/js/fingerprint.utils.js +30 -0
  10. package/src/js/gpu_pow.solver.js +96 -1
  11. package/src/js/library.js +40 -21
  12. package/src/js/mongodb-store.js +165 -79
  13. package/src/js/pow.solver.inline.js +627 -30
  14. package/src/js/pow.solver.js +453 -31
  15. package/src/js/problem-manager.js +1 -1
  16. package/src/js/redis-store.js +114 -42
  17. package/src/js/tests/dns-circuit-breaker.test.js +55 -0
  18. package/src/js/tests/fingerprint.client.init.test.js +143 -143
  19. package/src/js/tests/fingerprint.client.test.js +128 -128
  20. package/src/js/tests/fingerprint.engine.test.js +370 -370
  21. package/src/js/tests/fingerprint.test.js +119 -5
  22. package/src/js/tests/gpu_pow.test.js +81 -0
  23. package/src/js/tests/ip-reputation.test.js +1 -1
  24. package/src/js/tests/pow.solver.test.js +9 -6
  25. package/src/js/tests/usefulWorkSolver.test.js +162 -0
  26. package/src/php/Challenge/ChallengeUtils.php +137 -35
  27. package/src/php/FingerprintClient.php +22 -5
  28. package/src/php/FingerprintEngine.php +335 -58
  29. package/src/php/Optimization/Optimization.php +23 -16
  30. package/src/php/Optimization/OptimizationOperators.php +27 -0
  31. package/src/php/ProblemManager.php +1 -1
  32. package/src/php/Store/MongoDbStore.php +184 -104
  33. package/src/php/Store/RedisStore.php +91 -11
  34. package/src/php/Tests/ChallengeUtilsTest.php +285 -285
  35. package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
  36. package/src/php/Tests/IpReputationTest.php +1 -1
  37. package/src/php/Tests/PowTest.php +1 -1
  38. package/src/php/Tests/RequestUtilsTest.php +29 -0
  39. package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
  40. package/src/php/Utils/BigInt.php +202 -202
  41. package/src/php/Utils/RequestUtils.php +74 -21
  42. package/src/php/Utils/TLSClientHelloParser.php +212 -116
@@ -31,6 +31,7 @@ const {
31
31
  default_whitelist,
32
32
  stopThresholdAutoTuning,
33
33
  getCompositeDeviceHash,
34
+ sanitizeTrafficData,
34
35
  } = fingerprint;
35
36
  const { store, getRequestPatternScore, getDeviceHash } = __internal;
36
37
  const { getRenderingAnomalyScore } = __internal;
@@ -749,7 +750,7 @@ describe('Fingerprint & PoW Security Suite', () => {
749
750
  pow_type: 'cpu_mem',
750
751
  pow_nonce: nonce,
751
752
  pow_solution_cpu: String(cpuSolution),
752
- pow_solution_mem: String(memSolution),
753
+ pow_solution_mem: JSON.stringify(memSolution),
753
754
  pow_fp: solverFingerprint
754
755
  },
755
756
  headers: { 'user-agent': userAgent, 'x-device-fingerprint': solverFingerprint },
@@ -843,7 +844,7 @@ describe('Fingerprint & PoW Security Suite', () => {
843
844
  pow_type: 'cpu_mem',
844
845
  pow_nonce: nonce,
845
846
  pow_solution_cpu: String(cpuSolution),
846
- pow_solution_mem: String(memSolution),
847
+ pow_solution_mem: JSON.stringify(memSolution),
847
848
  pow_fp: solverFingerprint // The client submits its fingerprint.
848
849
  },
849
850
  headers: { 'user-agent': userAgent, 'x-device-fingerprint': solverFingerprint }, rawHeaders: ['User-Agent', userAgent], httpVersion: '1.1'
@@ -1083,6 +1084,35 @@ describe('Fingerprint & PoW Security Suite', () => {
1083
1084
  describe('Threshold Auto-Tuning', () => {
1084
1085
  let setIntervalSpy, clearIntervalSpy, consoleLogSpy;
1085
1086
 
1087
+ test('should sanitize traffic data based on stable hardware cluster', async () => {
1088
+ const trafficData = [];
1089
+ // Attacker rotating IPs and deviceIds but having the same hardware fingerprint
1090
+ for (let i = 0; i < 100; i++) {
1091
+ trafficData.push({
1092
+ type: 'trap_triggered',
1093
+ deviceId: `attacker-dev-${i}`,
1094
+ clientIp: `192.168.1.${i}`,
1095
+ fingerprint: 'gpu:spoofed-nvidia-gpu|cvs:spoofed-canvas-data'
1096
+ });
1097
+ }
1098
+
1099
+ // Unique legit users
1100
+ for (let i = 0; i < 10; i++) {
1101
+ trafficData.push({
1102
+ type: 'challenge_solved',
1103
+ deviceId: `legit-dev-${i}`,
1104
+ clientIp: `10.0.0.${i}`,
1105
+ fingerprint: `gpu:legit-gpu-${i}|cvs:legit-canvas-${i}`
1106
+ });
1107
+ }
1108
+
1109
+ const sanitized = sanitizeTrafficData(trafficData);
1110
+ const attackerLogs = sanitized.filter(log => log.deviceId.startsWith('attacker-dev-'));
1111
+
1112
+ // Total 110 logs. 2% limit = Math.max(3, 2) = 3.
1113
+ expect(attackerLogs.length).toBeLessThanOrEqual(3);
1114
+ });
1115
+
1086
1116
  beforeEach(() => {
1087
1117
  setIntervalSpy = vi.spyOn(global, 'setInterval');
1088
1118
  clearIntervalSpy = vi.spyOn(global, 'clearInterval');
@@ -2122,7 +2152,7 @@ describe('Challenge Page Generation Security (XSS)', () => {
2122
2152
  });
2123
2153
 
2124
2154
  it('should escape the path parameter in generateCombinedPoWChallengePage to prevent XSS', () => {
2125
- const maliciousPath = `test.com";\nconsole.log("pwned");//`;
2155
+ const maliciousPath = `test.com";\nconsole.log("pwned");//`; // Original malicious path
2126
2156
  const challengeDetails = { nonce: 'test-nonce', target: '0000', path: maliciousPath };
2127
2157
 
2128
2158
  const html = generateCombinedPoWChallengePage(challengeDetails, 16, '127.0.0.1', 'secret', {}, '');
@@ -2305,9 +2335,9 @@ describe('Subnet Scoring (Node.js)', () => {
2305
2335
  // The new version handles other prefixes
2306
2336
  expect(getIpSubnet('10.20.30.40', 16)).toBe('10.20.0.0/16');
2307
2337
  // IPv6
2308
- expect(getIpSubnet('2001:db8:abcd:0012:0000:0000:0000:0001', 48)).toBe('2001:db8:abcd:0:0:0:0:0/48');
2338
+ expect(getIpSubnet('2001:db8:abcd:0012:0000:0000:0000:0001', 48)).toBe('2001:0db8:abcd:0000:0000:0000:0000:0000/48');
2309
2339
  // The new version handles other prefixes
2310
- expect(getIpSubnet('2a01:e0a:129:57c0:a1b2:c3d4:e5f6:a7b8', 64)).toBe('2a01:e0a:129:0:0:0:0:0/48');
2340
+ expect(getIpSubnet('2a01:e0a:129:57c0:a1b2:c3d4:e5f6:a7b8', 64)).toBe('2a01:0e0a:0129:0000:0000:0000:0000:0000/48');
2311
2341
  // Invalid IPs
2312
2342
  expect(getIpSubnet('not-an-ip')).toBeNull();
2313
2343
  });
@@ -2721,3 +2751,87 @@ describe('Botnet Cluster Scoring (Node.js)', () => {
2721
2751
  const isDiffIpValid = await fingerprint.isTicketValid('192.168.1.1', ticket, 'device-123', 'hash-abc', false);
2722
2752
  expect(isDiffIpValid).toBe(false);
2723
2753
  });
2754
+
2755
+ describe('IP Registration and Filtering', () => {
2756
+ const localInMemoryStore = {
2757
+ _map: new Map(),
2758
+ async get(key) { return this._map.get(key); },
2759
+ async set(key, value) { this._map.set(key, value); },
2760
+ async has(key) { return this._map.has(key); },
2761
+ async delete(key) { this._map.delete(key); },
2762
+ clear() { this._map.clear(); }
2763
+ };
2764
+
2765
+ beforeEach(() => {
2766
+ localInMemoryStore.clear();
2767
+ configureStore(localInMemoryStore);
2768
+ vi.restoreAllMocks();
2769
+ vi.spyOn(fingerprint.__internal, 'getTlsFingerprint').mockReturnValue({
2770
+ ja3: 'mock-ja3', ja4: 'mock-ja4'
2771
+ });
2772
+ });
2773
+
2774
+ it('should immediately allow requests from IPs in the static allowlist', async () => {
2775
+ const whitelistedIp = '198.51.100.42';
2776
+ const config = {
2777
+ weights: { historyScore: 1.0 },
2778
+ thresholds: { low: 20, block: 95 },
2779
+ whitelist: [
2780
+ { type: 'allowlist', entries: [whitelistedIp] }
2781
+ ]
2782
+ };
2783
+ const engine = new FingerprintEngine(config);
2784
+
2785
+ const req = {
2786
+ clientIp: whitelistedIp,
2787
+ path: '/',
2788
+ cookies: {},
2789
+ query: {},
2790
+ headers: { 'user-agent': 'test-ua' },
2791
+ rawHeaders: ['User-Agent', 'test-ua'],
2792
+ httpVersion: '1.1'
2793
+ };
2794
+
2795
+ const decision = await engine.processRequest(req);
2796
+ expect(decision.action).toBe('next');
2797
+ expect(decision.vector.whitelisted).toBe(100);
2798
+ expect(decision.vector.type).toBe('allowlist');
2799
+ });
2800
+
2801
+ it('should register distinct client IPs for a device in the data store', async () => {
2802
+ const ip1 = '192.168.1.100';
2803
+ const ip2 = '192.168.1.101';
2804
+ const deviceId = 'test-ip-reg-device';
2805
+
2806
+ await localInMemoryStore.set(`device:${deviceId}`, {
2807
+ initialDeviceHash: 'some-hash',
2808
+ ips: new Set([ip1]),
2809
+ lastUpdate: Date.now(),
2810
+ lastFpHash: 'some-hash',
2811
+ lastChangeTimestamp: 0,
2812
+ rapidChangeCount: 0,
2813
+ });
2814
+
2815
+ const req = {
2816
+ clientIp: ip2,
2817
+ path: '/',
2818
+ cookies: { device_id: deviceId },
2819
+ query: {},
2820
+ headers: { 'user-agent': 'test-ua' },
2821
+ rawHeaders: ['User-Agent', 'test-ua'],
2822
+ httpVersion: '1.1'
2823
+ };
2824
+
2825
+ const engine = new FingerprintEngine({
2826
+ weights: { historyScore: 1.0 },
2827
+ thresholds: { low: 20, block: 95 }
2828
+ });
2829
+
2830
+ await engine.processRequest(req);
2831
+
2832
+ const storedData = await localInMemoryStore.get(`device:${deviceId}`);
2833
+ expect(storedData.ips).toBeInstanceOf(Set);
2834
+ expect(storedData.ips.has(ip1)).toBe(true);
2835
+ expect(storedData.ips.has(ip2)).toBe(true);
2836
+ });
2837
+ });
@@ -115,4 +115,85 @@ describe('GpuPowSolver - Chaotic Logistic Map PoW', () => {
115
115
  expect(result.solution.split(',').length).toBe(64);
116
116
  vi.restoreAllMocks();
117
117
  });
118
+
119
+ it('should fallback to WebGL1 if both WebGPU and WebGL2 are not supported', async () => {
120
+ // Mock WebGPU as undefined
121
+ Object.defineProperty(global.navigator, 'gpu', {
122
+ value: undefined,
123
+ writable: true,
124
+ configurable: true
125
+ });
126
+
127
+ // Mock WebGL1 (webgl) context with float extensions enabled
128
+ const mockGl1 = {
129
+ FRAMEBUFFER: 36160,
130
+ FRAMEBUFFER_COMPLETE: 36053,
131
+ COLOR_ATTACHMENT0: 36064,
132
+ TEXTURE_2D: 3553,
133
+ RGBA: 6408,
134
+ FLOAT: 5126,
135
+ TRIANGLES: 4,
136
+ CLAMP_TO_EDGE: 33071,
137
+ TEXTURE_MAG_FILTER: 10240,
138
+ TEXTURE_MIN_FILTER: 10241,
139
+ NEAREST: 9728,
140
+ TEXTURE_WRAP_S: 10242,
141
+ TEXTURE_WRAP_T: 10243,
142
+ useProgram: vi.fn(),
143
+ createBuffer: vi.fn(),
144
+ bindBuffer: vi.fn(),
145
+ bufferData: vi.fn(),
146
+ enableVertexAttribArray: vi.fn(),
147
+ vertexAttribPointer: vi.fn(),
148
+ uniform1f: vi.fn(),
149
+ uniform1i: vi.fn(),
150
+ getUniformLocation: vi.fn(),
151
+ getAttribLocation: vi.fn(),
152
+ drawArrays: vi.fn(),
153
+ getExtension: vi.fn().mockImplementation((ext) => {
154
+ if (ext === 'OES_texture_float' || ext === 'WEBGL_color_buffer_float') {
155
+ return {};
156
+ }
157
+ return null;
158
+ }),
159
+ createTexture: vi.fn(),
160
+ bindTexture: vi.fn(),
161
+ texImage2D: vi.fn(),
162
+ texParameteri: vi.fn(),
163
+ createFramebuffer: vi.fn(),
164
+ bindFramebuffer: vi.fn(),
165
+ framebufferTexture2D: vi.fn(),
166
+ checkFramebufferStatus: vi.fn().mockReturnValue(36053), // gl.FRAMEBUFFER_COMPLETE
167
+ viewport: vi.fn(),
168
+ readPixels: vi.fn((x, y, w, h, format, type, pixels) => {
169
+ for (let i = 0; i < pixels.length; i++) {
170
+ pixels[i] = 0.75;
171
+ }
172
+ })
173
+ };
174
+
175
+ const mockCanvas = {
176
+ width: 8,
177
+ height: 8,
178
+ getContext: vi.fn().mockImplementation((contextId) => {
179
+ if (contextId === 'webgl2') return null;
180
+ if (contextId === 'webgl' || contextId === 'experimental-webgl') return mockGl1;
181
+ return null;
182
+ })
183
+ };
184
+
185
+ vi.spyOn(document, 'createElement').mockImplementation((tagName) => {
186
+ if (tagName === 'canvas') return mockCanvas;
187
+ return {};
188
+ });
189
+
190
+ vi.spyOn(GpuPowSolver, '_createProgram').mockReturnValue({});
191
+
192
+ const result = await GpuPowSolver.solve(seed, 100);
193
+
194
+ expect(result.platform).toBe('webgl1');
195
+ expect(result.solution.split(',').length).toBe(64);
196
+ expect(result.solution.split(',')[0]).toBe('0.750000');
197
+ vi.restoreAllMocks();
198
+ });
118
199
  });
@@ -96,7 +96,7 @@ describe('IP Reputation Local System (Node.js)', () => {
96
96
  expect(ipv4Subnet).toBe('192.168.1.0/24');
97
97
 
98
98
  const ipv6Subnet = __internal.getIpSubnet('2001:db8:abcd:12::1', 24, 48);
99
- expect(ipv6Subnet).toBe('2001:db8:abcd:0:0:0:0:0/48');
99
+ expect(ipv6Subnet).toBe('2001:0db8:abcd:0000:0000:0000:0000:0000/48');
100
100
  });
101
101
 
102
102
  it('should calculate client hints inconsistency score correctly', async () => {
@@ -63,8 +63,8 @@ describe('Proof-of-Work Solvers', () => {
63
63
  const solution1 = await solveMemory(seed, difficulty);
64
64
  const solution2 = await solveMemory(seed, difficulty);
65
65
 
66
- expect(solution1).toBe(solution2);
67
- expect(solution1).not.toBe(await solveMemory('different-seed', difficulty));
66
+ expect(solution1).toStrictEqual(solution2);
67
+ expect(solution1).not.toStrictEqual(await solveMemory('different-seed', difficulty));
68
68
  });
69
69
  });
70
70
 
@@ -106,7 +106,7 @@ describe('Proof-of-Work Solvers', () => {
106
106
 
107
107
  const solutions = await solveChallenge(challenge, ''); // Le fingerprint est passé en 2e arg
108
108
  expect(solutions.rawSolution).toHaveProperty('cpu', expect.any(Number));
109
- expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Number));
109
+ expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Object));
110
110
  }, 20000);
111
111
 
112
112
  it('should solve a "cpu_mem_inline" challenge for a browser', async () => {
@@ -122,7 +122,7 @@ describe('Proof-of-Work Solvers', () => {
122
122
 
123
123
  const solutions = await solveChallenge(challenge, '');
124
124
  expect(solutions.rawSolution).toHaveProperty('cpu', expect.any(Number));
125
- expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Number));
125
+ expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Object));
126
126
  }, 20000);
127
127
 
128
128
  it('should solve a "tsp" challenge', async () => {
@@ -223,9 +223,12 @@ describe('Proof-of-Work Solvers', () => {
223
223
  const seed = 'test-wasm-seed';
224
224
  const difficulty = 4; // 4 MB
225
225
 
226
- const solution = await solveMemory(seed, difficulty);
226
+ const result = await solveMemory(seed, difficulty);
227
227
 
228
- expect(solution).toBe(777);
228
+ expect(result).toBeInstanceOf(Object);
229
+ expect(result.solution).toBe(777);
230
+ expect(result.merkleRoot).toBeDefined();
231
+ expect(result.proofs).toBeDefined();
229
232
  expect(mockWasmModule._malloc).toHaveBeenCalledTimes(1);
230
233
  expect(mockWasmModule._free).toHaveBeenCalledTimes(1);
231
234
  expect(mockWasmModule._solve_memory_challenge).toHaveBeenCalledWith(1234, difficulty);
@@ -0,0 +1,162 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
+ import { solveChallenge } from '../pow.solver.js'; // Assuming pow.solver.js is the ES module version
3
+
4
+ // Mock crypto.subtle for hashing operations
5
+ const mockSubtleDigest = vi.fn(async (algorithm, data) => {
6
+ // Return a consistent mock hash for testing purposes
7
+ const hash = new Uint8Array(32); // 32 bytes for SHA-256
8
+ for (let i = 0; i < hash.length; i++) {
9
+ hash[i] = i; // Simple predictable hash
10
+ }
11
+ return hash.buffer;
12
+ });
13
+
14
+ // Mock Math.random for deterministic optimization results
15
+ const mockMathRandom = vi.fn(() => 0.5);
16
+
17
+ // Mock IndexedDB for pospace challenges, even if not directly used by this test
18
+ const mockIndexedDB = {
19
+ open: vi.fn(() => ({
20
+ onupgradeneeded: null,
21
+ onsuccess: null,
22
+ onerror: null,
23
+ result: {
24
+ objectStoreNames: { contains: vi.fn(() => true) },
25
+ createObjectStore: vi.fn(),
26
+ transaction: vi.fn(() => ({
27
+ objectStore: vi.fn(() => ({
28
+ put: vi.fn(),
29
+ get: vi.fn(() => ({ onsuccess: null, result: null }))
30
+ }))
31
+ }))
32
+ }
33
+ }))
34
+ };
35
+
36
+ describe('Useful Work Solver for Optimization Challenges', () => {
37
+ let originalMathRandom;
38
+ let originalIndexedDB;
39
+
40
+ beforeEach(() => {
41
+ // Save original implementations
42
+ originalMathRandom = global.Math.random;
43
+ originalIndexedDB = global.indexedDB;
44
+
45
+ // Mock global objects
46
+ vi.stubGlobal('crypto', {
47
+ subtle: {
48
+ digest: mockSubtleDigest,
49
+ },
50
+ randomInt: vi.fn((min, max) => Math.floor(mockMathRandom() * (max - min) + min)), // Mock randomInt for crypto
51
+ randomBytes: vi.fn((length) => new Uint8Array(length).fill(0x01)), // Mock randomBytes for crypto
52
+ });
53
+ global.Math.random = mockMathRandom;
54
+ global.indexedDB = mockIndexedDB;
55
+
56
+ // Reset mocks
57
+ mockSubtleDigest.mockClear();
58
+ mockMathRandom.mockClear();
59
+ mockIndexedDB.open.mockClear();
60
+ });
61
+
62
+ afterEach(() => {
63
+ // Restore original implementations
64
+ vi.unstubAllGlobals();
65
+ global.Math.random = originalMathRandom;
66
+ global.indexedDB = originalIndexedDB;
67
+ });
68
+
69
+ it('should solve a "useful_work_task" with fraud.solve and return a Pareto front', async () => {
70
+ const mockChallenge = {
71
+ type: 'useful_work_task',
72
+ nonce: 'test-nonce-fraud',
73
+ usefulWorkTask: {
74
+ problemId: 'fraud_detection_challenge',
75
+ task: {
76
+ type: 'multi_objective_genetic_algorithm',
77
+ solverName: 'fraud.solve',
78
+ generations: 5, // Keep generations low for quick test
79
+ populationSize: 10, // Keep population low for quick test
80
+ payload: {
81
+ legitimateClicks: [
82
+ { fingerprint: 'fp1', timeToClick: 500, clickX: 10, clickY: 10, mouseEntropy: 0.8, scrollEvents: 5 },
83
+ { fingerprint: 'fp1', timeToClick: 600, clickX: 12, clickY: 12, mouseEntropy: 0.7, scrollEvents: 6 },
84
+ { fingerprint: 'fp2', timeToClick: 700, clickX: 20, clickY: 20, mouseEntropy: 0.9, scrollEvents: 7 },
85
+ ],
86
+ fraudulentClicks: [
87
+ { fingerprint: 'fp3', timeToClick: 50, clickX: 5, clickY: 5, mouseEntropy: 0.1, scrollEvents: 0 },
88
+ { fingerprint: 'fp3', timeToClick: 60, clickX: 6, clickY: 6, mouseEntropy: 0.2, scrollEvents: 1 },
89
+ { fingerprint: 'fp4', timeToClick: 100, clickX: 10, clickY: 10, mouseEntropy: 0.3, scrollEvents: 2 },
90
+ ],
91
+ },
92
+ },
93
+ },
94
+ };
95
+
96
+ const solution = await solveChallenge(mockChallenge);
97
+
98
+ // Assert the structure of the returned solution
99
+ expect(solution.type).toBe('useful_work_task');
100
+ expect(solution.nonce).toBe('test-nonce-fraud');
101
+ expect(solution.rawSolution).toHaveProperty('problem_id', 'fraud_detection_challenge');
102
+ expect(solution.rawSolution).toHaveProperty('work_result');
103
+
104
+ const workResult = solution.rawSolution.work_result;
105
+ expect(workResult).toHaveProperty('paretoFront');
106
+ expect(workResult.paretoFront).toBeInstanceOf(Array);
107
+ expect(workResult.paretoFront.length).toBeGreaterThan(0);
108
+
109
+ // Check the structure of a single solution in the Pareto front
110
+ const firstParetoSolution = workResult.paretoFront[0];
111
+ expect(firstParetoSolution).toHaveProperty('solution');
112
+ expect(firstParetoSolution.solution).toBeInstanceOf(Array); // fraud.solve returns an array of thresholds
113
+ expect(firstParetoSolution.solution.length).toBe(4); // [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents]
114
+ expect(firstParetoSolution).toHaveProperty('objectives');
115
+ expect(firstParetoSolution.objectives).toBeInstanceOf(Array); // [objective1, objective2]
116
+ expect(firstParetoSolution.objectives.length).toBe(2);
117
+ }, 10000); // Increase timeout for GA, even with low generations/population
118
+
119
+ it('should solve a "useful_work_task" with cpc.solve and return a Pareto front', async () => {
120
+ const mockChallenge = {
121
+ type: 'useful_work_task',
122
+ nonce: 'test-nonce-cpc',
123
+ usefulWorkTask: {
124
+ problemId: 'cpc_optimization_challenge',
125
+ task: {
126
+ type: 'multi_objective_genetic_algorithm',
127
+ solverName: 'cpc.solve',
128
+ generations: 5, // Keep generations low for quick test
129
+ populationSize: 10, // Keep population low for quick test
130
+ payload: {
131
+ platformParams: { optimalBaseCommission: 0.3, optimalBonusFactor: 0.1 },
132
+ website: { relevanceScore: 70 },
133
+ advertiser: { credits: 1000 },
134
+ estimatedImpressions: 500,
135
+ competingAds: [{ id: 'ad1' }, { id: 'ad2' }],
136
+ },
137
+ },
138
+ },
139
+ };
140
+
141
+ const solution = await solveChallenge(mockChallenge);
142
+
143
+ // Assert the structure of the returned solution
144
+ expect(solution.type).toBe('useful_work_task');
145
+ expect(solution.nonce).toBe('test-nonce-cpc');
146
+ expect(solution.rawSolution).toHaveProperty('problem_id', 'cpc_optimization_challenge');
147
+ expect(solution.rawSolution).toHaveProperty('work_result');
148
+
149
+ const workResult = solution.rawSolution.work_result;
150
+ expect(workResult).toHaveProperty('paretoFront');
151
+ expect(workResult.paretoFront).toBeInstanceOf(Array);
152
+ expect(workResult.paretoFront.length).toBeGreaterThan(0);
153
+
154
+ // Check the structure of a single solution in the Pareto front
155
+ const firstParetoSolution = workResult.paretoFront[0];
156
+ expect(firstParetoSolution).toHaveProperty('solution');
157
+ expect(typeof firstParetoSolution.solution).toBe('number'); // cpc.solve returns a single CPC value
158
+ expect(firstParetoSolution).toHaveProperty('objectives');
159
+ expect(firstParetoSolution.objectives).toBeInstanceOf(Array); // [platformRevenue, advertiserValue, marketImbalance]
160
+ expect(firstParetoSolution.objectives.length).toBe(3);
161
+ }, 10000);
162
+ });