@moltlaunch/sdk 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +307 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moltlaunch/sdk",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "MoltLaunch SDK - On-chain AI verification for AI agents",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
package/src/index.js CHANGED
@@ -484,6 +484,313 @@ class MoltLaunch {
484
484
  };
485
485
  return costs[proofType] || costs.threshold;
486
486
  }
487
+
488
+ // ==========================================
489
+ // HARDWARE-ANCHORED IDENTITY (Anti-Sybil)
490
+ // ==========================================
491
+
492
+ /**
493
+ * Collect environment fingerprint for hardware-anchored identity
494
+ * @returns {object} Raw fingerprint data
495
+ * @private
496
+ */
497
+ _collectFingerprint() {
498
+ const crypto = require('crypto');
499
+ const os = require('os');
500
+
501
+ const hardware = {
502
+ platform: os.platform(),
503
+ arch: os.arch(),
504
+ cpus: os.cpus().length,
505
+ cpuModel: os.cpus()[0]?.model || 'unknown',
506
+ totalMemory: os.totalmem(),
507
+ hostname: os.hostname(),
508
+ };
509
+
510
+ const runtime = {
511
+ nodeVersion: process.version,
512
+ pid: process.pid,
513
+ execPath: process.execPath,
514
+ cwd: process.cwd(),
515
+ env: {
516
+ USER: process.env.USER || process.env.USERNAME || 'unknown',
517
+ HOME: process.env.HOME || process.env.USERPROFILE || 'unknown',
518
+ SHELL: process.env.SHELL || 'unknown',
519
+ }
520
+ };
521
+
522
+ // Try to get network interfaces for fingerprinting
523
+ const nets = os.networkInterfaces();
524
+ const networkFingerprint = Object.keys(nets).sort().map(name => {
525
+ const iface = nets[name].find(n => !n.internal && n.family === 'IPv4');
526
+ return iface ? `${name}:${iface.mac}` : null;
527
+ }).filter(Boolean).join('|');
528
+
529
+ return { hardware, runtime, networkFingerprint };
530
+ }
531
+
532
+ /**
533
+ * Generate a hardware-anchored identity hash
534
+ * Combines hardware, runtime, code, and network fingerprints into a deterministic identity
535
+ *
536
+ * @param {object} options - Identity options
537
+ * @param {boolean} [options.includeHardware=true] - Include hardware fingerprint (CPU, memory)
538
+ * @param {boolean} [options.includeRuntime=true] - Include runtime fingerprint (Node version, OS)
539
+ * @param {boolean} [options.includeCode=false] - Include code hash (hash of main module)
540
+ * @param {string} [options.codeEntry] - Path to agent's main module for code hashing
541
+ * @param {string} [options.agentId] - Agent ID to bind identity to
542
+ * @param {boolean} [options.anchor=false] - Anchor identity on Solana
543
+ * @returns {Promise<IdentityResult>}
544
+ */
545
+ async generateIdentity(options = {}) {
546
+ const crypto = require('crypto');
547
+ const {
548
+ includeHardware = true,
549
+ includeRuntime = true,
550
+ includeCode = false,
551
+ codeEntry,
552
+ agentId,
553
+ anchor = false
554
+ } = options;
555
+
556
+ const fingerprint = this._collectFingerprint();
557
+ const components = [];
558
+
559
+ if (includeHardware) {
560
+ const hwHash = crypto.createHash('sha256')
561
+ .update(JSON.stringify(fingerprint.hardware))
562
+ .digest('hex');
563
+ components.push(`hw:${hwHash}`);
564
+ }
565
+
566
+ if (includeRuntime) {
567
+ const rtHash = crypto.createHash('sha256')
568
+ .update(JSON.stringify(fingerprint.runtime))
569
+ .digest('hex');
570
+ components.push(`rt:${rtHash}`);
571
+ }
572
+
573
+ if (includeCode && codeEntry) {
574
+ try {
575
+ const fs = require('fs');
576
+ const codeContent = fs.readFileSync(codeEntry, 'utf-8');
577
+ const codeHash = crypto.createHash('sha256')
578
+ .update(codeContent)
579
+ .digest('hex');
580
+ components.push(`code:${codeHash}`);
581
+ } catch (e) {
582
+ components.push(`code:unavailable`);
583
+ }
584
+ }
585
+
586
+ if (fingerprint.networkFingerprint) {
587
+ const netHash = crypto.createHash('sha256')
588
+ .update(fingerprint.networkFingerprint)
589
+ .digest('hex');
590
+ components.push(`net:${netHash}`);
591
+ }
592
+
593
+ // Generate deterministic identity hash
594
+ const identityHash = crypto.createHash('sha256')
595
+ .update(components.join('|'))
596
+ .digest('hex');
597
+
598
+ const identity = {
599
+ hash: identityHash,
600
+ components: components.length,
601
+ includesHardware: includeHardware,
602
+ includesRuntime: includeRuntime,
603
+ includesCode: includeCode && !!codeEntry,
604
+ includesNetwork: !!fingerprint.networkFingerprint,
605
+ generatedAt: new Date().toISOString(),
606
+ expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days
607
+ agentId: agentId || null
608
+ };
609
+
610
+ // Register with MoltLaunch API
611
+ if (agentId) {
612
+ try {
613
+ const res = await fetch(`${this.baseUrl}/api/identity/register`, {
614
+ method: 'POST',
615
+ headers: {
616
+ 'Content-Type': 'application/json',
617
+ ...(this.apiKey && { 'Authorization': `Bearer ${this.apiKey}` })
618
+ },
619
+ body: JSON.stringify({
620
+ agentId,
621
+ identityHash,
622
+ components: components.length,
623
+ includesHardware: includeHardware,
624
+ includesCode: includeCode
625
+ })
626
+ });
627
+
628
+ if (res.ok) {
629
+ const registration = await res.json();
630
+ identity.registered = true;
631
+ identity.registrationId = registration.registrationId;
632
+ } else {
633
+ identity.registered = false;
634
+ identity.registrationError = `API ${res.status}`;
635
+ }
636
+ } catch (e) {
637
+ identity.registered = false;
638
+ identity.registrationError = e.message;
639
+ }
640
+ }
641
+
642
+ // Anchor on Solana if requested
643
+ if (anchor && agentId) {
644
+ try {
645
+ const res = await fetch(`${this.baseUrl}/api/anchor/verification`, {
646
+ method: 'POST',
647
+ headers: { 'Content-Type': 'application/json' },
648
+ body: JSON.stringify({
649
+ agentId,
650
+ attestationHash: identityHash
651
+ })
652
+ });
653
+
654
+ if (res.ok) {
655
+ const anchorResult = await res.json();
656
+ identity.anchored = true;
657
+ identity.anchorSignature = anchorResult.signature;
658
+ identity.anchorExplorer = anchorResult.explorer;
659
+ } else {
660
+ identity.anchored = false;
661
+ }
662
+ } catch (e) {
663
+ identity.anchored = false;
664
+ identity.anchorError = e.message;
665
+ }
666
+ }
667
+
668
+ return identity;
669
+ }
670
+
671
+ /**
672
+ * Verify an agent's identity against their registered fingerprint
673
+ * @param {string} agentId - Agent ID to verify
674
+ * @returns {Promise<IdentityVerification>}
675
+ */
676
+ async verifyIdentity(agentId) {
677
+ // Generate current fingerprint
678
+ const currentIdentity = await this.generateIdentity({ agentId });
679
+
680
+ // Check against registered identity
681
+ try {
682
+ const res = await fetch(`${this.baseUrl}/api/identity/${encodeURIComponent(agentId)}`);
683
+ if (!res.ok) {
684
+ return {
685
+ valid: false,
686
+ agentId,
687
+ reason: 'No registered identity found',
688
+ currentHash: currentIdentity.hash
689
+ };
690
+ }
691
+
692
+ const registered = await res.json();
693
+ const matches = registered.identityHash === currentIdentity.hash;
694
+
695
+ return {
696
+ valid: matches,
697
+ agentId,
698
+ currentHash: currentIdentity.hash,
699
+ registeredHash: registered.identityHash,
700
+ match: matches,
701
+ registeredAt: registered.registeredAt,
702
+ reason: matches ? 'Identity confirmed' : 'Identity mismatch — different hardware or code'
703
+ };
704
+ } catch (e) {
705
+ return {
706
+ valid: false,
707
+ agentId,
708
+ reason: e.message,
709
+ currentHash: currentIdentity.hash
710
+ };
711
+ }
712
+ }
713
+
714
+ /**
715
+ * Check if two agents have the same hardware fingerprint (Sybil detection)
716
+ * @param {string} agentId1 - First agent
717
+ * @param {string} agentId2 - Second agent
718
+ * @returns {Promise<SybilCheck>}
719
+ */
720
+ async checkSybil(agentId1, agentId2) {
721
+ try {
722
+ const [id1, id2] = await Promise.all([
723
+ fetch(`${this.baseUrl}/api/identity/${encodeURIComponent(agentId1)}`).then(r => r.json()),
724
+ fetch(`${this.baseUrl}/api/identity/${encodeURIComponent(agentId2)}`).then(r => r.json())
725
+ ]);
726
+
727
+ const sameIdentity = id1.identityHash === id2.identityHash;
728
+
729
+ return {
730
+ agentId1,
731
+ agentId2,
732
+ sameIdentity,
733
+ sybilRisk: sameIdentity ? 'HIGH' : 'LOW',
734
+ reason: sameIdentity
735
+ ? 'Same hardware fingerprint — likely same operator'
736
+ : 'Different hardware fingerprints — likely different operators',
737
+ recommendation: sameIdentity
738
+ ? 'Do not seat at same table'
739
+ : 'Safe to interact'
740
+ };
741
+ } catch (e) {
742
+ return {
743
+ agentId1,
744
+ agentId2,
745
+ sameIdentity: null,
746
+ sybilRisk: 'UNKNOWN',
747
+ reason: `Could not compare: ${e.message}`
748
+ };
749
+ }
750
+ }
751
+
752
+ /**
753
+ * Check a list of agents for Sybil clusters (table seating check)
754
+ * @param {string[]} agentIds - List of agent IDs to check
755
+ * @returns {Promise<SybilTableCheck>}
756
+ */
757
+ async checkTableSybils(agentIds) {
758
+ // Fetch all identities
759
+ const identities = {};
760
+ for (const id of agentIds) {
761
+ try {
762
+ const res = await fetch(`${this.baseUrl}/api/identity/${encodeURIComponent(id)}`);
763
+ if (res.ok) {
764
+ const data = await res.json();
765
+ identities[id] = data.identityHash;
766
+ }
767
+ } catch {
768
+ // Skip agents without identity
769
+ }
770
+ }
771
+
772
+ // Find clusters (same identity hash)
773
+ const hashToAgents = {};
774
+ for (const [agentId, hash] of Object.entries(identities)) {
775
+ if (!hashToAgents[hash]) hashToAgents[hash] = [];
776
+ hashToAgents[hash].push(agentId);
777
+ }
778
+
779
+ const clusters = Object.values(hashToAgents).filter(group => group.length > 1);
780
+ const flagged = clusters.flat();
781
+
782
+ return {
783
+ totalAgents: agentIds.length,
784
+ identifiedAgents: Object.keys(identities).length,
785
+ unidentifiedAgents: agentIds.filter(id => !identities[id]),
786
+ sybilClusters: clusters,
787
+ flaggedAgents: flagged,
788
+ safe: clusters.length === 0,
789
+ recommendation: clusters.length === 0
790
+ ? 'No Sybil clusters detected — safe to proceed'
791
+ : `${clusters.length} Sybil cluster(s) detected — ${flagged.length} agents share hardware`
792
+ };
793
+ }
487
794
  }
488
795
 
489
796
  // Scoring helpers