@ruvector/edge-net 0.5.0 → 0.5.3

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.
@@ -19,6 +19,14 @@ import {
19
19
  generatePluginTemplate,
20
20
  } from '../plugins/index.js';
21
21
 
22
+ import { PluginFailureContract } from '../plugins/plugin-loader.js';
23
+ import CoreInvariants, {
24
+ EconomicBoundary,
25
+ IdentityFriction,
26
+ WorkVerifier,
27
+ DegradationController,
28
+ } from '../core-invariants.js';
29
+
22
30
  import { CompressionPlugin } from '../plugins/implementations/compression.js';
23
31
  import { E2EEncryptionPlugin } from '../plugins/implementations/e2e-encryption.js';
24
32
  import { FederatedLearningPlugin } from '../plugins/implementations/federated-learning.js';
@@ -364,6 +372,161 @@ test('Swarm intelligence plugin works', async () => {
364
372
  assert(result.iterations === 50, 'Should run 50 iterations');
365
373
  });
366
374
 
375
+ // --- Invariant Enforcement Tests ---
376
+ console.log('\n--- Core Invariants (Cogito, Creo, Codex) ---\n');
377
+
378
+ test('PluginFailureContract enforces circuit breaker', async () => {
379
+ const contract = new PluginFailureContract({
380
+ maxRetries: 3,
381
+ quarantineDurationMs: 100, // Short for testing
382
+ executionTimeoutMs: 50,
383
+ });
384
+
385
+ // Record 3 failures to trip circuit breaker
386
+ contract.recordFailure('test-plugin', new Error('Failure 1'));
387
+ contract.recordFailure('test-plugin', new Error('Failure 2'));
388
+ contract.recordFailure('test-plugin', new Error('Failure 3'));
389
+
390
+ // Plugin should now be blocked
391
+ const canExec = contract.canExecute('test-plugin');
392
+ assert(!canExec.allowed, 'Plugin should be blocked after 3 failures');
393
+ assert(canExec.reason.includes('quarantine'), 'Should mention quarantine');
394
+ });
395
+
396
+ test('PluginFailureContract provides health status', () => {
397
+ const contract = new PluginFailureContract();
398
+
399
+ contract.recordFailure('healthy-plugin', new Error('Single failure'));
400
+
401
+ const health = contract.getHealth('healthy-plugin');
402
+ assert(health.healthy, 'Plugin with 1 failure should still be healthy');
403
+ assertEqual(health.failureCount, 1, 'Should track 1 failure');
404
+
405
+ const summary = contract.getSummary();
406
+ assertEqual(summary.totalPlugins, 1, 'Should track 1 plugin');
407
+ });
408
+
409
+ test('Economic boundary prevents plugin credit modification', () => {
410
+ // Create mock credit system
411
+ const mockCreditSystem = {
412
+ getBalance: (nodeId) => 100,
413
+ getTransactionHistory: () => [],
414
+ getSummary: () => ({ balance: 100 }),
415
+ ledger: { credit: () => {}, debit: () => {} },
416
+ on: () => {},
417
+ };
418
+
419
+ const boundary = new EconomicBoundary(mockCreditSystem);
420
+ const pluginView = boundary.getPluginView();
421
+
422
+ // Read operations should work
423
+ assertEqual(pluginView.getBalance('node-1'), 100, 'Should read balance');
424
+
425
+ // Write operations should throw
426
+ let mintThrew = false;
427
+ try { pluginView.mint(); } catch (e) {
428
+ mintThrew = e.message.includes('INVARIANT VIOLATION');
429
+ }
430
+ assert(mintThrew, 'mint() should throw invariant violation');
431
+
432
+ let burnThrew = false;
433
+ try { pluginView.burn(); } catch (e) {
434
+ burnThrew = e.message.includes('INVARIANT VIOLATION');
435
+ }
436
+ assert(burnThrew, 'burn() should throw invariant violation');
437
+
438
+ let settleThrew = false;
439
+ try { pluginView.settle(); } catch (e) {
440
+ settleThrew = e.message.includes('INVARIANT VIOLATION');
441
+ }
442
+ assert(settleThrew, 'settle() should throw invariant violation');
443
+ });
444
+
445
+ test('Identity friction enforces activation delay', () => {
446
+ const friction = new IdentityFriction({
447
+ activationDelayMs: 100, // Short for testing
448
+ warmupTasks: 10,
449
+ });
450
+
451
+ // Register identity
452
+ friction.registerIdentity('new-node', 'test-public-key');
453
+
454
+ // Should not be able to execute immediately
455
+ const canExec = friction.canExecuteTasks('new-node');
456
+ assert(!canExec.allowed, 'New identity should not execute immediately');
457
+ assert(canExec.reason === 'Pending activation', 'Should be pending');
458
+ assert(canExec.remainingMs > 0, 'Should have remaining time');
459
+ });
460
+
461
+ test('Work verifier tracks submitted work', () => {
462
+ const verifier = new WorkVerifier();
463
+
464
+ const work = verifier.submitWork('task-1', 'node-1', { result: 'done' }, { proof: 'proof' });
465
+
466
+ assert(work.taskId === 'task-1', 'Should track task ID');
467
+ assert(work.status === 'pending', 'Should start pending');
468
+ assert(work.resultHash, 'Should hash result');
469
+ assert(work.challengeDeadline > Date.now(), 'Should have challenge window');
470
+ });
471
+
472
+ test('Degradation controller changes policy under load', () => {
473
+ const controller = new DegradationController({
474
+ warningLoadPercent: 70,
475
+ criticalLoadPercent: 90,
476
+ });
477
+
478
+ // Normal state
479
+ let policy = controller.getPolicy();
480
+ assertEqual(policy.level, 'normal', 'Should start normal');
481
+ assert(policy.acceptNewTasks, 'Should accept tasks');
482
+ assert(policy.pluginsEnabled, 'Plugins should be enabled');
483
+
484
+ // Update to high load
485
+ controller.updateMetrics({ cpuLoad: 95 });
486
+
487
+ policy = controller.getPolicy();
488
+ assertEqual(policy.level, 'degraded', 'Should be degraded at 95% load');
489
+ assert(!policy.pluginsEnabled, 'Plugins should be disabled under load');
490
+ });
491
+
492
+ test('Plugin loader provides economic boundary to sandbox', () => {
493
+ const loader = new PluginLoader();
494
+ const catalog = loader.getCatalog();
495
+
496
+ // Loader should have mock economic view
497
+ assert(catalog.length > 0, 'Should have plugins');
498
+
499
+ // Get stats with health
500
+ const stats = loader.getStats();
501
+ assert(stats.health, 'Stats should include health');
502
+ assertEqual(stats.health.totalPlugins, 0, 'No plugins tracked yet');
503
+ });
504
+
505
+ testAsync('Plugin loader isolates failures', async () => {
506
+ const loader = new PluginLoader({
507
+ maxRetries: 2,
508
+ executionTimeoutMs: 50,
509
+ });
510
+
511
+ // Load a plugin
512
+ await loader.load('compression');
513
+ const plugin = loader.get('compression');
514
+ assert(plugin, 'Plugin should load');
515
+
516
+ // Check health before failures
517
+ let health = loader.getHealth('compression');
518
+ assert(health.healthy, 'Should be healthy initially');
519
+
520
+ // Record failures to trigger circuit breaker
521
+ loader.failureContract.recordFailure('compression', new Error('Test failure 1'));
522
+ loader.failureContract.recordFailure('compression', new Error('Test failure 2'));
523
+
524
+ // Check health after failures
525
+ health = loader.getHealth('compression');
526
+ assert(!health.healthy, 'Should be unhealthy after failures');
527
+ assert(health.quarantine, 'Should be quarantined');
528
+ });
529
+
367
530
  // --- Summary ---
