@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.
- package/package.json +1 -1
- package/src/index.js +307 -0
package/package.json
CHANGED
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
|