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.
Files changed (232) hide show
  1. package/CHANGELOG.md +637 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/Caddyfile +77 -0
  4. package/README.md +119 -29
  5. package/adapters/adapter-mlv-bible/README.md +124 -0
  6. package/adapters/adapter-mlv-bible/index.js +400 -0
  7. package/adapters/chat-mod-adapter.js +532 -0
  8. package/adapters/content-adapter.js +273 -0
  9. package/content/api.js +50 -41
  10. package/content/index.js +2 -2
  11. package/content/store.js +355 -173
  12. package/dashboard/index.html +19 -3
  13. package/database/replication.js +117 -37
  14. package/docs/CRYPTO-AGILITY.md +204 -0
  15. package/docs/MTLS-RESEARCH.md +367 -0
  16. package/docs/NAMCHE-SPEC.md +681 -0
  17. package/docs/PEERQUANTA-YAKMESH-INTEGRATION.md +407 -0
  18. package/docs/PRECISION-DISCLOSURE.md +96 -0
  19. package/docs/README.md +76 -0
  20. package/docs/ROADMAP-2.4.0.md +447 -0
  21. package/docs/ROADMAP-2.5.0.md +244 -0
  22. package/docs/SECURITY-AUDIT-REPORT.md +306 -0
  23. package/docs/SST-INTEGRATION.md +712 -0
  24. package/docs/STEADYWATCH-IMPLEMENTATION.md +303 -0
  25. package/docs/TERNARY-AUDIT-REPORT.md +247 -0
  26. package/docs/TME-FAQ.md +221 -0
  27. package/docs/WHITEPAPER.md +623 -0
  28. package/docs/adapters.html +1001 -0
  29. package/docs/advanced-systems.html +1045 -0
  30. package/docs/annex.html +1046 -0
  31. package/docs/api.html +970 -0
  32. package/docs/business/response-templates.md +160 -0
  33. package/docs/c2c.html +1225 -0
  34. package/docs/cli.html +1332 -0
  35. package/docs/configuration.html +1248 -0
  36. package/docs/darshan.html +1085 -0
  37. package/docs/dharma.html +966 -0
  38. package/docs/docs-bundle.html +1075 -0
  39. package/docs/docs.css +3120 -0
  40. package/docs/docs.js +556 -0
  41. package/docs/doko.html +969 -0
  42. package/docs/geo-proof.html +858 -0
  43. package/docs/getting-started.html +840 -0
  44. package/docs/gumba-tutorial.html +1144 -0
  45. package/docs/gumba.html +1098 -0
  46. package/docs/index.html +914 -0
  47. package/docs/jhilke.html +1312 -0
  48. package/docs/karma.html +1100 -0
  49. package/docs/katha.html +1037 -0
  50. package/docs/lama.html +978 -0
  51. package/docs/mandala.html +1067 -0
  52. package/docs/mani.html +964 -0
  53. package/docs/mantra.html +967 -0
  54. package/docs/mesh.html +1409 -0
  55. package/docs/nakpak.html +869 -0
  56. package/docs/namche.html +928 -0
  57. package/docs/nav-order.json +53 -0
  58. package/docs/prahari.html +1043 -0
  59. package/docs/prism-bash.min.js +1 -0
  60. package/docs/prism-javascript.min.js +1 -0
  61. package/docs/prism-json.min.js +1 -0
  62. package/docs/prism-tomorrow.min.css +1 -0
  63. package/docs/prism.min.js +1 -0
  64. package/docs/privacy.html +699 -0
  65. package/docs/quick-reference.html +1181 -0
  66. package/docs/sakshi.html +1402 -0
  67. package/docs/sandboxing.md +386 -0
  68. package/docs/seva.html +911 -0
  69. package/docs/sherpa.html +871 -0
  70. package/docs/studio.html +860 -0
  71. package/docs/stupa.html +995 -0
  72. package/docs/tailwind.min.css +2 -0
  73. package/docs/tattva.html +1332 -0
  74. package/docs/terms.html +686 -0
  75. package/docs/time-server-deployment.md +166 -0
  76. package/docs/time-sources.html +1392 -0
  77. package/docs/tivra.html +1127 -0
  78. package/docs/trademark-policy.html +686 -0
  79. package/docs/tribhuj.html +1183 -0
  80. package/docs/trust-security.html +1029 -0
  81. package/docs/tutorials/backup-recovery.html +654 -0
  82. package/docs/tutorials/dashboard.html +604 -0
  83. package/docs/tutorials/domain-setup.html +605 -0
  84. package/docs/tutorials/host-website.html +456 -0
  85. package/docs/tutorials/mesh-network.html +505 -0
  86. package/docs/tutorials/mobile-access.html +445 -0
  87. package/docs/tutorials/privacy.html +467 -0
  88. package/docs/tutorials/raspberry-pi.html +600 -0
  89. package/docs/tutorials/security-basics.html +539 -0
  90. package/docs/tutorials/share-files.html +431 -0
  91. package/docs/tutorials/troubleshooting.html +637 -0
  92. package/docs/tutorials/trust-karma.html +419 -0
  93. package/docs/tutorials/yak-protocol.html +456 -0
  94. package/docs/tutorials.html +1034 -0
  95. package/docs/vani.html +1270 -0
  96. package/docs/webserver.html +809 -0
  97. package/docs/yak-protocol.html +940 -0
  98. package/docs/yak-timeserver-design.md +475 -0
  99. package/docs/yakapp.html +1015 -0
  100. package/docs/ypc27.html +1069 -0
  101. package/docs/yurt.html +1344 -0
  102. package/embedded-docs/bundle.js +334 -74
  103. package/gossip/protocol.js +247 -27
  104. package/identity/key-resolver.js +262 -0
  105. package/identity/machine-seed.js +632 -0
  106. package/identity/node-key.js +669 -368
  107. package/identity/tribhuj-ratchet.js +506 -0
  108. package/knowledge-base.js +37 -8
  109. package/launcher/yakmesh.bat +62 -0
  110. package/launcher/yakmesh.sh +70 -0
  111. package/mesh/annex.js +462 -108
  112. package/mesh/beacon-broadcast.js +113 -1
  113. package/mesh/darshan.js +1718 -0
  114. package/mesh/gumba.js +1567 -0
  115. package/mesh/jhilke.js +651 -0
  116. package/mesh/katha.js +1012 -0
  117. package/mesh/nakpak-routing.js +8 -5
  118. package/mesh/network.js +724 -34
  119. package/mesh/pulse-sync.js +4 -1
  120. package/mesh/rate-limiter.js +127 -15
  121. package/mesh/seva.js +526 -0
  122. package/mesh/sherpa-discovery.js +89 -8
  123. package/mesh/sybil-defense.js +19 -5
  124. package/mesh/temporal-encoder.js +4 -3
  125. package/mesh/vani.js +1364 -0
  126. package/mesh/yurt.js +1340 -0
  127. package/models/entropy-sentinel.onnx +0 -0
  128. package/models/karma-trust.onnx +0 -0
  129. package/models/manifest.json +43 -0
  130. package/models/sakshi-anomaly.onnx +0 -0
  131. package/oracle/code-proof-protocol.js +7 -6
  132. package/oracle/codebase-lock.js +257 -28
  133. package/oracle/index.js +74 -15
  134. package/oracle/ma902-snmp.js +678 -0
  135. package/oracle/module-sealer.js +5 -3
  136. package/oracle/network-identity.js +16 -0
  137. package/oracle/packet-checksum.js +201 -0
  138. package/oracle/sst.js +579 -0
  139. package/oracle/ternary-144t.js +714 -0
  140. package/oracle/ternary-ml.js +481 -0
  141. package/oracle/time-api.js +239 -0
  142. package/oracle/time-source.js +137 -47
  143. package/oracle/validation-oracle-hardened.js +1111 -1071
  144. package/oracle/validation-oracle.js +4 -2
  145. package/oracle/ypc27.js +211 -0
  146. package/package.json +20 -3
  147. package/protocol/yak-handler.js +35 -9
  148. package/protocol/yak-protocol.js +28 -13
  149. package/reference/cpp/yakmesh_mceliece_shard.cpp +168 -0
  150. package/reference/cpp/yakmesh_ypc27.cpp +179 -0
  151. package/sbom.json +87 -0
  152. package/scripts/security-audit.mjs +264 -0
  153. package/scripts/update-docs-nav.js +194 -0
  154. package/scripts/update-docs-sidebar.cjs +164 -0
  155. package/security/crypto-config.js +4 -3
  156. package/security/dharma-moderation.js +517 -0
  157. package/security/doko-identity.js +193 -143
  158. package/security/domain-consensus.js +86 -85
  159. package/security/fs-hardening.js +620 -0
  160. package/security/hardware-attestation.js +5 -3
  161. package/security/hybrid-trust.js +227 -87
  162. package/security/karma-rate-limiter.js +692 -0
  163. package/security/khata-protocol.js +22 -21
  164. package/security/khata-trust-integration.js +277 -150
  165. package/security/memory-safety.js +635 -0
  166. package/security/mesh-auth.js +11 -10
  167. package/security/mesh-revocation.js +373 -5
  168. package/security/namche-gateway.js +298 -69
  169. package/security/sakshi.js +460 -3
  170. package/security/sangha.js +770 -0
  171. package/security/secure-config.js +473 -0
  172. package/security/silicon-parity.js +13 -10
  173. package/security/steadywatch.js +1142 -0
  174. package/security/strike-system.js +32 -3
  175. package/security/temporal-signing.js +488 -0
  176. package/security/trit-commitment.js +464 -0
  177. package/server/crypto/annex.js +247 -0
  178. package/server/darshan-api.js +343 -0
  179. package/server/index.js +3259 -362
  180. package/server/komm-api.js +668 -0
  181. package/utils/accel.js +2273 -0
  182. package/utils/ternary-id.js +79 -0
  183. package/utils/verify-worker.js +57 -0
  184. package/webserver/index.js +95 -5
  185. package/assets/yakmesh-logo.png +0 -0
  186. package/assets/yakmesh-logo.svg +0 -80
  187. package/assets/yakmesh-logo2.png +0 -0
  188. package/assets/yakmesh-logo2sm.png +0 -0
  189. package/assets/ymsm.png +0 -0
  190. package/website/assets/silhouettes/adapters.svg +0 -107
  191. package/website/assets/silhouettes/api-endpoints.svg +0 -115
  192. package/website/assets/silhouettes/atomic-clock.svg +0 -83
  193. package/website/assets/silhouettes/base-camp.svg +0 -81
  194. package/website/assets/silhouettes/bridge.svg +0 -69
  195. package/website/assets/silhouettes/docs-bundle.svg +0 -113
  196. package/website/assets/silhouettes/doko-basket.svg +0 -70
  197. package/website/assets/silhouettes/fortress.svg +0 -93
  198. package/website/assets/silhouettes/gateway.svg +0 -54
  199. package/website/assets/silhouettes/gears.svg +0 -93
  200. package/website/assets/silhouettes/globe-satellite.svg +0 -67
  201. package/website/assets/silhouettes/karma-wheel.svg +0 -137
  202. package/website/assets/silhouettes/lama-council.svg +0 -141
  203. package/website/assets/silhouettes/mandala-network.svg +0 -169
  204. package/website/assets/silhouettes/mani-stones.svg +0 -149
  205. package/website/assets/silhouettes/mantra-wheel.svg +0 -116
  206. package/website/assets/silhouettes/mesh-nodes.svg +0 -113
  207. package/website/assets/silhouettes/nakpak.svg +0 -56
  208. package/website/assets/silhouettes/peak-lightning.svg +0 -73
  209. package/website/assets/silhouettes/sherpa.svg +0 -69
  210. package/website/assets/silhouettes/stupa-tower.svg +0 -119
  211. package/website/assets/silhouettes/tattva-eye.svg +0 -78
  212. package/website/assets/silhouettes/terminal.svg +0 -74
  213. package/website/assets/silhouettes/webserver.svg +0 -145
  214. package/website/assets/silhouettes/yak.svg +0 -78
  215. package/website/assets/yakmesh-logo.png +0 -0
  216. package/website/assets/yakmesh-logo.webp +0 -0
  217. package/website/assets/yakmesh-logo128x140.webp +0 -0
  218. package/website/assets/yakmesh-logo2.png +0 -0
  219. package/website/assets/yakmesh-logo2.svg +0 -51
  220. package/website/assets/yakmesh-logo40x44.webp +0 -0
  221. package/website/assets/yakmesh.gif +0 -0
  222. package/website/assets/yakmesh.ico +0 -0
  223. package/website/assets/yakmesh.jpg +0 -0
  224. package/website/assets/yakmesh.pdf +0 -0
  225. package/website/assets/yakmesh.png +0 -0
  226. package/website/assets/yakmesh.svg +0 -70
  227. package/website/assets/yakmesh128.webp +0 -0
  228. package/website/assets/yakmesh32.png +0 -0
  229. package/website/assets/yakmesh32.svg +0 -65
  230. package/website/assets/yakmesh32o.ico +0 -2
  231. package/website/assets/yakmesh32o.svg +0 -65
  232. 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 consensus proofs
