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