@moltlaunch/sdk 2.1.0 → 2.3.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 +427 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -484,6 +484,433 @@ class MoltLaunch {
|
|
|
484
484
|
};
|
|
485
485
|
return costs[proofType] || costs.threshold;
|
|
486
486
|
}
|
|
487
|
+
|
|
488
|
+
// ==========================================
|
|
489
|
+
// HARDWARE-ANCHORED IDENTITY (Anti-Sybil)
|
|
490
|
+
// DePIN-Rooted Device Identity
|
|
491
|
+
// ==========================================
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Collect environment fingerprint for hardware-anchored identity
|
|
495
|
+
* @returns {object} Raw fingerprint data
|
|
496
|
+
* @private
|
|
497
|
+
*/
|
|
498
|
+
_collectFingerprint() {
|
|
499
|
+
const crypto = require('crypto');
|
|
500
|
+
const os = require('os');
|
|
501
|
+
|
|
502
|
+
const hardware = {
|
|
503
|
+
platform: os.platform(),
|
|
504
|
+
arch: os.arch(),
|
|
505
|
+
cpus: os.cpus().length,
|
|
506
|
+
cpuModel: os.cpus()[0]?.model || 'unknown',
|
|
507
|
+
totalMemory: os.totalmem(),
|
|
508
|
+
hostname: os.hostname(),
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const runtime = {
|
|
512
|
+
nodeVersion: process.version,
|
|
513
|
+
pid: process.pid,
|
|
514
|
+
execPath: process.execPath,
|
|
515
|
+
cwd: process.cwd(),
|
|
516
|
+
env: {
|
|
517
|
+
USER: process.env.USER || process.env.USERNAME || 'unknown',
|
|
518
|
+
HOME: process.env.HOME || process.env.USERPROFILE || 'unknown',
|
|
519
|
+
SHELL: process.env.SHELL || 'unknown',
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// Try to get network interfaces for fingerprinting
|
|
524
|
+
const nets = os.networkInterfaces();
|
|
525
|
+
const networkFingerprint = Object.keys(nets).sort().map(name => {
|
|
526
|
+
const iface = nets[name].find(n => !n.internal && n.family === 'IPv4');
|
|
527
|
+
return iface ? `${name}:${iface.mac}` : null;
|
|
528
|
+
}).filter(Boolean).join('|');
|
|
529
|
+
|
|
530
|
+
return { hardware, runtime, networkFingerprint };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Try to read TPM endorsement key hash for hardware-rooted identity
|
|
535
|
+
* @returns {string|null} SHA-256 hash of TPM data, or null if unavailable
|
|
536
|
+
* @private
|
|
537
|
+
*/
|
|
538
|
+
_getTPMFingerprint() {
|
|
539
|
+
const crypto = require('crypto');
|
|
540
|
+
const fs = require('fs');
|
|
541
|
+
const os = require('os');
|
|
542
|
+
|
|
543
|
+
// TPM 2.0 paths (Linux)
|
|
544
|
+
const tpmPaths = [
|
|
545
|
+
'/sys/class/tpm/tpm0/device/description',
|
|
546
|
+
'/sys/class/tpm/tpm0/tpm_version_major',
|
|
547
|
+
'/sys/class/dmi/id/board_serial',
|
|
548
|
+
'/sys/class/dmi/id/product_uuid',
|
|
549
|
+
'/sys/class/dmi/id/chassis_serial',
|
|
550
|
+
];
|
|
551
|
+
|
|
552
|
+
const tpmData = [];
|
|
553
|
+
for (const p of tpmPaths) {
|
|
554
|
+
try {
|
|
555
|
+
const data = fs.readFileSync(p, 'utf-8').trim();
|
|
556
|
+
if (data && data !== 'None' && data !== '') {
|
|
557
|
+
tpmData.push(data);
|
|
558
|
+
}
|
|
559
|
+
} catch {}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// macOS: use IOPlatformUUID
|
|
563
|
+
if (os.platform() === 'darwin') {
|
|
564
|
+
try {
|
|
565
|
+
const { execSync } = require('child_process');
|
|
566
|
+
const uuid = execSync('ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID', { encoding: 'utf-8' });
|
|
567
|
+
const match = uuid.match(/"([A-F0-9-]+)"/);
|
|
568
|
+
if (match) tpmData.push(match[1]);
|
|
569
|
+
} catch {}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (tpmData.length === 0) return null;
|
|
573
|
+
|
|
574
|
+
return crypto.createHash('sha256')
|
|
575
|
+
.update(tpmData.join('|'))
|
|
576
|
+
.digest('hex');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Register a DePIN device attestation for hardware-rooted identity
|
|
581
|
+
* Links agent identity to a physically verified DePIN device
|
|
582
|
+
*
|
|
583
|
+
* @param {object} options - DePIN registration options
|
|
584
|
+
* @param {string} options.provider - DePIN provider name (e.g., 'io.net', 'akash', 'render')
|
|
585
|
+
* @param {string} options.deviceId - Device ID from the DePIN provider
|
|
586
|
+
* @param {string} [options.attestation] - Optional attestation data from the provider
|
|
587
|
+
* @param {string} options.agentId - Agent ID to bind DePIN identity to
|
|
588
|
+
* @returns {Promise<DePINRegistrationResult>}
|
|
589
|
+
*/
|
|
590
|
+
async registerDePINDevice(options = {}) {
|
|
591
|
+
const { provider, deviceId, attestation, agentId } = options;
|
|
592
|
+
|
|
593
|
+
const supported = ['io.net', 'akash', 'render', 'helium', 'hivemapper', 'nosana'];
|
|
594
|
+
|
|
595
|
+
if (!supported.includes(provider)) {
|
|
596
|
+
throw new Error(`Unsupported DePIN provider. Supported: ${supported.join(', ')}`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const res = await fetch(`${this.baseUrl}/api/identity/depin`, {
|
|
600
|
+
method: 'POST',
|
|
601
|
+
headers: { 'Content-Type': 'application/json' },
|
|
602
|
+
body: JSON.stringify({
|
|
603
|
+
agentId,
|
|
604
|
+
depinProvider: provider,
|
|
605
|
+
deviceId,
|
|
606
|
+
attestation,
|
|
607
|
+
timestamp: Date.now()
|
|
608
|
+
})
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
if (!res.ok) throw new Error(`DePIN registration failed: ${res.status}`);
|
|
612
|
+
return res.json();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Get identity trust report for an agent
|
|
617
|
+
* Shows trust ladder breakdown including DePIN and TPM attestation levels
|
|
618
|
+
*
|
|
619
|
+
* @param {string} agentId - Agent ID to get report for
|
|
620
|
+
* @returns {Promise<IdentityReport>}
|
|
621
|
+
*/
|
|
622
|
+
async getIdentityReport(agentId) {
|
|
623
|
+
const res = await fetch(`${this.baseUrl}/api/identity/${encodeURIComponent(agentId)}/report`);
|
|
624
|
+
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
625
|
+
return res.json();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Generate a hardware-anchored identity hash
|
|
630
|
+
* Combines hardware, runtime, code, and network fingerprints into a deterministic identity
|
|
631
|
+
*
|
|
632
|
+
* @param {object} options - Identity options
|
|
633
|
+
* @param {boolean} [options.includeHardware=true] - Include hardware fingerprint (CPU, memory)
|
|
634
|
+
* @param {boolean} [options.includeRuntime=true] - Include runtime fingerprint (Node version, OS)
|
|
635
|
+
* @param {boolean} [options.includeCode=false] - Include code hash (hash of main module)
|
|
636
|
+
* @param {string} [options.codeEntry] - Path to agent's main module for code hashing
|
|
637
|
+
* @param {string} [options.agentId] - Agent ID to bind identity to
|
|
638
|
+
* @param {boolean} [options.anchor=false] - Anchor identity on Solana
|
|
639
|
+
* @returns {Promise<IdentityResult>}
|
|
640
|
+
*/
|
|
641
|
+
async generateIdentity(options = {}) {
|
|
642
|
+
const crypto = require('crypto');
|
|
643
|
+
const {
|
|
644
|
+
includeHardware = true,
|
|
645
|
+
includeRuntime = true,
|
|
646
|
+
includeCode = false,
|
|
647
|
+
includeTPM = false,
|
|
648
|
+
depinProvider,
|
|
649
|
+
depinDeviceId,
|
|
650
|
+
codeEntry,
|
|
651
|
+
agentId,
|
|
652
|
+
anchor = false
|
|
653
|
+
} = options;
|
|
654
|
+
|
|
655
|
+
const fingerprint = this._collectFingerprint();
|
|
656
|
+
const components = [];
|
|
657
|
+
|
|
658
|
+
if (includeHardware) {
|
|
659
|
+
const hwHash = crypto.createHash('sha256')
|
|
660
|
+
.update(JSON.stringify(fingerprint.hardware))
|
|
661
|
+
.digest('hex');
|
|
662
|
+
components.push(`hw:${hwHash}`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (includeRuntime) {
|
|
666
|
+
const rtHash = crypto.createHash('sha256')
|
|
667
|
+
.update(JSON.stringify(fingerprint.runtime))
|
|
668
|
+
.digest('hex');
|
|
669
|
+
components.push(`rt:${rtHash}`);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (includeCode && codeEntry) {
|
|
673
|
+
try {
|
|
674
|
+
const fs = require('fs');
|
|
675
|
+
const codeContent = fs.readFileSync(codeEntry, 'utf-8');
|
|
676
|
+
const codeHash = crypto.createHash('sha256')
|
|
677
|
+
.update(codeContent)
|
|
678
|
+
.digest('hex');
|
|
679
|
+
components.push(`code:${codeHash}`);
|
|
680
|
+
} catch (e) {
|
|
681
|
+
components.push(`code:unavailable`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (fingerprint.networkFingerprint) {
|
|
686
|
+
const netHash = crypto.createHash('sha256')
|
|
687
|
+
.update(fingerprint.networkFingerprint)
|
|
688
|
+
.digest('hex');
|
|
689
|
+
components.push(`net:${netHash}`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// TPM attestation (hardware-rooted identity - trust level 4)
|
|
693
|
+
let tpmHash = null;
|
|
694
|
+
if (includeTPM) {
|
|
695
|
+
tpmHash = this._getTPMFingerprint();
|
|
696
|
+
if (tpmHash) {
|
|
697
|
+
components.push(`tpm:${tpmHash}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// DePIN device attestation (highest trust level 5)
|
|
702
|
+
if (depinProvider && depinDeviceId) {
|
|
703
|
+
const depinHash = crypto.createHash('sha256')
|
|
704
|
+
.update(`depin:${depinProvider}:${depinDeviceId}`)
|
|
705
|
+
.digest('hex');
|
|
706
|
+
components.push(`depin:${depinHash}`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Generate deterministic identity hash
|
|
710
|
+
const identityHash = crypto.createHash('sha256')
|
|
711
|
+
.update(components.join('|'))
|
|
712
|
+
.digest('hex');
|
|
713
|
+
|
|
714
|
+
const identity = {
|
|
715
|
+
hash: identityHash,
|
|
716
|
+
components: components.length,
|
|
717
|
+
includesHardware: includeHardware,
|
|
718
|
+
includesRuntime: includeRuntime,
|
|
719
|
+
includesCode: includeCode && !!codeEntry,
|
|
720
|
+
includesNetwork: !!fingerprint.networkFingerprint,
|
|
721
|
+
includesTPM: includeTPM && !!tpmHash,
|
|
722
|
+
tpmHash: tpmHash || null,
|
|
723
|
+
depinProvider: depinProvider || null,
|
|
724
|
+
depinDeviceId: depinDeviceId || null,
|
|
725
|
+
generatedAt: new Date().toISOString(),
|
|
726
|
+
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days
|
|
727
|
+
agentId: agentId || null
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// Register with MoltLaunch API
|
|
731
|
+
if (agentId) {
|
|
732
|
+
try {
|
|
733
|
+
const res = await fetch(`${this.baseUrl}/api/identity/register`, {
|
|
734
|
+
method: 'POST',
|
|
735
|
+
headers: {
|
|
736
|
+
'Content-Type': 'application/json',
|
|
737
|
+
...(this.apiKey && { 'Authorization': `Bearer ${this.apiKey}` })
|
|
738
|
+
},
|
|
739
|
+
body: JSON.stringify({
|
|
740
|
+
agentId,
|
|
741
|
+
identityHash,
|
|
742
|
+
components: components.length,
|
|
743
|
+
includesHardware: includeHardware,
|
|
744
|
+
includesCode: includeCode
|
|
745
|
+
})
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
if (res.ok) {
|
|
749
|
+
const registration = await res.json();
|
|
750
|
+
identity.registered = true;
|
|
751
|
+
identity.registrationId = registration.registrationId;
|
|
752
|
+
} else {
|
|
753
|
+
identity.registered = false;
|
|
754
|
+
identity.registrationError = `API ${res.status}`;
|
|
755
|
+
}
|
|
756
|
+
} catch (e) {
|
|
757
|
+
identity.registered = false;
|
|
758
|
+
identity.registrationError = e.message;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Anchor on Solana if requested
|
|
763
|
+
if (anchor && agentId) {
|
|
764
|
+
try {
|
|
765
|
+
const res = await fetch(`${this.baseUrl}/api/anchor/verification`, {
|
|
766
|
+
method: 'POST',
|
|
767
|
+
headers: { 'Content-Type': 'application/json' },
|
|
768
|
+
body: JSON.stringify({
|
|
769
|
+
agentId,
|
|
770
|
+
attestationHash: identityHash
|
|
771
|
+
})
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
if (res.ok) {
|
|
775
|
+
const anchorResult = await res.json();
|
|
776
|
+
identity.anchored = true;
|
|
777
|
+
identity.anchorSignature = anchorResult.signature;
|
|
778
|
+
identity.anchorExplorer = anchorResult.explorer;
|
|
779
|
+
} else {
|
|
780
|
+
identity.anchored = false;
|
|
781
|
+
}
|
|
782
|
+
} catch (e) {
|
|
783
|
+
identity.anchored = false;
|
|
784
|
+
identity.anchorError = e.message;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return identity;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Verify an agent's identity against their registered fingerprint
|
|
793
|
+
* @param {string} agentId - Agent ID to verify
|
|
794
|
+
* @returns {Promise<IdentityVerification>}
|
|
795
|
+
*/
|
|
796
|
+
async verifyIdentity(agentId) {
|
|
797
|
+
// Generate current fingerprint
|
|
798
|
+
const currentIdentity = await this.generateIdentity({ agentId });
|
|
799
|
+
|
|
800
|
+
// Check against registered identity
|
|
801
|
+
try {
|
|
802
|
+
const res = await fetch(`${this.baseUrl}/api/identity/${encodeURIComponent(agentId)}`);
|
|
803
|
+
if (!res.ok) {
|
|
804
|
+
return {
|
|
805
|
+
valid: false,
|
|
806
|
+
agentId,
|
|
807
|
+
reason: 'No registered identity found',
|
|
808
|
+
currentHash: currentIdentity.hash
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const registered = await res.json();
|
|
813
|
+
const matches = registered.identityHash === currentIdentity.hash;
|
|
814
|
+
|
|
815
|
+
return {
|
|
816
|
+
valid: matches,
|
|
817
|
+
agentId,
|
|
818
|
+
currentHash: currentIdentity.hash,
|
|
819
|
+
registeredHash: registered.identityHash,
|
|
820
|
+
match: matches,
|
|
821
|
+
registeredAt: registered.registeredAt,
|
|
822
|
+
reason: matches ? 'Identity confirmed' : 'Identity mismatch — different hardware or code'
|
|
823
|
+
};
|
|
824
|
+
} catch (e) {
|
|
825
|
+
return {
|
|
826
|
+
valid: false,
|
|
827
|
+
agentId,
|
|
828
|
+
reason: e.message,
|
|
829
|
+
currentHash: currentIdentity.hash
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Check if two agents have the same hardware fingerprint (Sybil detection)
|
|
836
|
+
* @param {string} agentId1 - First agent
|
|
837
|
+
* @param {string} agentId2 - Second agent
|
|
838
|
+
* @returns {Promise<SybilCheck>}
|
|
839
|
+
*/
|
|
840
|
+
async checkSybil(agentId1, agentId2) {
|
|
841
|
+
try {
|
|
842
|
+
const [id1, id2] = await Promise.all([
|
|
843
|
+
fetch(`${this.baseUrl}/api/identity/${encodeURIComponent(agentId1)}`).then(r => r.json()),
|
|
844
|
+
fetch(`${this.baseUrl}/api/identity/${encodeURIComponent(agentId2)}`).then(r => r.json())
|
|
845
|
+
]);
|
|
846
|
+
|
|
847
|
+
const sameIdentity = id1.identityHash === id2.identityHash;
|
|
848
|
+
|
|
849
|
+
return {
|
|
850
|
+
agentId1,
|
|
851
|
+
agentId2,
|
|
852
|
+
sameIdentity,
|
|
853
|
+
sybilRisk: sameIdentity ? 'HIGH' : 'LOW',
|
|
854
|
+
reason: sameIdentity
|
|
855
|
+
? 'Same hardware fingerprint — likely same operator'
|
|
856
|
+
: 'Different hardware fingerprints — likely different operators',
|
|
857
|
+
recommendation: sameIdentity
|
|
858
|
+
? 'Do not seat at same table'
|
|
859
|
+
: 'Safe to interact'
|
|
860
|
+
};
|
|
861
|
+
} catch (e) {
|
|
862
|
+
return {
|
|
863
|
+
agentId1,
|
|
864
|
+
agentId2,
|
|
865
|
+
sameIdentity: null,
|
|
866
|
+
sybilRisk: 'UNKNOWN',
|
|
867
|
+
reason: `Could not compare: ${e.message}`
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Check a list of agents for Sybil clusters (table seating check)
|
|
874
|
+
* @param {string[]} agentIds - List of agent IDs to check
|
|
875
|
+
* @returns {Promise<SybilTableCheck>}
|
|
876
|
+
*/
|
|
877
|
+
async checkTableSybils(agentIds) {
|
|
878
|
+
// Fetch all identities
|
|
879
|
+
const identities = {};
|
|
880
|
+
for (const id of agentIds) {
|
|
881
|
+
try {
|
|
882
|
+
const res = await fetch(`${this.baseUrl}/api/identity/${encodeURIComponent(id)}`);
|
|
883
|
+
if (res.ok) {
|
|
884
|
+
const data = await res.json();
|
|
885
|
+
identities[id] = data.identityHash;
|
|
886
|
+
}
|
|
887
|
+
} catch {
|
|
888
|
+
// Skip agents without identity
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Find clusters (same identity hash)
|
|
893
|
+
const hashToAgents = {};
|
|
894
|
+
for (const [agentId, hash] of Object.entries(identities)) {
|
|
895
|
+
if (!hashToAgents[hash]) hashToAgents[hash] = [];
|
|
896
|
+
hashToAgents[hash].push(agentId);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const clusters = Object.values(hashToAgents).filter(group => group.length > 1);
|
|
900
|
+
const flagged = clusters.flat();
|
|
901
|
+
|
|
902
|
+
return {
|
|
903
|
+
totalAgents: agentIds.length,
|
|
904
|
+
identifiedAgents: Object.keys(identities).length,
|
|
905
|
+
unidentifiedAgents: agentIds.filter(id => !identities[id]),
|
|
906
|
+
sybilClusters: clusters,
|
|
907
|
+
flaggedAgents: flagged,
|
|
908
|
+
safe: clusters.length === 0,
|
|
909
|
+
recommendation: clusters.length === 0
|
|
910
|
+
? 'No Sybil clusters detected — safe to proceed'
|
|
911
|
+
: `${clusters.length} Sybil cluster(s) detected — ${flagged.length} agents share hardware`
|
|
912
|
+
};
|
|
913
|
+
}
|
|
487
914
|
}
|
|
488
915
|
|
|
489
916
|
// Scoring helpers
|