yakmesh 2.8.2 → 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/CONTRIBUTING.md +42 -0
- package/Caddyfile +77 -0
- package/README.md +119 -29
- package/adapters/adapter-mlv-bible/README.md +124 -0
- package/adapters/adapter-mlv-bible/index.js +400 -0
- package/adapters/chat-mod-adapter.js +532 -0
- package/adapters/content-adapter.js +273 -0
- package/content/api.js +50 -41
- package/content/index.js +2 -2
- package/content/store.js +355 -173
- 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 +334 -74
- 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 +113 -1
- package/mesh/darshan.js +1718 -0
- package/mesh/gumba.js +1567 -0
- package/mesh/jhilke.js +651 -0
- package/mesh/katha.js +1012 -0
- package/mesh/nakpak-routing.js +8 -5
- package/mesh/network.js +724 -34
- package/mesh/pulse-sync.js +4 -1
- package/mesh/rate-limiter.js +127 -15
- 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/vani.js +1364 -0
- package/mesh/yurt.js +1340 -0
- 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/network-identity.js +16 -0
- package/oracle/packet-checksum.js +201 -0
- package/oracle/sst.js +579 -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 +28 -13
- 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-nav.js +194 -0
- package/scripts/update-docs-sidebar.cjs +164 -0
- package/security/crypto-config.js +4 -3
- package/security/dharma-moderation.js +517 -0
- 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 +373 -5
- package/security/namche-gateway.js +298 -69
- package/security/sakshi.js +460 -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/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
|
|
@@ -19,6 +21,12 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlink
|
|
|
19
21
|
import { join, dirname } from 'path';
|
|
20
22
|
import { createLogger } from '../utils/logger.js';
|
|
21
23
|
|
|
24
|
+
// Import iO system for human-readable content names
|
|
25
|
+
import { deriveNetworkName } from '../oracle/network-identity.js';
|
|
26
|
+
|
|
27
|
+
// Import 144T ternary system for hex-free content addressing
|
|
28
|
+
import { TritAddress, TOTAL_TRITS } from '../oracle/ternary-144t.js';
|
|
29
|
+
|
|
22
30
|
const log = createLogger('content:store');
|
|
23
31
|
|
|
24
32
|
/**
|
|
@@ -38,16 +46,21 @@ export const ContentType = {
|
|
|
38
46
|
|
|
39
47
|
/**
|
|
40
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.
|
|
41
54
|
*/
|
|
42
55
|
export const ContentStatus = {
|
|
43
|
-
LOCAL: 'local', //
|
|
44
|
-
|
|
45
|
-
VERIFIED: 'verified', //
|
|
46
|
-
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
|
|
47
59
|
};
|
|
48
60
|
|
|
49
61
|
/**
|
|
50
|
-
* Compute content hash (SHA3-256)
|
|
62
|
+
* Compute content hash (SHA3-256) — returns hex string
|
|
63
|
+
* @deprecated Use computeContentHash144T for new content (hex-free addressing)
|
|
51
64
|
*/
|
|
52
65
|
export function computeContentHash(content) {
|
|
53
66
|
if (typeof content === 'string') {
|
|
@@ -63,83 +76,116 @@ export function computeContentHash(content) {
|
|
|
63
76
|
return bytesToHex(sha3_256(utf8ToBytes(JSON.stringify(content))));
|
|
64
77
|
}
|
|
65
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
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Derive human-readable iO name from content hash
|
|
132
|
+
* Uses 3-word quantum wordlist for memorable, shareable names
|
|
133
|
+
*
|
|
134
|
+
* @param {string} hash - SHA3-256 content hash (hex)
|
|
135
|
+
* @returns {string} Human-readable name like "qubit-lattice-prism"
|
|
136
|
+
*/
|
|
137
|
+
export function deriveContentName(hash) {
|
|
138
|
+
return deriveNetworkName(hash, 3); // 3 words = 24 bits = 16M+ unique names
|
|
139
|
+
}
|
|
140
|
+
|
|
66
141
|
/**
|
|
67
142
|
* Content metadata
|
|
68
143
|
*/
|
|
69
144
|
class ContentMetadata {
|
|
70
145
|
constructor(options = {}) {
|
|
71
|
-
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)
|
|
148
|
+
this.ioName = options.ioName || null; // Auto-generated iO name (human-readable)
|
|
72
149
|
this.contentType = options.contentType || ContentType.BINARY;
|
|
73
150
|
this.size = options.size || 0;
|
|
74
151
|
this.createdAt = options.createdAt || Date.now();
|
|
75
152
|
this.publishedBy = options.publishedBy || null;
|
|
76
153
|
this.status = options.status || ContentStatus.LOCAL;
|
|
77
|
-
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)
|
|
78
156
|
this.tags = options.tags || [];
|
|
79
|
-
this.name = options.name || null; // Optional
|
|
157
|
+
this.name = options.name || null; // Optional custom name (user-provided)
|
|
80
158
|
this.ttl = options.ttl || 0; // 0 = permanent
|
|
81
159
|
}
|
|
82
160
|
|
|
83
161
|
toJSON() {
|
|
84
162
|
return {
|
|
85
163
|
hash: this.hash,
|
|
164
|
+
hash144t: this.hash144t,
|
|
165
|
+
ioName: this.ioName,
|
|
86
166
|
contentType: this.contentType,
|
|
87
167
|
size: this.size,
|
|
88
168
|
createdAt: this.createdAt,
|
|
89
169
|
publishedBy: this.publishedBy,
|
|
90
170
|
status: this.status,
|
|
91
|
-
|
|
171
|
+
publisherSignature: this.publisherSignature,
|
|
172
|
+
publisherBackupSignature: this.publisherBackupSignature,
|
|
92
173
|
tags: this.tags,
|
|
93
174
|
name: this.name,
|
|
94
175
|
ttl: this.ttl,
|
|
95
176
|
};
|
|
96
177
|
}
|
|
97
178
|
|
|
98
|
-
static fromJSON(json) {
|
|
99
|
-
return new ContentMetadata(json);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Consensus proof for light client verification
|
|
105
|
-
*/
|
|
106
|
-
class ConsensusProof {
|
|
107
|
-
constructor(options = {}) {
|
|
108
|
-
this.contentHash = options.contentHash;
|
|
109
|
-
this.timestamp = options.timestamp || Date.now();
|
|
110
|
-
this.validators = options.validators || []; // Array of { nodeId, signature }
|
|
111
|
-
this.quorum = options.quorum || 0; // Required signatures
|
|
112
|
-
this.networkId = options.networkId || null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
179
|
/**
|
|
116
|
-
*
|
|
180
|
+
* Get the public-facing content ID (144T preferred, fallback to iO name)
|
|
181
|
+
* Never returns hex to external callers.
|
|
117
182
|
*/
|
|
118
|
-
|
|
119
|
-
return this.
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Add validator signature
|
|
124
|
-
*/
|
|
125
|
-
addValidator(nodeId, signature) {
|
|
126
|
-
if (!this.validators.find(v => v.nodeId === nodeId)) {
|
|
127
|
-
this.validators.push({ nodeId, signature, timestamp: Date.now() });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
toJSON() {
|
|
132
|
-
return {
|
|
133
|
-
contentHash: this.contentHash,
|
|
134
|
-
timestamp: this.timestamp,
|
|
135
|
-
validators: this.validators,
|
|
136
|
-
quorum: this.quorum,
|
|
137
|
-
networkId: this.networkId,
|
|
138
|
-
};
|
|
183
|
+
getPublicId() {
|
|
184
|
+
return this.hash144t || this.ioName || this.name;
|
|
139
185
|
}
|
|
140
186
|
|
|
141
187
|
static fromJSON(json) {
|
|
142
|
-
return new
|
|
188
|
+
return new ContentMetadata(json);
|
|
143
189
|
}
|
|
144
190
|
}
|
|
145
191
|
|
|
@@ -153,18 +199,17 @@ export class ContentStore {
|
|
|
153
199
|
dataDir: config.dataDir || './data/content',
|
|
154
200
|
maxContentSize: config.maxContentSize || 10 * 1024 * 1024, // 10MB default
|
|
155
201
|
cacheSize: config.cacheSize || 100, // LRU cache entries
|
|
156
|
-
quorumSize: config.quorumSize || 2, // Minimum validators
|
|
157
202
|
...config,
|
|
158
203
|
};
|
|
159
204
|
|
|
160
205
|
this.contentDir = join(this.config.dataDir, 'objects');
|
|
161
206
|
this.metaDir = join(this.config.dataDir, 'meta');
|
|
162
|
-
|
|
207
|
+
|
|
163
208
|
// In-memory caches
|
|
164
209
|
this.contentCache = new Map(); // hash -> content (LRU)
|
|
165
210
|
this.metaCache = new Map(); // hash -> ContentMetadata
|
|
166
211
|
this.nameIndex = new Map(); // name -> hash (for human-readable lookup)
|
|
167
|
-
|
|
212
|
+
|
|
168
213
|
// Mesh integration (set by init)
|
|
169
214
|
this.mesh = null;
|
|
170
215
|
this.identity = null;
|
|
@@ -189,13 +234,13 @@ export class ContentStore {
|
|
|
189
234
|
this.identity = node.identity;
|
|
190
235
|
this.oracle = node.oracle;
|
|
191
236
|
this.gossip = node.gossip;
|
|
192
|
-
|
|
237
|
+
|
|
193
238
|
// Content gossip is handled by the server via mesh.on('rumor')
|
|
194
239
|
// which calls contentStore._handleContentGossip()
|
|
195
240
|
}
|
|
196
241
|
|
|
197
242
|
log.info('Content store initialized', { dataDir: this.config.dataDir, objectCount: this.metaCache.size });
|
|
198
|
-
|
|
243
|
+
|
|
199
244
|
return this;
|
|
200
245
|
}
|
|
201
246
|
|
|
@@ -213,6 +258,18 @@ export class ContentStore {
|
|
|
213
258
|
const json = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
214
259
|
const meta = ContentMetadata.fromJSON(json);
|
|
215
260
|
this.metaCache.set(meta.hash, meta);
|
|
261
|
+
|
|
262
|
+
// Index iO name (auto-generated or derive if missing from old data)
|
|
263
|
+
if (meta.ioName) {
|
|
264
|
+
this.nameIndex.set(meta.ioName, meta.hash);
|
|
265
|
+
} else {
|
|
266
|
+
// Backfill ioName for content stored before iO naming was added
|
|
267
|
+
const ioName = deriveContentName(meta.hash);
|
|
268
|
+
meta.ioName = ioName;
|
|
269
|
+
this.nameIndex.set(ioName, meta.hash);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Index custom name if provided
|
|
216
273
|
if (meta.name) {
|
|
217
274
|
this.nameIndex.set(meta.name, meta.hash);
|
|
218
275
|
}
|
|
@@ -223,34 +280,50 @@ export class ContentStore {
|
|
|
223
280
|
}
|
|
224
281
|
|
|
225
282
|
/**
|
|
226
|
-
* 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.
|
|
227
287
|
*/
|
|
228
288
|
_getContentPath(hash) {
|
|
229
|
-
//
|
|
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)
|
|
230
298
|
const prefix = hash.slice(0, 2);
|
|
231
299
|
const suffix = hash.slice(2);
|
|
232
300
|
return join(this.contentDir, prefix, suffix);
|
|
233
301
|
}
|
|
234
302
|
|
|
235
303
|
/**
|
|
236
|
-
* Get metadata path for a hash
|
|
304
|
+
* Get metadata path for a hash (supports both hex and 144T)
|
|
237
305
|
*/
|
|
238
306
|
_getMetaPath(hash) {
|
|
239
|
-
|
|
307
|
+
// Normalize 144T to filename-safe format (remove separators)
|
|
308
|
+
const safeHash = hash.replace(/[.:]/g, '');
|
|
309
|
+
return join(this.metaDir, `${safeHash}.json`);
|
|
240
310
|
}
|
|
241
311
|
|
|
242
312
|
/**
|
|
243
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.
|
|
244
317
|
*/
|
|
245
318
|
async store(content, options = {}) {
|
|
246
|
-
// Compute hash
|
|
247
|
-
const hash =
|
|
248
|
-
|
|
319
|
+
// Compute both hash formats
|
|
320
|
+
const { hex: hash, trit: hash144t } = computeContentHash144T(content);
|
|
321
|
+
|
|
249
322
|
// Check size limit
|
|
250
|
-
const size = Buffer.isBuffer(content) ? content.length :
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
323
|
+
const size = Buffer.isBuffer(content) ? content.length :
|
|
324
|
+
typeof content === 'string' ? Buffer.byteLength(content) :
|
|
325
|
+
Buffer.byteLength(JSON.stringify(content));
|
|
326
|
+
|
|
254
327
|
if (size > this.config.maxContentSize) {
|
|
255
328
|
throw new Error(`Content exceeds max size: ${size} > ${this.config.maxContentSize}`);
|
|
256
329
|
}
|
|
@@ -258,12 +331,23 @@ export class ContentStore {
|
|
|
258
331
|
// Check if already exists
|
|
259
332
|
if (this.has(hash)) {
|
|
260
333
|
const existing = this.getMeta(hash);
|
|
261
|
-
return {
|
|
334
|
+
return {
|
|
335
|
+
hash,
|
|
336
|
+
hash144t: existing.hash144t || hash144t,
|
|
337
|
+
ioName: existing.ioName,
|
|
338
|
+
status: 'exists',
|
|
339
|
+
meta: existing
|
|
340
|
+
};
|
|
262
341
|
}
|
|
263
342
|
|
|
264
|
-
//
|
|
343
|
+
// Generate iO name for human-readable sharing
|
|
344
|
+
const ioName = deriveContentName(hash);
|
|
345
|
+
|
|
346
|
+
// Create metadata with both hash formats
|
|
265
347
|
const meta = new ContentMetadata({
|
|
266
348
|
hash,
|
|
349
|
+
hash144t,
|
|
350
|
+
ioName,
|
|
267
351
|
contentType: options.contentType || this._detectContentType(content),
|
|
268
352
|
size,
|
|
269
353
|
publishedBy: this.identity?.identity?.nodeId || options.publishedBy || 'unknown',
|
|
@@ -276,7 +360,7 @@ export class ContentStore {
|
|
|
276
360
|
// Write content to disk
|
|
277
361
|
const contentPath = this._getContentPath(hash);
|
|
278
362
|
mkdirSync(dirname(contentPath), { recursive: true });
|
|
279
|
-
|
|
363
|
+
|
|
280
364
|
if (Buffer.isBuffer(content)) {
|
|
281
365
|
writeFileSync(contentPath, content);
|
|
282
366
|
} else if (typeof content === 'string') {
|
|
@@ -290,8 +374,12 @@ export class ContentStore {
|
|
|
290
374
|
|
|
291
375
|
// Update caches
|
|
292
376
|
this.metaCache.set(hash, meta);
|
|
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
|
|
293
381
|
if (meta.name) {
|
|
294
|
-
this.nameIndex.set(meta.name, hash);
|
|
382
|
+
this.nameIndex.set(meta.name, hash); // Custom name if provided
|
|
295
383
|
}
|
|
296
384
|
this._addToContentCache(hash, content);
|
|
297
385
|
|
|
@@ -300,17 +388,28 @@ export class ContentStore {
|
|
|
300
388
|
await this.publish(hash);
|
|
301
389
|
}
|
|
302
390
|
|
|
303
|
-
|
|
391
|
+
log.info('Content stored', { hash144t: hash144t.split('.')[0] + '...', ioName, size });
|
|
392
|
+
return { hash, hash144t, ioName, status: 'stored', meta };
|
|
304
393
|
}
|
|
305
394
|
|
|
306
395
|
/**
|
|
307
|
-
*
|
|
396
|
+
* Resolve any content identifier to internal hex hash.
|
|
397
|
+
* Accepts: hex hash, 144T address, iO name, or custom name.
|
|
398
|
+
* @private
|
|
308
399
|
*/
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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);
|
|
314
413
|
|
|
315
414
|
// Check memory cache
|
|
316
415
|
if (this.contentCache.has(hash)) {
|
|
@@ -326,24 +425,25 @@ export class ContentStore {
|
|
|
326
425
|
// Load and cache
|
|
327
426
|
const content = readFileSync(contentPath);
|
|
328
427
|
this._addToContentCache(hash, content);
|
|
329
|
-
|
|
428
|
+
|
|
330
429
|
return content;
|
|
331
430
|
}
|
|
332
431
|
|
|
333
432
|
/**
|
|
334
|
-
* Get content with metadata and
|
|
433
|
+
* Get content with metadata and verification status
|
|
335
434
|
*/
|
|
336
|
-
getWithProof(
|
|
435
|
+
getWithProof(id) {
|
|
436
|
+
const hash = this._resolveHash(id);
|
|
337
437
|
const content = this.get(hash);
|
|
338
438
|
if (!content) return null;
|
|
339
439
|
|
|
340
440
|
const meta = this.getMeta(hash);
|
|
341
|
-
|
|
441
|
+
|
|
342
442
|
return {
|
|
343
443
|
content,
|
|
344
444
|
hash,
|
|
445
|
+
hash144t: meta?.hash144t || null,
|
|
345
446
|
meta: meta?.toJSON() || null,
|
|
346
|
-
proof: meta?.consensusProof || null,
|
|
347
447
|
verified: meta?.status === ContentStatus.VERIFIED,
|
|
348
448
|
};
|
|
349
449
|
}
|
|
@@ -351,22 +451,16 @@ export class ContentStore {
|
|
|
351
451
|
/**
|
|
352
452
|
* Get metadata for content
|
|
353
453
|
*/
|
|
354
|
-
getMeta(
|
|
355
|
-
|
|
356
|
-
if (!hash.match(/^[a-f0-9]{64}$/i)) {
|
|
357
|
-
hash = this.nameIndex.get(hash) || hash;
|
|
358
|
-
}
|
|
454
|
+
getMeta(id) {
|
|
455
|
+
const hash = this._resolveHash(id);
|
|
359
456
|
return this.metaCache.get(hash) || null;
|
|
360
457
|
}
|
|
361
458
|
|
|
362
459
|
/**
|
|
363
460
|
* Check if content exists
|
|
364
461
|
*/
|
|
365
|
-
has(
|
|
366
|
-
|
|
367
|
-
if (!hash.match(/^[a-f0-9]{64}$/i)) {
|
|
368
|
-
hash = this.nameIndex.get(hash) || hash;
|
|
369
|
-
}
|
|
462
|
+
has(id) {
|
|
463
|
+
const hash = this._resolveHash(id);
|
|
370
464
|
return this.metaCache.has(hash) || existsSync(this._getContentPath(hash));
|
|
371
465
|
}
|
|
372
466
|
|
|
@@ -375,11 +469,11 @@ export class ContentStore {
|
|
|
375
469
|
*/
|
|
376
470
|
delete(hash) {
|
|
377
471
|
const meta = this.getMeta(hash);
|
|
378
|
-
|
|
472
|
+
|
|
379
473
|
// Remove from disk
|
|
380
474
|
const contentPath = this._getContentPath(hash);
|
|
381
475
|
const metaPath = this._getMetaPath(hash);
|
|
382
|
-
|
|
476
|
+
|
|
383
477
|
if (existsSync(contentPath)) unlinkSync(contentPath);
|
|
384
478
|
if (existsSync(metaPath)) unlinkSync(metaPath);
|
|
385
479
|
|
|
@@ -398,28 +492,31 @@ export class ContentStore {
|
|
|
398
492
|
*/
|
|
399
493
|
list(options = {}) {
|
|
400
494
|
const { tag, status, limit = 100, offset = 0 } = options;
|
|
401
|
-
|
|
495
|
+
|
|
402
496
|
let items = Array.from(this.metaCache.values());
|
|
403
|
-
|
|
497
|
+
|
|
404
498
|
// Filter by tag
|
|
405
499
|
if (tag) {
|
|
406
500
|
items = items.filter(m => m.tags.includes(tag));
|
|
407
501
|
}
|
|
408
|
-
|
|
502
|
+
|
|
409
503
|
// Filter by status
|
|
410
504
|
if (status) {
|
|
411
505
|
items = items.filter(m => m.status === status);
|
|
412
506
|
}
|
|
413
|
-
|
|
507
|
+
|
|
414
508
|
// Sort by created date (newest first)
|
|
415
509
|
items.sort((a, b) => b.createdAt - a.createdAt);
|
|
416
|
-
|
|
510
|
+
|
|
417
511
|
// Paginate
|
|
418
512
|
return items.slice(offset, offset + limit).map(m => m.toJSON());
|
|
419
513
|
}
|
|
420
514
|
|
|
421
515
|
/**
|
|
422
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.
|
|
423
520
|
*/
|
|
424
521
|
async publish(hash) {
|
|
425
522
|
const meta = this.getMeta(hash);
|
|
@@ -427,6 +524,25 @@ export class ContentStore {
|
|
|
427
524
|
throw new Error(`Content not found: ${hash}`);
|
|
428
525
|
}
|
|
429
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
|
+
|
|
430
546
|
// Create announcement message
|
|
431
547
|
const announcement = {
|
|
432
548
|
type: 'content_announce',
|
|
@@ -435,17 +551,14 @@ export class ContentStore {
|
|
|
435
551
|
contentType: meta.contentType,
|
|
436
552
|
size: meta.size,
|
|
437
553
|
publishedBy: meta.publishedBy,
|
|
554
|
+
publisherSignature,
|
|
555
|
+
publisherBackupSignature,
|
|
438
556
|
tags: meta.tags,
|
|
439
557
|
name: meta.name,
|
|
440
558
|
},
|
|
441
559
|
timestamp: Date.now(),
|
|
442
560
|
};
|
|
443
561
|
|
|
444
|
-
// Sign with node identity
|
|
445
|
-
if (this.identity) {
|
|
446
|
-
announcement.signature = this.identity.sign(JSON.stringify(announcement));
|
|
447
|
-
}
|
|
448
|
-
|
|
449
562
|
// Gossip to mesh
|
|
450
563
|
if (this.gossip) {
|
|
451
564
|
log.debug('Gossiping content_announce', { hash: hash.slice(0, 16) });
|
|
@@ -454,8 +567,8 @@ export class ContentStore {
|
|
|
454
567
|
log.warn('No gossip protocol available for content announce');
|
|
455
568
|
}
|
|
456
569
|
|
|
457
|
-
// Update status
|
|
458
|
-
meta.status = ContentStatus.
|
|
570
|
+
// Update status to ANNOUNCED (published to mesh)
|
|
571
|
+
meta.status = ContentStatus.ANNOUNCED;
|
|
459
572
|
writeFileSync(this._getMetaPath(hash), JSON.stringify(meta.toJSON(), null, 2));
|
|
460
573
|
|
|
461
574
|
return { published: true, hash };
|
|
@@ -530,13 +643,12 @@ export class ContentStore {
|
|
|
530
643
|
} else {
|
|
531
644
|
contentBase64 = Buffer.from(JSON.stringify(result.content), 'utf8').toString('base64');
|
|
532
645
|
}
|
|
533
|
-
|
|
646
|
+
|
|
534
647
|
this.gossip.spreadRumor('content', {
|
|
535
648
|
type: 'content_response',
|
|
536
649
|
hash: data.hash,
|
|
537
650
|
content: contentBase64,
|
|
538
651
|
meta: result.meta,
|
|
539
|
-
proof: result.proof,
|
|
540
652
|
timestamp: Date.now(),
|
|
541
653
|
});
|
|
542
654
|
}
|
|
@@ -544,76 +656,146 @@ export class ContentStore {
|
|
|
544
656
|
break;
|
|
545
657
|
|
|
546
658
|
case 'content_response':
|
|
547
|
-
// Received content from peer
|
|
659
|
+
// Received content from peer — verify integrity + authorship
|
|
548
660
|
if (!this.has(data.hash)) {
|
|
549
661
|
const content = Buffer.from(data.content, 'base64');
|
|
550
662
|
const computedHash = computeContentHash(content);
|
|
551
|
-
|
|
552
|
-
// Verify hash
|
|
663
|
+
|
|
664
|
+
// Gate 1: Verify hash integrity
|
|
553
665
|
if (computedHash !== data.hash) {
|
|
554
666
|
console.warn(`⚠️ Content hash mismatch from ${origin.slice(0, 16)}...`);
|
|
555
667
|
return;
|
|
556
|
-
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Store it — use explicit allowlist, never spread remote meta directly (proto pollution defense)
|
|
671
|
+
const remoteMeta = data.meta || {};
|
|
557
672
|
await this.store(content, {
|
|
558
|
-
|
|
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,
|
|
559
680
|
publish: false, // Don't re-gossip
|
|
560
681
|
});
|
|
561
682
|
|
|
562
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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;
|
|
568
743
|
}
|
|
569
744
|
|
|
570
|
-
|
|
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 });
|
|
571
747
|
}
|
|
572
748
|
break;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
573
751
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
+
}
|
|
594
778
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
writeFileSync(this._getMetaPath(data.hash), JSON.stringify(meta.toJSON(), null, 2));
|
|
614
|
-
}
|
|
615
|
-
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;
|
|
616
797
|
}
|
|
798
|
+
return null;
|
|
617
799
|
}
|
|
618
800
|
|
|
619
801
|
/**
|
|
@@ -635,9 +817,9 @@ export class ContentStore {
|
|
|
635
817
|
if (typeof content === 'object' && !Buffer.isBuffer(content)) {
|
|
636
818
|
return ContentType.JSON;
|
|
637
819
|
}
|
|
638
|
-
|
|
820
|
+
|
|
639
821
|
const str = content.toString().slice(0, 100);
|
|
640
|
-
|
|
822
|
+
|
|
641
823
|
if (str.startsWith('<!DOCTYPE') || str.startsWith('<html')) {
|
|
642
824
|
return ContentType.HTML;
|
|
643
825
|
}
|
|
@@ -647,7 +829,7 @@ export class ContentStore {
|
|
|
647
829
|
if (str.includes('function') || str.includes('const ') || str.includes('import ')) {
|
|
648
830
|
return ContentType.JAVASCRIPT;
|
|
649
831
|
}
|
|
650
|
-
|
|
832
|
+
|
|
651
833
|
return ContentType.TEXT;
|
|
652
834
|
}
|
|
653
835
|
|
|
@@ -657,14 +839,14 @@ export class ContentStore {
|
|
|
657
839
|
getStats() {
|
|
658
840
|
let totalSize = 0;
|
|
659
841
|
let verified = 0;
|
|
660
|
-
let
|
|
842
|
+
let announced = 0;
|
|
661
843
|
let local = 0;
|
|
662
844
|
|
|
663
845
|
for (const meta of this.metaCache.values()) {
|
|
664
846
|
totalSize += meta.size;
|
|
665
847
|
switch (meta.status) {
|
|
666
848
|
case ContentStatus.VERIFIED: verified++; break;
|
|
667
|
-
case ContentStatus.
|
|
849
|
+
case ContentStatus.ANNOUNCED: announced++; break;
|
|
668
850
|
case ContentStatus.LOCAL: local++; break;
|
|
669
851
|
}
|
|
670
852
|
}
|
|
@@ -673,7 +855,7 @@ export class ContentStore {
|
|
|
673
855
|
totalObjects: this.metaCache.size,
|
|
674
856
|
totalSize,
|
|
675
857
|
verified,
|
|
676
|
-
|
|
858
|
+
announced,
|
|
677
859
|
local,
|
|
678
860
|
cacheSize: this.contentCache.size,
|
|
679
861
|
dataDir: this.config.dataDir,
|
|
@@ -681,5 +863,5 @@ export class ContentStore {
|
|
|
681
863
|
}
|
|
682
864
|
}
|
|
683
865
|
|
|
684
|
-
export { ContentMetadata
|
|
866
|
+
export { ContentMetadata };
|
|
685
867
|
export default ContentStore;
|