yakmesh 2.9.0 → 3.0.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/CHANGELOG.md +637 -0
- package/Caddyfile +77 -0
- package/README.md +119 -29
- package/content/api.js +50 -41
- package/content/index.js +1 -2
- package/content/store.js +323 -177
- package/dashboard/index.html +19 -3
- package/database/replication.js +117 -37
- package/docs/CRYPTO-AGILITY.md +204 -0
- package/docs/MTLS-RESEARCH.md +367 -0
- package/docs/NAMCHE-SPEC.md +681 -0
- package/docs/PEERQUANTA-YAKMESH-INTEGRATION.md +407 -0
- package/docs/PRECISION-DISCLOSURE.md +96 -0
- package/docs/README.md +76 -0
- package/docs/ROADMAP-2.4.0.md +447 -0
- package/docs/ROADMAP-2.5.0.md +244 -0
- package/docs/SECURITY-AUDIT-REPORT.md +306 -0
- package/docs/SST-INTEGRATION.md +712 -0
- package/docs/STEADYWATCH-IMPLEMENTATION.md +303 -0
- package/docs/TERNARY-AUDIT-REPORT.md +247 -0
- package/docs/TME-FAQ.md +221 -0
- package/docs/WHITEPAPER.md +623 -0
- package/docs/adapters.html +1001 -0
- package/docs/advanced-systems.html +1045 -0
- package/docs/annex.html +1046 -0
- package/docs/api.html +970 -0
- package/docs/business/response-templates.md +160 -0
- package/docs/c2c.html +1225 -0
- package/docs/cli.html +1332 -0
- package/docs/configuration.html +1248 -0
- package/docs/darshan.html +1085 -0
- package/docs/dharma.html +966 -0
- package/docs/docs-bundle.html +1075 -0
- package/docs/docs.css +3120 -0
- package/docs/docs.js +556 -0
- package/docs/doko.html +969 -0
- package/docs/geo-proof.html +858 -0
- package/docs/getting-started.html +840 -0
- package/docs/gumba-tutorial.html +1144 -0
- package/docs/gumba.html +1098 -0
- package/docs/index.html +914 -0
- package/docs/jhilke.html +1312 -0
- package/docs/karma.html +1100 -0
- package/docs/katha.html +1037 -0
- package/docs/lama.html +978 -0
- package/docs/mandala.html +1067 -0
- package/docs/mani.html +964 -0
- package/docs/mantra.html +967 -0
- package/docs/mesh.html +1409 -0
- package/docs/nakpak.html +869 -0
- package/docs/namche.html +928 -0
- package/docs/nav-order.json +53 -0
- package/docs/prahari.html +1043 -0
- package/docs/prism-bash.min.js +1 -0
- package/docs/prism-javascript.min.js +1 -0
- package/docs/prism-json.min.js +1 -0
- package/docs/prism-tomorrow.min.css +1 -0
- package/docs/prism.min.js +1 -0
- package/docs/privacy.html +699 -0
- package/docs/quick-reference.html +1181 -0
- package/docs/sakshi.html +1402 -0
- package/docs/sandboxing.md +386 -0
- package/docs/seva.html +911 -0
- package/docs/sherpa.html +871 -0
- package/docs/studio.html +860 -0
- package/docs/stupa.html +995 -0
- package/docs/tailwind.min.css +2 -0
- package/docs/tattva.html +1332 -0
- package/docs/terms.html +686 -0
- package/docs/time-server-deployment.md +166 -0
- package/docs/time-sources.html +1392 -0
- package/docs/tivra.html +1127 -0
- package/docs/trademark-policy.html +686 -0
- package/docs/tribhuj.html +1183 -0
- package/docs/trust-security.html +1029 -0
- package/docs/tutorials/backup-recovery.html +654 -0
- package/docs/tutorials/dashboard.html +604 -0
- package/docs/tutorials/domain-setup.html +605 -0
- package/docs/tutorials/host-website.html +456 -0
- package/docs/tutorials/mesh-network.html +505 -0
- package/docs/tutorials/mobile-access.html +445 -0
- package/docs/tutorials/privacy.html +467 -0
- package/docs/tutorials/raspberry-pi.html +600 -0
- package/docs/tutorials/security-basics.html +539 -0
- package/docs/tutorials/share-files.html +431 -0
- package/docs/tutorials/troubleshooting.html +637 -0
- package/docs/tutorials/trust-karma.html +419 -0
- package/docs/tutorials/yak-protocol.html +456 -0
- package/docs/tutorials.html +1034 -0
- package/docs/vani.html +1270 -0
- package/docs/webserver.html +809 -0
- package/docs/yak-protocol.html +940 -0
- package/docs/yak-timeserver-design.md +475 -0
- package/docs/yakapp.html +1015 -0
- package/docs/ypc27.html +1069 -0
- package/docs/yurt.html +1344 -0
- package/embedded-docs/bundle.js +274 -114
- package/gossip/protocol.js +247 -27
- package/identity/key-resolver.js +262 -0
- package/identity/machine-seed.js +632 -0
- package/identity/node-key.js +669 -368
- package/identity/tribhuj-ratchet.js +506 -0
- package/knowledge-base.js +37 -8
- package/launcher/yakmesh.bat +62 -0
- package/launcher/yakmesh.sh +70 -0
- package/mesh/annex.js +462 -108
- package/mesh/beacon-broadcast.js +4 -1
- package/mesh/darshan.js +17 -5
- package/mesh/gumba.js +47 -13
- package/mesh/jhilke.js +651 -0
- package/mesh/katha.js +5 -2
- package/mesh/nakpak-routing.js +8 -5
- package/mesh/network.js +724 -34
- package/mesh/pulse-sync.js +4 -1
- package/mesh/seva.js +526 -0
- package/mesh/sherpa-discovery.js +89 -8
- package/mesh/sybil-defense.js +19 -5
- package/mesh/temporal-encoder.js +4 -3
- package/mesh/yurt.js +72 -17
- package/models/entropy-sentinel.onnx +0 -0
- package/models/karma-trust.onnx +0 -0
- package/models/manifest.json +43 -0
- package/models/sakshi-anomaly.onnx +0 -0
- package/oracle/code-proof-protocol.js +7 -6
- package/oracle/codebase-lock.js +257 -28
- package/oracle/index.js +74 -15
- package/oracle/ma902-snmp.js +678 -0
- package/oracle/module-sealer.js +5 -3
- package/oracle/packet-checksum.js +201 -0
- package/oracle/ternary-144t.js +714 -0
- package/oracle/ternary-ml.js +481 -0
- package/oracle/time-api.js +239 -0
- package/oracle/time-source.js +137 -47
- package/oracle/validation-oracle-hardened.js +1111 -1071
- package/oracle/validation-oracle.js +4 -2
- package/oracle/ypc27.js +211 -0
- package/package.json +20 -3
- package/protocol/yak-handler.js +35 -9
- package/protocol/yak-protocol.js +6 -5
- package/reference/cpp/yakmesh_mceliece_shard.cpp +168 -0
- package/reference/cpp/yakmesh_ypc27.cpp +179 -0
- package/sbom.json +87 -0
- package/scripts/security-audit.mjs +264 -0
- package/scripts/update-docs-sidebar.cjs +164 -0
- package/security/crypto-config.js +4 -3
- package/security/dharma-moderation.js +4 -3
- package/security/doko-identity.js +193 -143
- package/security/domain-consensus.js +86 -85
- package/security/fs-hardening.js +620 -0
- package/security/hardware-attestation.js +5 -3
- package/security/hybrid-trust.js +227 -87
- package/security/karma-rate-limiter.js +692 -0
- package/security/khata-protocol.js +22 -21
- package/security/khata-trust-integration.js +277 -150
- package/security/memory-safety.js +635 -0
- package/security/mesh-auth.js +11 -10
- package/security/mesh-revocation.js +18 -5
- package/security/namche-gateway.js +298 -69
- package/security/sakshi.js +102 -3
- package/security/sangha.js +770 -0
- package/security/secure-config.js +473 -0
- package/security/silicon-parity.js +13 -10
- package/security/steadywatch.js +1142 -0
- package/security/strike-system.js +32 -3
- package/security/temporal-signing.js +488 -0
- package/security/trit-commitment.js +464 -0
- package/server/crypto/annex.js +247 -0
- package/server/darshan-api.js +343 -0
- package/server/index.js +3259 -362
- package/server/komm-api.js +668 -0
- package/utils/accel.js +2273 -0
- package/utils/ternary-id.js +79 -0
- package/utils/verify-worker.js +57 -0
- package/webserver/index.js +95 -5
- package/assets/yakmesh-logo.png +0 -0
- package/assets/yakmesh-logo.svg +0 -80
- package/assets/yakmesh-logo2.png +0 -0
- package/assets/yakmesh-logo2sm.png +0 -0
- package/assets/ymsm.png +0 -0
- package/scripts/update-docs-nav.cjs +0 -194
- package/update-docs-nav.cjs +0 -18
- package/update-nav.ps1 +0 -16
- package/website/assets/silhouettes/adapters.svg +0 -107
- package/website/assets/silhouettes/api-endpoints.svg +0 -115
- package/website/assets/silhouettes/atomic-clock.svg +0 -83
- package/website/assets/silhouettes/base-camp.svg +0 -81
- package/website/assets/silhouettes/bridge.svg +0 -69
- package/website/assets/silhouettes/docs-bundle.svg +0 -113
- package/website/assets/silhouettes/doko-basket.svg +0 -70
- package/website/assets/silhouettes/fortress.svg +0 -93
- package/website/assets/silhouettes/gateway.svg +0 -54
- package/website/assets/silhouettes/gears.svg +0 -93
- package/website/assets/silhouettes/globe-satellite.svg +0 -67
- package/website/assets/silhouettes/karma-wheel.svg +0 -137
- package/website/assets/silhouettes/lama-council.svg +0 -141
- package/website/assets/silhouettes/mandala-network.svg +0 -169
- package/website/assets/silhouettes/mani-stones.svg +0 -149
- package/website/assets/silhouettes/mantra-wheel.svg +0 -116
- package/website/assets/silhouettes/mesh-nodes.svg +0 -113
- package/website/assets/silhouettes/nakpak.svg +0 -56
- package/website/assets/silhouettes/peak-lightning.svg +0 -73
- package/website/assets/silhouettes/sherpa.svg +0 -69
- package/website/assets/silhouettes/stupa-tower.svg +0 -119
- package/website/assets/silhouettes/tattva-eye.svg +0 -78
- package/website/assets/silhouettes/terminal.svg +0 -74
- package/website/assets/silhouettes/webserver.svg +0 -145
- package/website/assets/silhouettes/yak.svg +0 -78
- package/website/assets/yakmesh-logo.png +0 -0
- package/website/assets/yakmesh-logo.webp +0 -0
- package/website/assets/yakmesh-logo128x140.webp +0 -0
- package/website/assets/yakmesh-logo2.png +0 -0
- package/website/assets/yakmesh-logo2.svg +0 -51
- package/website/assets/yakmesh-logo40x44.webp +0 -0
- package/website/assets/yakmesh.gif +0 -0
- package/website/assets/yakmesh.ico +0 -0
- package/website/assets/yakmesh.jpg +0 -0
- package/website/assets/yakmesh.pdf +0 -0
- package/website/assets/yakmesh.png +0 -0
- package/website/assets/yakmesh.svg +0 -70
- package/website/assets/yakmesh128.webp +0 -0
- package/website/assets/yakmesh32.png +0 -0
- package/website/assets/yakmesh32.svg +0 -65
- package/website/assets/yakmesh32o.ico +0 -2
- package/website/assets/yakmesh32o.svg +0 -65
- package/website/assets/yakmesh32o.svgz +0 -0
package/content/store.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* YAKMESH™ Content Store
|
|
3
|
-
* Content-addressed storage with
|
|
3
|
+
* Content-addressed storage with integrity verification
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
5
|
+
* Content validity is determined by math, not votes:
|
|
6
|
+
* - Integrity: SHA3-256 hash of content matches claimed hash
|
|
7
|
+
* - Authorship: Publisher's ML-DSA-65 signature over the hash
|
|
8
|
+
* - Any node can independently verify both — one proof = proven
|
|
9
|
+
*
|
|
10
|
+
* No voting. No quorum. No 51% attack surface.
|
|
11
|
+
* "The math checks out" is the only consensus needed.
|
|
10
12
|
*
|
|
11
13
|
* @module content/store
|
|
12
14
|
* @license MIT
|
|
@@ -22,6 +24,9 @@ import { createLogger } from '../utils/logger.js';
|
|
|
22
24
|
// Import iO system for human-readable content names
|
|
23
25
|
import { deriveNetworkName } from '../oracle/network-identity.js';
|
|
24
26
|
|
|
27
|
+
// Import 144T ternary system for hex-free content addressing
|
|
28
|
+
import { TritAddress, TOTAL_TRITS } from '../oracle/ternary-144t.js';
|
|
29
|
+
|
|
25
30
|
const log = createLogger('content:store');
|
|
26
31
|
|
|
27
32
|
/**
|
|
@@ -41,16 +46,21 @@ export const ContentType = {
|
|
|
41
46
|
|
|
42
47
|
/**
|
|
43
48
|
* Content status in the network
|
|
49
|
+
*
|
|
50
|
+
* Yakmesh does NOT use voting/quorum consensus for content.
|
|
51
|
+
* Content integrity = SHA3-256 hash match.
|
|
52
|
+
* Content authorship = publisher's ML-DSA-65 signature over the hash.
|
|
53
|
+
* Any node can independently verify both — one proof = proven.
|
|
44
54
|
*/
|
|
45
55
|
export const ContentStatus = {
|
|
46
|
-
LOCAL: 'local', //
|
|
47
|
-
|
|
48
|
-
VERIFIED: 'verified', //
|
|
49
|
-
REJECTED: 'rejected', // Failed consensus
|
|
56
|
+
LOCAL: 'local', // Stored on this node, not yet announced
|
|
57
|
+
ANNOUNCED: 'announced', // Published to mesh via gossip
|
|
58
|
+
VERIFIED: 'verified', // Hash integrity + publisher signature confirmed
|
|
50
59
|
};
|
|
51
60
|
|
|
52
61
|
/**
|
|
53
|
-
* Compute content hash (SHA3-256)
|
|
62
|
+
* Compute content hash (SHA3-256) — returns hex string
|
|
63
|
+
* @deprecated Use computeContentHash144T for new content (hex-free addressing)
|
|
54
64
|
*/
|
|
55
65
|
export function computeContentHash(content) {
|
|
56
66
|
if (typeof content === 'string') {
|
|
@@ -66,6 +76,57 @@ export function computeContentHash(content) {
|
|
|
66
76
|
return bytesToHex(sha3_256(utf8ToBytes(JSON.stringify(content))));
|
|
67
77
|
}
|
|
68
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Compute content hash as 144T ternary address
|
|
81
|
+
*
|
|
82
|
+
* This is the preferred content addressing format:
|
|
83
|
+
* - No hex digits (no "666" ever)
|
|
84
|
+
* - Unified with node/mesh addressing system
|
|
85
|
+
* - Enables hierarchical content routing
|
|
86
|
+
* - Alphabet: {T, 0, 1} only (T = -1 in balanced ternary)
|
|
87
|
+
*
|
|
88
|
+
* @param {string | Buffer | Uint8Array | object} content — content to hash
|
|
89
|
+
* @returns {{ hex: string, trit: string, tritAddress: TritAddress }} — both formats for compatibility
|
|
90
|
+
*/
|
|
91
|
+
export function computeContentHash144T(content) {
|
|
92
|
+
// First compute SHA3-256 as hex (internal only)
|
|
93
|
+
const hex = computeContentHash(content);
|
|
94
|
+
|
|
95
|
+
// Convert to 144T ternary address
|
|
96
|
+
const tritAddress = TritAddress.fromHex(hex);
|
|
97
|
+
const trit = tritAddress.toString(true); // compact format with tier separators
|
|
98
|
+
|
|
99
|
+
return { hex, trit, tritAddress };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate that a hex string doesn't contain forbidden patterns.
|
|
104
|
+
* Used as a guard for any remaining hex output.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} hex — hex string to validate
|
|
107
|
+
* @returns {boolean} — true if safe, false if contains forbidden pattern
|
|
108
|
+
*/
|
|
109
|
+
export function isHexSafe(hex) {
|
|
110
|
+
// Reject any hex containing "666" sequence
|
|
111
|
+
return !hex.toLowerCase().includes('666');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if a string is a valid 144T trit address.
|
|
116
|
+
* Format: 4 tiers separated by dots, each tier has 4 sub-blocks separated by colons.
|
|
117
|
+
* Characters: T (negative), 0 (neutral), 1 (positive)
|
|
118
|
+
*
|
|
119
|
+
* @param {string} s — string to check
|
|
120
|
+
* @returns {boolean}
|
|
121
|
+
*/
|
|
122
|
+
export function isTritAddress(s) {
|
|
123
|
+
if (!s || typeof s !== 'string') return false;
|
|
124
|
+
// Remove separators and check length + characters
|
|
125
|
+
const clean = s.replace(/[.:]/g, '');
|
|
126
|
+
if (clean.length !== TOTAL_TRITS) return false;
|
|
127
|
+
return /^[T01]+$/i.test(clean);
|
|
128
|
+
}
|
|
129
|
+
|
|
69
130
|
/**
|
|
70
131
|
* Derive human-readable iO name from content hash
|
|
71
132
|
* Uses 3-word quantum wordlist for memorable, shareable names
|
|
@@ -82,14 +143,16 @@ export function deriveContentName(hash) {
|
|
|
82
143
|
*/
|
|
83
144
|
class ContentMetadata {
|
|
84
145
|
constructor(options = {}) {
|
|
85
|
-
this.hash = options.hash;
|
|
146
|
+
this.hash = options.hash; // SHA3-256 hex (legacy, internal)
|
|
147
|
+
this.hash144t = options.hash144t || null; // 144T ternary address (preferred, public)
|
|
86
148
|
this.ioName = options.ioName || null; // Auto-generated iO name (human-readable)
|
|
87
149
|
this.contentType = options.contentType || ContentType.BINARY;
|
|
88
150
|
this.size = options.size || 0;
|
|
89
151
|
this.createdAt = options.createdAt || Date.now();
|
|
90
152
|
this.publishedBy = options.publishedBy || null;
|
|
91
153
|
this.status = options.status || ContentStatus.LOCAL;
|
|
92
|
-
this.
|
|
154
|
+
this.publisherSignature = options.publisherSignature || null; // ML-DSA-65 sig over content hash
|
|
155
|
+
this.publisherBackupSignature = options.publisherBackupSignature || null; // SLH-DSA sig (dual-sig defense-in-depth)
|
|
93
156
|
this.tags = options.tags || [];
|
|
94
157
|
this.name = options.name || null; // Optional custom name (user-provided)
|
|
95
158
|
this.ttl = options.ttl || 0; // 0 = permanent
|
|
@@ -98,64 +161,31 @@ class ContentMetadata {
|
|
|
98
161
|
toJSON() {
|
|
99
162
|
return {
|
|
100
163
|
hash: this.hash,
|
|
164
|
+
hash144t: this.hash144t,
|
|
101
165
|
ioName: this.ioName,
|
|
102
166
|
contentType: this.contentType,
|
|
103
167
|
size: this.size,
|
|
104
168
|
createdAt: this.createdAt,
|
|
105
169
|
publishedBy: this.publishedBy,
|
|
106
170
|
status: this.status,
|
|
107
|
-
|
|
171
|
+
publisherSignature: this.publisherSignature,
|
|
172
|
+
publisherBackupSignature: this.publisherBackupSignature,
|
|
108
173
|
tags: this.tags,
|
|
109
174
|
name: this.name,
|
|
110
175
|
ttl: this.ttl,
|
|
111
176
|
};
|
|
112
177
|
}
|
|
113
178
|
|
|
114
|
-
static fromJSON(json) {
|
|
115
|
-
return new ContentMetadata(json);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Consensus proof for light client verification
|
|
121
|
-
*/
|
|
122
|
-
class ConsensusProof {
|
|
123
|
-
constructor(options = {}) {
|
|
124
|
-
this.contentHash = options.contentHash;
|
|
125
|
-
this.timestamp = options.timestamp || Date.now();
|
|
126
|
-
this.validators = options.validators || []; // Array of { nodeId, signature }
|
|
127
|
-
this.quorum = options.quorum || 0; // Required signatures
|
|
128
|
-
this.networkId = options.networkId || null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Check if proof has quorum
|
|
133
|
-
*/
|
|
134
|
-
hasQuorum() {
|
|
135
|
-
return this.validators.length >= this.quorum;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
179
|
/**
|
|
139
|
-
*
|
|
180
|
+
* Get the public-facing content ID (144T preferred, fallback to iO name)
|
|
181
|
+
* Never returns hex to external callers.
|
|
140
182
|
*/
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.validators.push({ nodeId, signature, timestamp: Date.now() });
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
toJSON() {
|
|
148
|
-
return {
|
|
149
|
-
contentHash: this.contentHash,
|
|
150
|
-
timestamp: this.timestamp,
|
|
151
|
-
validators: this.validators,
|
|
152
|
-
quorum: this.quorum,
|
|
153
|
-
networkId: this.networkId,
|
|
154
|
-
};
|
|
183
|
+
getPublicId() {
|
|
184
|
+
return this.hash144t || this.ioName || this.name;
|
|
155
185
|
}
|
|
156
186
|
|
|
157
187
|
static fromJSON(json) {
|
|
158
|
-
return new
|
|
188
|
+
return new ContentMetadata(json);
|
|
159
189
|
}
|
|
160
190
|
}
|
|
161
191
|
|
|
@@ -169,18 +199,17 @@ export class ContentStore {
|
|
|
169
199
|
dataDir: config.dataDir || './data/content',
|
|
170
200
|
maxContentSize: config.maxContentSize || 10 * 1024 * 1024, // 10MB default
|
|
171
201
|
cacheSize: config.cacheSize || 100, // LRU cache entries
|
|
172
|
-
quorumSize: config.quorumSize || 2, // Minimum validators
|
|
173
202
|
...config,
|
|
174
203
|
};
|
|
175
204
|
|
|
176
205
|
this.contentDir = join(this.config.dataDir, 'objects');
|
|
177
206
|
this.metaDir = join(this.config.dataDir, 'meta');
|
|
178
|
-
|
|
207
|
+
|
|
179
208
|
// In-memory caches
|
|
180
209
|
this.contentCache = new Map(); // hash -> content (LRU)
|
|
181
210
|
this.metaCache = new Map(); // hash -> ContentMetadata
|
|
182
211
|
this.nameIndex = new Map(); // name -> hash (for human-readable lookup)
|
|
183
|
-
|
|
212
|
+
|
|
184
213
|
// Mesh integration (set by init)
|
|
185
214
|
this.mesh = null;
|
|
186
215
|
this.identity = null;
|
|
@@ -205,13 +234,13 @@ export class ContentStore {
|
|
|
205
234
|
this.identity = node.identity;
|
|
206
235
|
this.oracle = node.oracle;
|
|
207
236
|
this.gossip = node.gossip;
|
|
208
|
-
|
|
237
|
+
|
|
209
238
|
// Content gossip is handled by the server via mesh.on('rumor')
|
|
210
239
|
// which calls contentStore._handleContentGossip()
|
|
211
240
|
}
|
|
212
241
|
|
|
213
242
|
log.info('Content store initialized', { dataDir: this.config.dataDir, objectCount: this.metaCache.size });
|
|
214
|
-
|
|
243
|
+
|
|
215
244
|
return this;
|
|
216
245
|
}
|
|
217
246
|
|
|
@@ -229,7 +258,7 @@ export class ContentStore {
|
|
|
229
258
|
const json = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
230
259
|
const meta = ContentMetadata.fromJSON(json);
|
|
231
260
|
this.metaCache.set(meta.hash, meta);
|
|
232
|
-
|
|
261
|
+
|
|
233
262
|
// Index iO name (auto-generated or derive if missing from old data)
|
|
234
263
|
if (meta.ioName) {
|
|
235
264
|
this.nameIndex.set(meta.ioName, meta.hash);
|
|
@@ -239,7 +268,7 @@ export class ContentStore {
|
|
|
239
268
|
meta.ioName = ioName;
|
|
240
269
|
this.nameIndex.set(ioName, meta.hash);
|
|
241
270
|
}
|
|
242
|
-
|
|
271
|
+
|
|
243
272
|
// Index custom name if provided
|
|
244
273
|
if (meta.name) {
|
|
245
274
|
this.nameIndex.set(meta.name, meta.hash);
|
|
@@ -251,34 +280,50 @@ export class ContentStore {
|
|
|
251
280
|
}
|
|
252
281
|
|
|
253
282
|
/**
|
|
254
|
-
* Get content path for a hash
|
|
283
|
+
* Get content path for a hash (supports both hex and 144T)
|
|
284
|
+
*
|
|
285
|
+
* For ternary addresses, uses first tier's first sub-block (9 chars) as prefix.
|
|
286
|
+
* For hex (legacy), uses first 2 chars as prefix.
|
|
255
287
|
*/
|
|
256
288
|
_getContentPath(hash) {
|
|
257
|
-
//
|
|
289
|
+
// Check if this is a 144T address (contains T, 0, 1 and dots/colons)
|
|
290
|
+
if (isTritAddress(hash)) {
|
|
291
|
+
// Use first 9 trits (first sub-block) as directory prefix
|
|
292
|
+
const clean = hash.replace(/[.:]/g, '');
|
|
293
|
+
const prefix = clean.slice(0, 9);
|
|
294
|
+
const suffix = clean.slice(9);
|
|
295
|
+
return join(this.contentDir, 't', prefix, suffix);
|
|
296
|
+
}
|
|
297
|
+
// Legacy hex: store in subdirectories for filesystem efficiency (git-style)
|
|
258
298
|
const prefix = hash.slice(0, 2);
|
|
259
299
|
const suffix = hash.slice(2);
|
|
260
300
|
return join(this.contentDir, prefix, suffix);
|
|
261
301
|
}
|
|
262
302
|
|
|
263
303
|
/**
|
|
264
|
-
* Get metadata path for a hash
|
|
304
|
+
* Get metadata path for a hash (supports both hex and 144T)
|
|
265
305
|
*/
|
|
266
306
|
_getMetaPath(hash) {
|
|
267
|
-
|
|
307
|
+
// Normalize 144T to filename-safe format (remove separators)
|
|
308
|
+
const safeHash = hash.replace(/[.:]/g, '');
|
|
309
|
+
return join(this.metaDir, `${safeHash}.json`);
|
|
268
310
|
}
|
|
269
311
|
|
|
270
312
|
/**
|
|
271
313
|
* Store content
|
|
314
|
+
*
|
|
315
|
+
* Returns the 144T hash (preferred) along with legacy hex for compatibility.
|
|
316
|
+
* Internal storage uses hex paths for backward compatibility with existing content.
|
|
272
317
|
*/
|
|
273
318
|
async store(content, options = {}) {
|
|
274
|
-
// Compute hash
|
|
275
|
-
const hash =
|
|
276
|
-
|
|
319
|
+
// Compute both hash formats
|
|
320
|
+
const { hex: hash, trit: hash144t } = computeContentHash144T(content);
|
|
321
|
+
|
|
277
322
|
// Check size limit
|
|
278
|
-
const size = Buffer.isBuffer(content) ? content.length :
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
323
|
+
const size = Buffer.isBuffer(content) ? content.length :
|
|
324
|
+
typeof content === 'string' ? Buffer.byteLength(content) :
|
|
325
|
+
Buffer.byteLength(JSON.stringify(content));
|
|
326
|
+
|
|
282
327
|
if (size > this.config.maxContentSize) {
|
|
283
328
|
throw new Error(`Content exceeds max size: ${size} > ${this.config.maxContentSize}`);
|
|
284
329
|
}
|
|
@@ -286,15 +331,22 @@ export class ContentStore {
|
|
|
286
331
|
// Check if already exists
|
|
287
332
|
if (this.has(hash)) {
|
|
288
333
|
const existing = this.getMeta(hash);
|
|
289
|
-
return {
|
|
334
|
+
return {
|
|
335
|
+
hash,
|
|
336
|
+
hash144t: existing.hash144t || hash144t,
|
|
337
|
+
ioName: existing.ioName,
|
|
338
|
+
status: 'exists',
|
|
339
|
+
meta: existing
|
|
340
|
+
};
|
|
290
341
|
}
|
|
291
342
|
|
|
292
343
|
// Generate iO name for human-readable sharing
|
|
293
344
|
const ioName = deriveContentName(hash);
|
|
294
345
|
|
|
295
|
-
// Create metadata
|
|
346
|
+
// Create metadata with both hash formats
|
|
296
347
|
const meta = new ContentMetadata({
|
|
297
348
|
hash,
|
|
349
|
+
hash144t,
|
|
298
350
|
ioName,
|
|
299
351
|
contentType: options.contentType || this._detectContentType(content),
|
|
300
352
|
size,
|
|
@@ -308,7 +360,7 @@ export class ContentStore {
|
|
|
308
360
|
// Write content to disk
|
|
309
361
|
const contentPath = this._getContentPath(hash);
|
|
310
362
|
mkdirSync(dirname(contentPath), { recursive: true });
|
|
311
|
-
|
|
363
|
+
|
|
312
364
|
if (Buffer.isBuffer(content)) {
|
|
313
365
|
writeFileSync(contentPath, content);
|
|
314
366
|
} else if (typeof content === 'string') {
|
|
@@ -322,9 +374,10 @@ export class ContentStore {
|
|
|
322
374
|
|
|
323
375
|
// Update caches
|
|
324
376
|
this.metaCache.set(hash, meta);
|
|
325
|
-
|
|
326
|
-
// Index
|
|
327
|
-
this.nameIndex.set(ioName, hash);
|
|
377
|
+
|
|
378
|
+
// Index by iO name, 144T address, and custom name for flexible lookup
|
|
379
|
+
this.nameIndex.set(ioName, hash); // iO name always indexed
|
|
380
|
+
this.nameIndex.set(hash144t, hash); // 144T address indexed
|
|
328
381
|
if (meta.name) {
|
|
329
382
|
this.nameIndex.set(meta.name, hash); // Custom name if provided
|
|
330
383
|
}
|
|
@@ -335,18 +388,28 @@ export class ContentStore {
|
|
|
335
388
|
await this.publish(hash);
|
|
336
389
|
}
|
|
337
390
|
|
|
338
|
-
log.info('Content stored', {
|
|
339
|
-
return { hash, ioName, status: 'stored', meta };
|
|
391
|
+
log.info('Content stored', { hash144t: hash144t.split('.')[0] + '...', ioName, size });
|
|
392
|
+
return { hash, hash144t, ioName, status: 'stored', meta };
|
|
340
393
|
}
|
|
341
394
|
|
|
342
395
|
/**
|
|
343
|
-
*
|
|
396
|
+
* Resolve any content identifier to internal hex hash.
|
|
397
|
+
* Accepts: hex hash, 144T address, iO name, or custom name.
|
|
398
|
+
* @private
|
|
344
399
|
*/
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
400
|
+
_resolveHash(id) {
|
|
401
|
+
if (!id) return null;
|
|
402
|
+
// Already hex?
|
|
403
|
+
if (/^[a-f0-9]{64}$/i.test(id)) return id;
|
|
404
|
+
// 144T address or name - look up in index
|
|
405
|
+
return this.nameIndex.get(id) || id;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Retrieve content by hash, 144T address, iO name, or custom name
|
|
410
|
+
*/
|
|
411
|
+
get(id) {
|
|
412
|
+
const hash = this._resolveHash(id);
|
|
350
413
|
|
|
351
414
|
// Check memory cache
|
|
352
415
|
if (this.contentCache.has(hash)) {
|
|
@@ -362,24 +425,25 @@ export class ContentStore {
|
|
|
362
425
|
// Load and cache
|
|
363
426
|
const content = readFileSync(contentPath);
|
|
364
427
|
this._addToContentCache(hash, content);
|
|
365
|
-
|
|
428
|
+
|
|
366
429
|
return content;
|
|
367
430
|
}
|
|
368
431
|
|
|
369
432
|
/**
|
|
370
|
-
* Get content with metadata and
|
|
433
|
+
* Get content with metadata and verification status
|
|
371
434
|
*/
|
|
372
|
-
getWithProof(
|
|
435
|
+
getWithProof(id) {
|
|
436
|
+
const hash = this._resolveHash(id);
|
|
373
437
|
const content = this.get(hash);
|
|
374
438
|
if (!content) return null;
|
|
375
439
|
|
|
376
440
|
const meta = this.getMeta(hash);
|
|
377
|
-
|
|
441
|
+
|
|
378
442
|
return {
|
|
379
443
|
content,
|
|
380
444
|
hash,
|
|
445
|
+
hash144t: meta?.hash144t || null,
|
|
381
446
|
meta: meta?.toJSON() || null,
|
|
382
|
-
proof: meta?.consensusProof || null,
|
|
383
447
|
verified: meta?.status === ContentStatus.VERIFIED,
|
|
384
448
|
};
|
|
385
449
|
}
|
|
@@ -387,22 +451,16 @@ export class ContentStore {
|
|
|
387
451
|
/**
|
|
388
452
|
* Get metadata for content
|
|
389
453
|
*/
|
|
390
|
-
getMeta(
|
|
391
|
-
|
|
392
|
-
if (!hash.match(/^[a-f0-9]{64}$/i)) {
|
|
393
|
-
hash = this.nameIndex.get(hash) || hash;
|
|
394
|
-
}
|
|
454
|
+
getMeta(id) {
|
|
455
|
+
const hash = this._resolveHash(id);
|
|
395
456
|
return this.metaCache.get(hash) || null;
|
|
396
457
|
}
|
|
397
458
|
|
|
398
459
|
/**
|
|
399
460
|
* Check if content exists
|
|
400
461
|
*/
|
|
401
|
-
has(
|
|
402
|
-
|
|
403
|
-
if (!hash.match(/^[a-f0-9]{64}$/i)) {
|
|
404
|
-
hash = this.nameIndex.get(hash) || hash;
|
|
405
|
-
}
|
|
462
|
+
has(id) {
|
|
463
|
+
const hash = this._resolveHash(id);
|
|
406
464
|
return this.metaCache.has(hash) || existsSync(this._getContentPath(hash));
|
|
407
465
|
}
|
|
408
466
|
|
|
@@ -411,11 +469,11 @@ export class ContentStore {
|
|
|
411
469
|
*/
|
|
412
470
|
delete(hash) {
|
|
413
471
|
const meta = this.getMeta(hash);
|
|
414
|
-
|
|
472
|
+
|
|
415
473
|
// Remove from disk
|
|
416
474
|
const contentPath = this._getContentPath(hash);
|
|
417
475
|
const metaPath = this._getMetaPath(hash);
|
|
418
|
-
|
|
476
|
+
|
|
419
477
|
if (existsSync(contentPath)) unlinkSync(contentPath);
|
|
420
478
|
if (existsSync(metaPath)) unlinkSync(metaPath);
|
|
421
479
|
|
|
@@ -434,28 +492,31 @@ export class ContentStore {
|
|
|
434
492
|
*/
|
|
435
493
|
list(options = {}) {
|
|
436
494
|
const { tag, status, limit = 100, offset = 0 } = options;
|
|
437
|
-
|
|
495
|
+
|
|
438
496
|
let items = Array.from(this.metaCache.values());
|
|
439
|
-
|
|
497
|
+
|
|
440
498
|
// Filter by tag
|
|
441
499
|
if (tag) {
|
|
442
500
|
items = items.filter(m => m.tags.includes(tag));
|
|
443
501
|
}
|
|
444
|
-
|
|
502
|
+
|
|
445
503
|
// Filter by status
|
|
446
504
|
if (status) {
|
|
447
505
|
items = items.filter(m => m.status === status);
|
|
448
506
|
}
|
|
449
|
-
|
|
507
|
+
|
|
450
508
|
// Sort by created date (newest first)
|
|
451
509
|
items.sort((a, b) => b.createdAt - a.createdAt);
|
|
452
|
-
|
|
510
|
+
|
|
453
511
|
// Paginate
|
|
454
512
|
return items.slice(offset, offset + limit).map(m => m.toJSON());
|
|
455
513
|
}
|
|
456
514
|
|
|
457
515
|
/**
|
|
458
516
|
* Publish content to mesh
|
|
517
|
+
* Signs the content hash with the publisher's ML-DSA-65 identity.
|
|
518
|
+
* Any receiving node can independently verify: hash(content) === hash AND
|
|
519
|
+
* verify(hash, publisherSignature, publisherPubKey) === true.
|
|
459
520
|
*/
|
|
460
521
|
async publish(hash) {
|
|
461
522
|
const meta = this.getMeta(hash);
|
|
@@ -463,6 +524,25 @@ export class ContentStore {
|
|
|
463
524
|
throw new Error(`Content not found: ${hash}`);
|
|
464
525
|
}
|
|
465
526
|
|
|
527
|
+
// Sign the content hash with publisher's identity (authorship proof)
|
|
528
|
+
// Uses dual signature (ML-DSA-65 + SLH-DSA) when available for defense-in-depth
|
|
529
|
+
let publisherSignature = null;
|
|
530
|
+
let publisherBackupSignature = null;
|
|
531
|
+
if (this.identity) {
|
|
532
|
+
if (this.identity.hasDualSignature?.()) {
|
|
533
|
+
// Critical op: use both ML-DSA-65 AND SLH-DSA (defense-in-depth)
|
|
534
|
+
const sigs = this.identity.signCritical(hash);
|
|
535
|
+
publisherSignature = sigs.primary;
|
|
536
|
+
publisherBackupSignature = sigs.backup;
|
|
537
|
+
meta.publisherSignature = publisherSignature;
|
|
538
|
+
meta.publisherBackupSignature = publisherBackupSignature;
|
|
539
|
+
} else {
|
|
540
|
+
// Fallback: ML-DSA-65 only (no SLH-DSA backup key available)
|
|
541
|
+
publisherSignature = this.identity.sign(hash);
|
|
542
|
+
meta.publisherSignature = publisherSignature;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
466
546
|
// Create announcement message
|
|
467
547
|
const announcement = {
|
|
468
548
|
type: 'content_announce',
|
|
@@ -471,17 +551,14 @@ export class ContentStore {
|
|
|
471
551
|
contentType: meta.contentType,
|
|
472
552
|
size: meta.size,
|
|
473
553
|
publishedBy: meta.publishedBy,
|
|
554
|
+
publisherSignature,
|
|
555
|
+
publisherBackupSignature,
|
|
474
556
|
tags: meta.tags,
|
|
475
557
|
name: meta.name,
|
|
476
558
|
},
|
|
477
559
|
timestamp: Date.now(),
|
|
478
560
|
};
|
|
479
561
|
|
|
480
|
-
// Sign with node identity
|
|
481
|
-
if (this.identity) {
|
|
482
|
-
announcement.signature = this.identity.sign(JSON.stringify(announcement));
|
|
483
|
-
}
|
|
484
|
-
|
|
485
562
|
// Gossip to mesh
|
|
486
563
|
if (this.gossip) {
|
|
487
564
|
log.debug('Gossiping content_announce', { hash: hash.slice(0, 16) });
|
|
@@ -490,8 +567,8 @@ export class ContentStore {
|
|
|
490
567
|
log.warn('No gossip protocol available for content announce');
|
|
491
568
|
}
|
|
492
569
|
|
|
493
|
-
// Update status
|
|
494
|
-
meta.status = ContentStatus.
|
|
570
|
+
// Update status to ANNOUNCED (published to mesh)
|
|
571
|
+
meta.status = ContentStatus.ANNOUNCED;
|
|
495
572
|
writeFileSync(this._getMetaPath(hash), JSON.stringify(meta.toJSON(), null, 2));
|
|
496
573
|
|
|
497
574
|
return { published: true, hash };
|
|
@@ -566,13 +643,12 @@ export class ContentStore {
|
|
|
566
643
|
} else {
|
|
567
644
|
contentBase64 = Buffer.from(JSON.stringify(result.content), 'utf8').toString('base64');
|
|
568
645
|
}
|
|
569
|
-
|
|
646
|
+
|
|
570
647
|
this.gossip.spreadRumor('content', {
|
|
571
648
|
type: 'content_response',
|
|
572
649
|
hash: data.hash,
|
|
573
650
|
content: contentBase64,
|
|
574
651
|
meta: result.meta,
|
|
575
|
-
proof: result.proof,
|
|
576
652
|
timestamp: Date.now(),
|
|
577
653
|
});
|
|
578
654
|
}
|
|
@@ -580,76 +656,146 @@ export class ContentStore {
|
|
|
580
656
|
break;
|
|
581
657
|
|
|
582
658
|
case 'content_response':
|
|
583
|
-
// Received content from peer
|
|
659
|
+
// Received content from peer — verify integrity + authorship
|
|
584
660
|
if (!this.has(data.hash)) {
|
|
585
661
|
const content = Buffer.from(data.content, 'base64');
|
|
586
662
|
const computedHash = computeContentHash(content);
|
|
587
|
-
|
|
588
|
-
// Verify hash
|
|
663
|
+
|
|
664
|
+
// Gate 1: Verify hash integrity
|
|
589
665
|
if (computedHash !== data.hash) {
|
|
590
666
|
console.warn(`⚠️ Content hash mismatch from ${origin.slice(0, 16)}...`);
|
|
591
667
|
return;
|
|
592
|
-
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Store it — use explicit allowlist, never spread remote meta directly (proto pollution defense)
|
|
671
|
+
const remoteMeta = data.meta || {};
|
|
593
672
|
await this.store(content, {
|
|
594
|
-
|
|
673
|
+
contentType: remoteMeta.contentType,
|
|
674
|
+
size: remoteMeta.size,
|
|
675
|
+
publishedBy: remoteMeta.publishedBy,
|
|
676
|
+
publisherSignature: remoteMeta.publisherSignature,
|
|
677
|
+
publisherBackupSignature: remoteMeta.publisherBackupSignature,
|
|
678
|
+
tags: remoteMeta.tags,
|
|
679
|
+
name: remoteMeta.name,
|
|
595
680
|
publish: false, // Don't re-gossip
|
|
596
681
|
});
|
|
597
682
|
|
|
598
|
-
//
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
683
|
+
// Gate 2: Verify publisher signature (authorship)
|
|
684
|
+
const meta = this.getMeta(data.hash);
|
|
685
|
+
const publisherSig = data.meta?.publisherSignature;
|
|
686
|
+
const publisherBackupSig = data.meta?.publisherBackupSignature;
|
|
687
|
+
const publisherId = data.meta?.publishedBy;
|
|
688
|
+
|
|
689
|
+
if (publisherSig && publisherId && this.identity) {
|
|
690
|
+
const publisherPubKey = this._getPeerPublicKey(publisherId);
|
|
691
|
+
if (!publisherPubKey) {
|
|
692
|
+
// Can't look up publisher's key — store as ANNOUNCED
|
|
693
|
+
meta.status = ContentStatus.ANNOUNCED;
|
|
694
|
+
log.debug('Content received but publisher key unknown', { hash: data.hash.slice(0, 16) });
|
|
695
|
+
} else if (publisherBackupSig) {
|
|
696
|
+
// Dual-sig present: verify BOTH ML-DSA-65 + SLH-DSA (defense-in-depth)
|
|
697
|
+
const backupPubKey = this._getPeerBackupPublicKey(publisherId);
|
|
698
|
+
if (backupPubKey) {
|
|
699
|
+
const result = this.identity.verifyCritical(
|
|
700
|
+
data.hash, publisherSig, publisherBackupSig, publisherPubKey, backupPubKey
|
|
701
|
+
);
|
|
702
|
+
if (result.valid) {
|
|
703
|
+
meta.status = ContentStatus.VERIFIED;
|
|
704
|
+
meta.publisherSignature = publisherSig;
|
|
705
|
+
meta.publisherBackupSignature = publisherBackupSig;
|
|
706
|
+
log.info('Content verified (dual-sig: ML-DSA + SLH-DSA)', {
|
|
707
|
+
hash: data.hash.slice(0, 16), publisher: publisherId.slice(0, 16),
|
|
708
|
+
});
|
|
709
|
+
} else {
|
|
710
|
+
// At least one sig failed — reject as potentially tampered
|
|
711
|
+
meta.status = ContentStatus.ANNOUNCED;
|
|
712
|
+
log.warn('Content dual-sig verification FAILED', {
|
|
713
|
+
hash: data.hash.slice(0, 16),
|
|
714
|
+
primaryValid: result.primaryValid,
|
|
715
|
+
backupValid: result.backupValid,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
} else {
|
|
719
|
+
// No backup key for publisher — fall back to primary-only verification
|
|
720
|
+
if (this.identity.verify(data.hash, publisherSig, publisherPubKey)) {
|
|
721
|
+
meta.status = ContentStatus.VERIFIED;
|
|
722
|
+
meta.publisherSignature = publisherSig;
|
|
723
|
+
log.info('Content verified (ML-DSA only, no backup key for publisher)', {
|
|
724
|
+
hash: data.hash.slice(0, 16), publisher: publisherId.slice(0, 16),
|
|
725
|
+
});
|
|
726
|
+
} else {
|
|
727
|
+
meta.status = ContentStatus.ANNOUNCED;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
} else if (this.identity.verify(data.hash, publisherSig, publisherPubKey)) {
|
|
731
|
+
// Single sig only — verify ML-DSA-65
|
|
732
|
+
meta.status = ContentStatus.VERIFIED;
|
|
733
|
+
meta.publisherSignature = publisherSig;
|
|
734
|
+
log.info('Content verified (hash + publisher sig)', { hash: data.hash.slice(0, 16), publisher: publisherId.slice(0, 16) });
|
|
735
|
+
} else {
|
|
736
|
+
// Have content but can't confirm authorship — ANNOUNCED
|
|
737
|
+
meta.status = ContentStatus.ANNOUNCED;
|
|
738
|
+
log.debug('Content received but publisher sig unverifiable', { hash: data.hash.slice(0, 16) });
|
|
739
|
+
}
|
|
740
|
+
} else {
|
|
741
|
+
// No publisher sig available — store as ANNOUNCED
|
|
742
|
+
meta.status = ContentStatus.ANNOUNCED;
|
|
604
743
|
}
|
|
605
744
|
|
|
606
|
-
|
|
745
|
+
writeFileSync(this._getMetaPath(data.hash), JSON.stringify(meta.toJSON(), null, 2));
|
|
746
|
+
log.info('Content received', { hash: data.hash.slice(0, 16), status: meta.status });
|
|
607
747
|
}
|
|
608
748
|
break;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
609
751
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
752
|
+
/**
|
|
753
|
+
* Resolve a peer's public key from mesh state.
|
|
754
|
+
* Checks WS peers, relay keys, SHERPA registry, and self.
|
|
755
|
+
*/
|
|
756
|
+
_getPeerPublicKey(nodeId) {
|
|
757
|
+
// Self
|
|
758
|
+
if (this.identity && nodeId === this.identity.identity.nodeId) {
|
|
759
|
+
return this.identity.identity.publicKey;
|
|
760
|
+
}
|
|
761
|
+
// WS peer info
|
|
762
|
+
if (this.mesh?.peers) {
|
|
763
|
+
const peer = this.mesh.peers.get(nodeId);
|
|
764
|
+
if (peer?.identity?.publicKey) return peer.identity.publicKey;
|
|
765
|
+
}
|
|
766
|
+
// Relay peer keys (stored during signed registration)
|
|
767
|
+
if (this.mesh?._relayPeerKeys) {
|
|
768
|
+
const key = this.mesh._relayPeerKeys.get(nodeId);
|
|
769
|
+
if (key) return key;
|
|
770
|
+
}
|
|
771
|
+
// SHERPA registry
|
|
772
|
+
if (this.mesh?.sherpa?.registry) {
|
|
773
|
+
const regPeer = this.mesh.sherpa.registry.get(nodeId);
|
|
774
|
+
if (regPeer?.publicKey) return regPeer.publicKey;
|
|
775
|
+
}
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
630
778
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
writeFileSync(this._getMetaPath(data.hash), JSON.stringify(meta.toJSON(), null, 2));
|
|
650
|
-
}
|
|
651
|
-
break;
|
|
779
|
+
/**
|
|
780
|
+
* Resolve a peer's SLH-DSA backup public key from mesh state.
|
|
781
|
+
* Same lookup chain as _getPeerPublicKey but for the backupPublicKey field.
|
|
782
|
+
*/
|
|
783
|
+
_getPeerBackupPublicKey(nodeId) {
|
|
784
|
+
// Self
|
|
785
|
+
if (this.identity && nodeId === this.identity.identity.nodeId) {
|
|
786
|
+
return this.identity.identity.backupPublicKey || null;
|
|
787
|
+
}
|
|
788
|
+
// WS peer info
|
|
789
|
+
if (this.mesh?.peers) {
|
|
790
|
+
const peer = this.mesh.peers.get(nodeId);
|
|
791
|
+
if (peer?.identity?.backupPublicKey) return peer.identity.backupPublicKey;
|
|
792
|
+
}
|
|
793
|
+
// SHERPA registry
|
|
794
|
+
if (this.mesh?.sherpa?.registry) {
|
|
795
|
+
const regPeer = this.mesh.sherpa.registry.get(nodeId);
|
|
796
|
+
if (regPeer?.backupPublicKey) return regPeer.backupPublicKey;
|
|
652
797
|
}
|
|
798
|
+
return null;
|
|
653
799
|
}
|
|
654
800
|
|
|
655
801
|
/**
|
|
@@ -671,9 +817,9 @@ export class ContentStore {
|
|
|
671
817
|
if (typeof content === 'object' && !Buffer.isBuffer(content)) {
|
|
672
818
|
return ContentType.JSON;
|
|
673
819
|
}
|
|
674
|
-
|
|
820
|
+
|
|
675
821
|
const str = content.toString().slice(0, 100);
|
|
676
|
-
|
|
822
|
+
|
|
677
823
|
if (str.startsWith('<!DOCTYPE') || str.startsWith('<html')) {
|
|
678
824
|
return ContentType.HTML;
|
|
679
825
|
}
|
|
@@ -683,7 +829,7 @@ export class ContentStore {
|
|
|
683
829
|
if (str.includes('function') || str.includes('const ') || str.includes('import ')) {
|
|
684
830
|
return ContentType.JAVASCRIPT;
|
|
685
831
|
}
|
|
686
|
-
|
|
832
|
+
|
|
687
833
|
return ContentType.TEXT;
|
|
688
834
|
}
|
|
689
835
|
|
|
@@ -693,14 +839,14 @@ export class ContentStore {
|
|
|
693
839
|
getStats() {
|
|
694
840
|
let totalSize = 0;
|
|
695
841
|
let verified = 0;
|
|
696
|
-
let
|
|
842
|
+
let announced = 0;
|
|
697
843
|
let local = 0;
|
|
698
844
|
|
|
699
845
|
for (const meta of this.metaCache.values()) {
|
|
700
846
|
totalSize += meta.size;
|
|
701
847
|
switch (meta.status) {
|
|
702
848
|
case ContentStatus.VERIFIED: verified++; break;
|
|
703
|
-
case ContentStatus.
|
|
849
|
+
case ContentStatus.ANNOUNCED: announced++; break;
|
|
704
850
|
case ContentStatus.LOCAL: local++; break;
|
|
705
851
|
}
|
|
706
852
|
}
|
|
@@ -709,7 +855,7 @@ export class ContentStore {
|
|
|
709
855
|
totalObjects: this.metaCache.size,
|
|
710
856
|
totalSize,
|
|
711
857
|
verified,
|
|
712
|
-
|
|
858
|
+
announced,
|
|
713
859
|
local,
|
|
714
860
|
cacheSize: this.contentCache.size,
|
|
715
861
|
dataDir: this.config.dataDir,
|
|
@@ -717,5 +863,5 @@ export class ContentStore {
|
|
|
717
863
|
}
|
|
718
864
|
}
|
|
719
865
|
|
|
720
|
-
export { ContentMetadata
|
|
866
|
+
export { ContentMetadata };
|
|
721
867
|
export default ContentStore;
|