368
531
  console.log('\n╔════════════════════════════════════════════════════════════════╗');
369
532
  console.log('║ TEST SUMMARY ║');
@@ -0,0 +1,368 @@
1
+ /**
2
+ * WASM Core Tests
3
+ *
4
+ * Validates security fixes and core functionality:
5
+ * - Cryptographic randomness (no Math.random)
6
+ * - Ed25519 fail-closed behavior
7
+ * - Memory bounds
8
+ * - Genesis signing and verification
9
+ * - Lineage verification
10
+ */
11
+
12
+ import { describe, test, expect, beforeAll } from 'vitest';
13
+ import {
14
+ detectPlatform,
15
+ getPlatformCapabilities,
16
+ WasmCrypto,
17
+ WasmGenesis,
18
+ WasmInference,
19
+ } from '../models/wasm-core.js';
20
+
21
+ describe('Platform Detection', () => {
22
+ test('detects Node.js platform', () => {
23
+ const platform = detectPlatform();
24
+ expect(platform).toBe('node');
25
+ });
26
+
27
+ test('returns platform capabilities', () => {
28
+ const caps = getPlatformCapabilities();
29
+ expect(caps).toHaveProperty('platform');
30
+ expect(caps).toHaveProperty('hasWebAssembly');
31
+ expect(caps).toHaveProperty('hasWebCrypto');
32
+ expect(caps).toHaveProperty('maxMemory');
33
+ expect(typeof caps.maxMemory).toBe('number');
34
+ expect(caps.maxMemory).toBeGreaterThan(0);
35
+ });
36
+ });
37
+
38
+ describe('WasmCrypto', () => {
39
+ let crypto;
40
+
41
+ beforeAll(async () => {
42
+ crypto = new WasmCrypto();
43
+ await crypto.init();
44
+ });
45
+
46
+ test('computes SHA256 hash', async () => {
47
+ const hash = await crypto.sha256('hello world');
48
+ expect(hash).toBeInstanceOf(Uint8Array);
49
+ expect(hash.length).toBe(32);
50
+ });
51
+
52
+ test('computes SHA256 hex string', async () => {
53
+ const hashHex = await crypto.sha256Hex('hello world');
54
+ expect(typeof hashHex).toBe('string');
55
+ expect(hashHex.length).toBe(64);
56
+ // Known SHA256 of "hello world"
57
+ expect(hashHex).toBe('b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9');
58
+ });
59
+
60
+ test('canonicalizes objects deterministically', () => {
61
+ const obj1 = { b: 2, a: 1 };
62
+ const obj2 = { a: 1, b: 2 };
63
+ const canon1 = crypto.canonicalize(obj1);
64
+ const canon2 = crypto.canonicalize(obj2);
65
+ expect(canon1).toBe(canon2);
66
+ expect(canon1).toBe('{"a":1,"b":2}');
67
+ });
68
+
69
+ test('canonicalizes nested objects', () => {
70
+ const obj = { z: { b: 2, a: 1 }, y: [3, 2, 1] };
71
+ const canon = crypto.canonicalize(obj);
72
+ expect(canon).toBe('{"y":[3,2,1],"z":{"a":1,"b":2}}');
73
+ });
74
+
75
+ test('rejects Infinity and NaN', () => {
76
+ expect(() => crypto.canonicalize({ x: Infinity }))
77
+ .toThrow('Cannot canonicalize Infinity/NaN');
78
+ expect(() => crypto.canonicalize({ x: NaN }))
79
+ .toThrow('Cannot canonicalize Infinity/NaN');
80
+ });
81
+
82
+ test('computes hash of canonical object', async () => {
83
+ const hash = await crypto.hashCanonical({ test: 'value' });
84
+ expect(typeof hash).toBe('string');
85
+ expect(hash.length).toBe(64);
86
+ });
87
+
88
+ test('merkle root of single hash returns the hash', async () => {
89
+ const hash = await crypto.sha256('test');
90
+ const root = await crypto.merkleRoot([hash]);
91
+ // Single element should return itself (or same value)
92
+ expect(root).toBeInstanceOf(Uint8Array);
93
+ expect(root.length).toBe(32);
94
+ });
95
+ });
96
+
97
+ describe('WasmGenesis Security', () => {
98
+ let genesis;
99
+
100
+ beforeAll(async () => {
101
+ genesis = new WasmGenesis({
102
+ networkName: 'test-net',
103
+ version: '1.0.0',
104
+ });
105
+ await genesis.init();
106
+ });
107
+
108
+ test('generates cryptographically random network IDs', async () => {
109
+ const ids = new Set();
110
+ for (let i = 0; i < 10; i++) {
111
+ const id = await genesis._generateNetworkId(Date.now());
112
+ expect(id).toMatch(/^net_[a-f0-9]{16}$/);
113
+ ids.add(id);
114
+ }
115
+ // All IDs should be unique
116
+ expect(ids.size).toBe(10);
117
+ });
118
+
119
+ test('births network with Ed25519 keypair', async () => {
120
+ const result = await genesis.birthNetwork({
121
+ traits: { test: true },
122
+ });
123
+
124
+ expect(result).toHaveProperty('networkId');
125
+ expect(result).toHaveProperty('manifest');
126
+ expect(result).toHaveProperty('genesisHash');
127
+ expect(result).toHaveProperty('signature');
128
+ expect(result).toHaveProperty('publicKey');
129
+
130
+ // Verify signature format (128 hex chars = 64 bytes)
131
+ expect(result.signature.length).toBe(128);
132
+ // Verify public key format (64 hex chars = 32 bytes)
133
+ expect(result.publicKey.length).toBe(64);
134
+ });
135
+
136
+ test('manifest includes cryptographic signature', async () => {
137
+ const result = await genesis.birthNetwork();
138
+ const manifest = result.manifest;
139
+
140
+ expect(manifest.integrity).toHaveProperty('signature');
141
+ expect(manifest.integrity).toHaveProperty('signatureAlgorithm', 'Ed25519');
142
+ expect(manifest.integrity).toHaveProperty('genesisHash');
143
+ expect(manifest.genesis).toHaveProperty('publicKey');
144
+ expect(manifest.genesis).toHaveProperty('keyAlgorithm', 'Ed25519');
145
+ });
146
+
147
+ test('verifies valid genesis signature', async () => {
148
+ const result = await genesis.birthNetwork();
149
+ const verification = await genesis.verifyGenesis(result.manifest);
150
+
151
+ expect(verification.valid).toBe(true);
152
+ expect(verification.genesisHash).toBe(result.genesisHash);
153
+ });
154
+
155
+ test('rejects tampered genesis', async () => {
156
+ const result = await genesis.birthNetwork();
157
+ const manifest = JSON.parse(JSON.stringify(result.manifest));
158
+
159
+ // Tamper with the genesis
160
+ manifest.genesis.traits.hacked = true;
161
+
162
+ const verification = await genesis.verifyGenesis(manifest);
163
+ expect(verification.valid).toBe(false);
164
+ expect(verification.error).toMatch(/hash mismatch/i);
165
+ });
166
+
167
+ test('rejects missing signature', async () => {
168
+ const result = await genesis.birthNetwork();
169
+ const manifest = JSON.parse(JSON.stringify(result.manifest));
170
+
171
+ // Remove signature
172
+ delete manifest.integrity.signature;
173
+
174
+ const verification = await genesis.verifyGenesis(manifest);
175
+ expect(verification.valid).toBe(false);
176
+ expect(verification.error).toMatch(/Missing.*signature/i);
177
+ });
178
+ });
179
+
180
+ describe('WasmGenesis Lineage', () => {
181
+ let genesis;
182
+
183
+ beforeAll(async () => {
184
+ genesis = new WasmGenesis({ networkName: 'lineage-test' });
185
+ await genesis.init();
186
+ });
187
+
188
+ test('verifies root network (no parent)', async () => {
189
+ const result = await genesis.birthNetwork();
190
+ const verification = await genesis.verifyLineage(result.manifest);
191
+
192
+ expect(verification.valid).toBe(true);
193
+ expect(verification.isRoot).toBe(true);
194
+ expect(verification.genesisVerified).toBe(true);
195
+ });
196
+
197
+ test('reproduces with lineage tracking', async () => {
198
+ const parent = await genesis.birthNetwork({
199
+ traits: { generation: 0, fitness: 1.0 },
200
+ });
201
+
202
+ const child = await genesis.reproduce(parent.manifest, {
203
+ mutationRate: 0.1,
204
+ });
205
+
206
+ expect(child.manifest.lineage).toBeDefined();
207
+ expect(child.manifest.lineage.parentId).toBe(parent.manifest.genesis.networkId);
208
+ expect(child.manifest.lineage.generation).toBe(1);
209
+ });
210
+
211
+ test('verifies valid lineage chain', async () => {
212
+ const parent = await genesis.birthNetwork();
213
+ const child = await genesis.reproduce(parent.manifest);
214
+
215
+ const verification = await genesis.verifyLineage(child.manifest, parent.manifest);
216
+
217
+ expect(verification.valid).toBe(true);
218
+ expect(verification.genesisVerified).toBe(true);
219
+ expect(verification.parentVerified).toBe(true);
220
+ expect(verification.parentId).toBe(parent.manifest.genesis.networkId);
221
+ });
222
+
223
+ test('rejects mismatched parent ID', async () => {
224
+ const parent = await genesis.birthNetwork();
225
+ const fakeParent = await genesis.birthNetwork();
226
+ const child = await genesis.reproduce(parent.manifest);
227
+
228
+ // Try to verify with wrong parent
229
+ const verification = await genesis.verifyLineage(child.manifest, fakeParent.manifest);
230
+
231
+ expect(verification.valid).toBe(false);
232
+ expect(verification.error).toMatch(/Parent ID mismatch/i);
233
+ });
234
+
235
+ test('rejects broken generation sequence', async () => {
236
+ const parent = await genesis.birthNetwork();
237
+ const child = await genesis.reproduce(parent.manifest);
238
+
239
+ // Tamper with generation
240
+ const tamperedManifest = JSON.parse(JSON.stringify(child.manifest));
241
+ tamperedManifest.lineage.generation = 5;
242
+
243
+ // Re-sign would be needed for real verification
244
+ // But the generation check should fail first
245
+ const verification = await genesis.verifyLineage(tamperedManifest, parent.manifest);
246
+
247
+ // Should fail on genesis hash mismatch since we tampered
248
+ expect(verification.valid).toBe(false);
249
+ });
250
+ });
251
+
252
+ describe('WasmInference', () => {
253
+ let inference;
254
+
255
+ beforeAll(async () => {
256
+ inference = new WasmInference();
257
+ await inference.init();
258
+ });
259
+
260
+ test('initializes with platform capabilities', async () => {
261
+ expect(inference.ready).toBe(true);
262
+ });
263
+
264
+ test('loads model with correct hash path', async () => {
265
+ const modelData = new Uint8Array([1, 2, 3, 4, 5]);
266
+ const manifest = {
267
+ artifacts: {
268
+ model: {
269
+ sha256: await inference.crypto.sha256Hex(modelData),
270
+ },
271
+ },
272
+ };
273
+
274
+ await inference.loadModel(modelData, manifest);
275
+ expect(inference.model).toBeDefined();
276
+ expect(inference.model.manifest).toBe(manifest);
277
+ });
278
+
279
+ test('rejects model with hash mismatch', async () => {
280
+ const modelData = new Uint8Array([1, 2, 3, 4, 5]);
281
+ const manifest = {
282
+ artifacts: {
283
+ model: {
284
+ sha256: 'wrong_hash_value_here',
285
+ },
286
+ },
287
+ };
288
+
289
+ await expect(inference.loadModel(modelData, manifest))
290
+ .rejects.toThrow(/hash mismatch/i);
291
+ });
292
+
293
+ test('supports legacy artifact format', async () => {
294
+ const modelData = new Uint8Array([1, 2, 3, 4, 5]);
295
+ const hash = await inference.crypto.sha256Hex(modelData);
296
+ const manifest = {
297
+ artifacts: [{ sha256: hash }],
298
+ };
299
+
300
+ await inference.loadModel(modelData, manifest);
301
+ expect(inference.model).toBeDefined();
302
+ });
303
+ });
304
+
305
+ describe('Security: No Math.random', () => {
306
+ test('source code does not use Math.random for security', async () => {
307
+ const fs = await import('fs/promises');
308
+ const source = await fs.readFile(
309
+ new URL('../models/wasm-core.js', import.meta.url),
310
+ 'utf-8'
311
+ );
312
+
313
+ // Find all Math.random occurrences
314
+ const mathRandomUsage = source.match(/Math\.random\(\)/g) || [];
315
+
316
+ // Math.random should NOT be used for security-critical operations
317
+ // Check that any usage is clearly documented as non-security
318
+ if (mathRandomUsage.length > 0) {
319
+ // Verify they're only in non-security contexts (placeholder code)
320
+ const lines = source.split('\n');
321
+ for (let i = 0; i < lines.length; i++) {
322
+ if (lines[i].includes('Math.random()')) {
323
+ // Check if this line is commented or in placeholder section
324
+ const context = lines.slice(Math.max(0, i - 5), i + 1).join('\n');
325
+ expect(context).not.toMatch(/_generateNetworkId/);
326
+ expect(context).not.toMatch(/sign/i);
327
+ expect(context).not.toMatch(/key/i);
328
+ }
329
+ }
330
+ }
331
+ });
332
+ });
333
+
334
+ describe('Security: Fail Closed', () => {
335
+ test('Ed25519 fallback throws instead of returning zeros', async () => {
336
+ // The JS fallback should throw when Ed25519 is unavailable
337
+ // We can't easily test this without mocking crypto.subtle
338
+ // but we can verify the code pattern exists
339
+ const fs = await import('fs/promises');
340
+ const source = await fs.readFile(
341
+ new URL('../models/wasm-core.js', import.meta.url),
342
+ 'utf-8'
343
+ );
344
+
345
+ // Verify fail-closed pattern exists
346
+ expect(source).toContain('FAIL CLOSED');
347
+ expect(source).toContain('throw new Error');
348
+ expect(source).not.toContain('returning mock signature');
349
+ });
350
+ });
351
+
352
+ describe('Memory Bounds', () => {
353
+ test('WASM memory is reasonably bounded', async () => {
354
+ const fs = await import('fs/promises');
355
+ const source = await fs.readFile(
356
+ new URL('../models/wasm-core.js', import.meta.url),
357
+ 'utf-8'
358
+ );
359
+
360
+ // Check that memory max is not 65536 (4GB)
361
+ const memoryMatch = source.match(/maximum:\s*(\d+)/);
362
+ if (memoryMatch) {
363
+ const maxPages = parseInt(memoryMatch[1], 10);
364
+ // Should be <= 1024 pages (64MB) for edge platforms
365
+ expect(maxPages).toBeLessThanOrEqual(1024);
366
+ }
367
+ });
368
+ });