3
+ * Content-addressed storage with integrity verification
4
4
  *
5
- * Provides public content delivery while maintaining mesh security:
6
- * - Content addressed by hash (trustless verification)
7
- * - Consensus proofs for light client verification
8
- * - Edge caching for instant public access
9
- * - Mesh sync for decentralized replication
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', // Only on this node
44
- PENDING: 'pending', // Awaiting consensus
45
- VERIFIED: 'verified', // Consensus reached
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.consensusProof = options.consensusProof || null;
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 human-readable name
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
- consensusProof: this.consensusProof,
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
- * Check if proof has quorum
180
+ * Get the public-facing content ID (144T preferred, fallback to iO name)
181
+ * Never returns hex to external callers.
117
182
  */
118
- hasQuorum() {
119
- return this.validators.length >= this.quorum;
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 ConsensusProof(json);
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
- // Store in subdirectories for filesystem efficiency (git-style)
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
- return join(this.metaDir, `${hash}.json`);
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 = computeContentHash(content);
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
- typeof content === 'string' ? Buffer.byteLength(content) :
252
- Buffer.byteLength(JSON.stringify(content));
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 { hash, status: 'exists', meta: existing };
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
- // Create metadata
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
- return { hash, status: 'stored', meta };
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
- * Retrieve content by hash
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
- get(hash) {
310
- // Resolve name to hash if needed
311
- if (!hash.match(/^[a-f0-9]{64}$/i)) {
312
- hash = this.nameIndex.get(hash) || hash;
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 proof
433
+ * Get content with metadata and verification status
335
434
  */
336
- getWithProof(hash) {
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(hash) {
355
- // Resolve name if needed
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(hash) {
366
- // Resolve name if needed
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.PENDING;
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
- } // Store it
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
- ...data.meta,
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
- // Apply consensus proof if present
563
- if (data.proof) {
564
- const meta = this.getMeta(data.hash);
565
- meta.consensusProof = ConsensusProof.fromJSON(data.proof);
566
- meta.status = data.proof.hasQuorum?.() ? ContentStatus.VERIFIED : ContentStatus.PENDING;
567
- writeFileSync(this._getMetaPath(data.hash), JSON.stringify(meta.toJSON(), null, 2));
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
- log.info('Content received', { hash: data.hash.slice(0, 16) });
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
- case 'content_validate':
575
- // Peer is requesting validation vote
576
- if (this.has(data.hash) && this.identity && this.oracle) {
577
- const content = this.get(data.hash);
578
- const isValid = this.oracle.validateContent(content, data.contentType);
579
-
580
- if (isValid) {
581
- // Sign validation
582
- const vote = {
583
- type: 'content_vote',
584
- hash: data.hash,
585
- nodeId: this.identity.identity.nodeId,
586
- vote: 'valid',
587
- signature: this.identity.sign(data.hash),
588
- timestamp: Date.now(),
589
- };
590
- this.gossip.spreadRumor('content', vote);
591
- }
592
- }
593
- break;
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
- case 'content_vote':
596
- // Received validation vote
597
- const meta = this.getMeta(data.hash);
598
- if (meta) {
599
- if (!meta.consensusProof) {
600
- meta.consensusProof = new ConsensusProof({
601
- contentHash: data.hash,
602
- quorum: this.config.quorumSize,
603
- networkId: this.mesh?.networkId,
604
- });
605
- }
606
- meta.consensusProof.addValidator(data.nodeId, data.signature);
607
-
608
- if (meta.consensusProof.hasQuorum()) {
609
- meta.status = ContentStatus.VERIFIED;
610
- log.info('Content verified (quorum reached)', { hash: data.hash.slice(0, 16) });
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 pending = 0;
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.PENDING: pending++; break;
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
- pending,
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, ConsensusProof };
866
+ export { ContentMetadata };
685
867
  export default ContentStore;