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/mesh/gumba.js ADDED
@@ -0,0 +1,1567 @@
1
+ /**
2
+ * GUMBA - Guarded Universal Message Bundle Access
3
+ *
4
+ * A novel cryptographic access control system where:
5
+ * - Keys NEVER leave the host node
6
+ * - Access is granted via cryptographic PROOFS, not key distribution
7
+ * - The proof IS the access - not the key
8
+ *
9
+ * ╔═══════════════════════════════════════════════════════════════════════════════╗
10
+ * ║ "The key is not the access. The proof is the access." ║
11
+ * ║ ║
12
+ * ║ Like a guarded temple: You don't get a copy of the master key. ║
13
+ * ║ You prove you belong. The guardian opens the door. ║
14
+ * ║ The door stays locked. The key stays hidden. ║
15
+ * ╚═══════════════════════════════════════════════════════════════════════════════╝
16
+ *
17
+ * SECURITY MODEL:
18
+ * - Bundle Key: Derived deterministically, never transmitted
19
+ * - Membership: Merkle tree of authorized DOKO hashes
20
+ * - Access Proof: Challenge-response, attestation, or ZK proof
21
+ * - Content Delivery: Via ANNEX E2E tunnel after access granted
22
+ *
23
+ * PROOF TYPES:
24
+ * 1. CHALLENGE - Sign a nonce with DOKO private key
25
+ * 2. ATTESTATION - Another member vouches for you (time-limited)
26
+ * 3. MERKLE - Prove membership in tree without revealing identity
27
+ *
28
+ * Part of the Himalayan Protocol Family:
29
+ * - ANNEX: E2E encrypted channels (used for content delivery)
30
+ * - DOKO: Identity certificates (used for membership)
31
+ * - GUMBA: Access control (this module)
32
+ *
33
+ * Named after the Tibetan word for "monastery" - a guarded sacred space
34
+ * where access is granted to those who prove their dedication.
35
+ *
36
+ * @module mesh/gumba
37
+ * @license MIT
38
+ * @copyright 2026 YAKMESH™ Contributors
39
+ */
40
+
41
+ import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
42
+ import { sha3_256 as _nobleSha3 } from '@noble/hashes/sha3.js';
43
+ import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils.js';
44
+ import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
45
+ // ACCEL: Hardware-accelerated crypto
46
+ import { sha3_256, mlDsa65Sign, mlDsa65Verify } from '../utils/accel.js';
47
+ import { createLogger } from '../utils/logger.js';
48
+ import EventEmitter from 'events';
49
+
50
+ const log = createLogger('mesh:gumba');
51
+ const peerTag = (id) => id?.split('-pq-').pop() || id?.slice?.(-8) || String(id);
52
+
53
+ // ═══════════════════════════════════════════════════════════════════════════════
54
+ // CONFIGURATION
55
+ // ═══════════════════════════════════════════════════════════════════════════════
56
+
57
+ export const GUMBA_CONFIG = Object.freeze({
58
+ // Encryption
59
+ symmetricAlgorithm: 'aes-256-gcm',
60
+ nonceSize: 12,
61
+ authTagLength: 16,
62
+
63
+ // Key derivation
64
+ keyDerivationSalt: 'YAKMESH-GUMBA-2026',
65
+ bundleVersion: 1,
66
+
67
+ // Access control
68
+ challengeExpiry: 30000, // 30 second challenge window
69
+ attestationMaxAge: 86400000, // 24 hour attestation validity
70
+ proofCacheTime: 300000, // 5 minute proof cache
71
+
72
+ // Bundle limits
73
+ maxBundleSize: 10 * 1024 * 1024, // 10MB max bundle
74
+ maxMembers: 10000, // Max members per bundle
75
+ maxMessagesPerBundle: 100000, // Max messages before rotation
76
+
77
+ // Message types
78
+ messageTypes: {
79
+ // Access protocol
80
+ CHALLENGE: 'gumba:challenge',
81
+ RESPONSE: 'gumba:response',
82
+ ACCESS_GRANTED: 'gumba:access_granted',
83
+ ACCESS_DENIED: 'gumba:access_denied',
84
+
85
+ // Attestation
86
+ ATTEST: 'gumba:attest',
87
+ REVOKE_ATTEST: 'gumba:revoke_attest',
88
+
89
+ // Content
90
+ CONTENT: 'gumba:content',
91
+ SYNC: 'gumba:sync',
92
+
93
+ // Membership
94
+ MEMBER_ADD: 'gumba:member_add',
95
+ MEMBER_REMOVE: 'gumba:member_remove',
96
+ },
97
+ });
98
+
99
+ /**
100
+ * Proof types for access verification
101
+ */
102
+ export const GUMBA_PROOF_TYPE = Object.freeze({
103
+ CHALLENGE: 'challenge', // Direct challenge-response
104
+ ATTESTATION: 'attestation', // Vouched by existing member
105
+ MERKLE: 'merkle', // ZK merkle inclusion proof
106
+ });
107
+
108
+ /**
109
+ * Member roles in a GUMBA bundle
110
+ */
111
+ export const GUMBA_ROLE = Object.freeze({
112
+ OWNER: 'owner', // Can add/remove members, delete bundle
113
+ ADMIN: 'admin', // Can add/remove members
114
+ MEMBER: 'member', // Can read and write
115
+ READER: 'reader', // Can only read
116
+ });
117
+
118
+ // ═══════════════════════════════════════════════════════════════════════════════
119
+ // GUMBA KEY - Deterministic key derivation (NEVER transmitted)
120
+ // ═══════════════════════════════════════════════════════════════════════════════
121
+
122
+ /**
123
+ * GumbaKey - The bundle encryption key
124
+ *
125
+ * Derived deterministically from owner's secret + bundle ID.
126
+ * This key NEVER leaves the host node. Ever.
127
+ */
128
+ export class GumbaKey {
129
+ /**
130
+ * @param {string} bundleId - Unique bundle identifier
131
+ * @param {Uint8Array} ownerSecret - Owner's secret key material
132
+ */
133
+ constructor(bundleId, ownerSecret) {
134
+ this.bundleId = bundleId;
135
+ this.createdAt = Date.now();
136
+
137
+ // Derive the bundle key (deterministic, reproducible on same node)
138
+ this.key = this._deriveKey(ownerSecret);
139
+
140
+ // Track usage for rotation
141
+ this.messageCount = 0;
142
+ this.lastUsed = Date.now();
143
+ }
144
+
145
+ /**
146
+ * Derive encryption key from owner secret
147
+ * Uses HKDF-like construction with SHA3-256
148
+ */
149
+ _deriveKey(ownerSecret) {
150
+ // Stage 1: Extract
151
+ const prk = createHash('sha3-256')
152
+ .update(GUMBA_CONFIG.keyDerivationSalt)
153
+ .update(ownerSecret)
154
+ .digest();
155
+
156
+ // Stage 2: Expand with bundle context
157
+ const info = Buffer.concat([
158
+ utf8ToBytes(`GUMBA-v${GUMBA_CONFIG.bundleVersion}`),
159
+ utf8ToBytes(':'),
160
+ utf8ToBytes(this.bundleId),
161
+ ]);
162
+
163
+ return createHash('sha3-256')
164
+ .update(prk)
165
+ .update(info)
166
+ .update(Buffer.from([0x01])) // Counter for HKDF
167
+ .digest();
168
+ }
169
+
170
+ /**
171
+ * Encrypt content into the bundle
172
+ */
173
+ seal(plaintext, metadata = {}) {
174
+ const nonce = randomBytes(GUMBA_CONFIG.nonceSize);
175
+ const cipher = createCipheriv(
176
+ GUMBA_CONFIG.symmetricAlgorithm,
177
+ this.key,
178
+ nonce,
179
+ { authTagLength: GUMBA_CONFIG.authTagLength }
180
+ );
181
+
182
+ // Include metadata in AAD for integrity
183
+ const aad = Buffer.from(JSON.stringify({
184
+ bundleId: this.bundleId,
185
+ timestamp: Date.now(),
186
+ ...metadata,
187
+ }));
188
+ cipher.setAAD(aad);
189
+
190
+ const data = typeof plaintext === 'string' ? plaintext : JSON.stringify(plaintext);
191
+ const encrypted = Buffer.concat([
192
+ cipher.update(data, 'utf8'),
193
+ cipher.final(),
194
+ ]);
195
+
196
+ this.messageCount++;
197
+ this.lastUsed = Date.now();
198
+
199
+ return {
200
+ nonce: nonce.toString('hex'),
201
+ ciphertext: encrypted.toString('hex'),
202
+ authTag: cipher.getAuthTag().toString('hex'),
203
+ aad: aad.toString('hex'),
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Decrypt content from the bundle
209
+ * Only called locally - plaintext delivered via ANNEX
210
+ */
211
+ unseal(encrypted) {
212
+ const nonce = Buffer.from(encrypted.nonce, 'hex');
213
+ const ciphertext = Buffer.from(encrypted.ciphertext, 'hex');
214
+ const authTag = Buffer.from(encrypted.authTag, 'hex');
215
+ const aad = Buffer.from(encrypted.aad, 'hex');
216
+
217
+ const decipher = createDecipheriv(
218
+ GUMBA_CONFIG.symmetricAlgorithm,
219
+ this.key,
220
+ nonce,
221
+ { authTagLength: GUMBA_CONFIG.authTagLength }
222
+ );
223
+
224
+ decipher.setAAD(aad);
225
+ decipher.setAuthTag(authTag);
226
+
227
+ const decrypted = Buffer.concat([
228
+ decipher.update(ciphertext),
229
+ decipher.final(),
230
+ ]);
231
+
232
+ return decrypted.toString('utf8');
233
+ }
234
+
235
+ /**
236
+ * Check if key needs rotation
237
+ */
238
+ needsRotation() {
239
+ return this.messageCount >= GUMBA_CONFIG.maxMessagesPerBundle;
240
+ }
241
+
242
+ /**
243
+ * Securely clear key material
244
+ */
245
+ destroy() {
246
+ if (this.key) {
247
+ this.key.fill(0);
248
+ this.key = null;
249
+ }
250
+ }
251
+ }
252
+
253
+ // ═══════════════════════════════════════════════════════════════════════════════
254
+ // GUMBA MEMBER TREE - Merkle tree for membership proofs
255
+ // ═══════════════════════════════════════════════════════════════════════════════
256
+
257
+ /**
258
+ * MemberTree - Merkle tree of authorized DOKO identities
259
+ *
260
+ * Enables:
261
+ * - O(log n) membership verification
262
+ * - Merkle proofs for ZK-style access
263
+ * - Efficient member add/remove
264
+ */
265
+ export class GumbaMemberTree {
266
+ constructor() {
267
+ this.members = new Map(); // dokoId -> { role, addedAt, addedBy }
268
+ this.leafHashes = []; // Sorted leaf hashes
269
+ this.root = null; // Merkle root
270
+ this.dirty = true; // Needs rebuild
271
+ }
272
+
273
+ /**
274
+ * Hash a DOKO ID for tree inclusion
275
+ */
276
+ static hashMember(dokoId, role) {
277
+ return bytesToHex(sha3_256(utf8ToBytes(`${dokoId}:${role}`)));
278
+ }
279
+
280
+ /**
281
+ * Add a member to the tree
282
+ */
283
+ addMember(dokoId, role = GUMBA_ROLE.MEMBER, addedBy = null) {
284
+ if (this.members.size >= GUMBA_CONFIG.maxMembers) {
285
+ throw new Error('Maximum members reached');
286
+ }
287
+
288
+ this.members.set(dokoId, {
289
+ role,
290
+ addedAt: Date.now(),
291
+ addedBy,
292
+ });
293
+
294
+ this.dirty = true;
295
+ log.debug('Member added', { dokoId: dokoId.slice(0, 16), role });
296
+
297
+ return true;
298
+ }
299
+
300
+ /**
301
+ * Remove a member from the tree
302
+ */
303
+ removeMember(dokoId) {
304
+ const removed = this.members.delete(dokoId);
305
+ if (removed) {
306
+ this.dirty = true;
307
+ log.debug('Member removed', { dokoId: dokoId.slice(0, 16) });
308
+ }
309
+ return removed;
310
+ }
311
+
312
+ /**
313
+ * Check if a DOKO is a member
314
+ */
315
+ isMember(dokoId) {
316
+ return this.members.has(dokoId);
317
+ }
318
+
319
+ /**
320
+ * Get member info
321
+ */
322
+ getMember(dokoId) {
323
+ return this.members.get(dokoId) || null;
324
+ }
325
+
326
+ /**
327
+ * Get member role
328
+ */
329
+ getRole(dokoId) {
330
+ return this.members.get(dokoId)?.role || null;
331
+ }
332
+
333
+ /**
334
+ * Check if member has required role
335
+ */
336
+ hasRole(dokoId, requiredRole) {
337
+ const member = this.members.get(dokoId);
338
+ if (!member) return false;
339
+
340
+ const roleHierarchy = [GUMBA_ROLE.READER, GUMBA_ROLE.MEMBER, GUMBA_ROLE.ADMIN, GUMBA_ROLE.OWNER];
341
+ const memberLevel = roleHierarchy.indexOf(member.role);
342
+ const requiredLevel = roleHierarchy.indexOf(requiredRole);
343
+
344
+ return memberLevel >= requiredLevel;
345
+ }
346
+
347
+ /**
348
+ * Rebuild the Merkle tree
349
+ */
350
+ rebuild() {
351
+ if (!this.dirty) return this.root;
352
+
353
+ // Create sorted leaf hashes
354
+ this.leafHashes = Array.from(this.members.entries())
355
+ .map(([dokoId, info]) => GumbaMemberTree.hashMember(dokoId, info.role))
356
+ .sort();
357
+
358
+ if (this.leafHashes.length === 0) {
359
+ this.root = bytesToHex(sha3_256(utf8ToBytes('GUMBA:EMPTY')));
360
+ } else {
361
+ this.root = this._buildTree(this.leafHashes);
362
+ }
363
+
364
+ this.dirty = false;
365
+ return this.root;
366
+ }
367
+
368
+ /**
369
+ * Build Merkle tree recursively
370
+ */
371
+ _buildTree(leaves) {
372
+ if (leaves.length === 1) return leaves[0];
373
+
374
+ const nextLevel = [];
375
+ for (let i = 0; i < leaves.length; i += 2) {
376
+ const left = leaves[i];
377
+ const right = leaves[i + 1] || left; // Duplicate if odd
378
+ const parent = bytesToHex(sha3_256(hexToBytes(left + right)));
379
+ nextLevel.push(parent);
380
+ }
381
+
382
+ return this._buildTree(nextLevel);
383
+ }
384
+
385
+ /**
386
+ * Generate Merkle proof for a member
387
+ */
388
+ getProof(dokoId) {
389
+ const member = this.members.get(dokoId);
390
+ if (!member) return null;
391
+
392
+ this.rebuild();
393
+
394
+ const leafHash = GumbaMemberTree.hashMember(dokoId, member.role);
395
+ const leafIndex = this.leafHashes.indexOf(leafHash);
396
+ if (leafIndex === -1) return null;
397
+
398
+ const proof = [];
399
+ let currentLevel = [...this.leafHashes];
400
+ let index = leafIndex;
401
+
402
+ while (currentLevel.length > 1) {
403
+ const siblingIndex = index % 2 === 0 ? index + 1 : index - 1;
404
+ const sibling = currentLevel[siblingIndex] || currentLevel[index];
405
+
406
+ proof.push({
407
+ hash: sibling,
408
+ position: index % 2 === 0 ? 'right' : 'left',
409
+ });
410
+
411
+ // Move to next level
412
+ const nextLevel = [];
413
+ for (let i = 0; i < currentLevel.length; i += 2) {
414
+ const left = currentLevel[i];
415
+ const right = currentLevel[i + 1] || left;
416
+ nextLevel.push(bytesToHex(sha3_256(hexToBytes(left + right))));
417
+ }
418
+
419
+ currentLevel = nextLevel;
420
+ index = Math.floor(index / 2);
421
+ }
422
+
423
+ return {
424
+ leaf: leafHash,
425
+ root: this.root,
426
+ path: proof,
427
+ };
428
+ }
429
+
430
+ /**
431
+ * Verify a Merkle proof
432
+ */
433
+ static verifyProof(proof) {
434
+ let current = proof.leaf;
435
+
436
+ for (const step of proof.path) {
437
+ const combined = step.position === 'left'
438
+ ? step.hash + current
439
+ : current + step.hash;
440
+ current = bytesToHex(sha3_256(hexToBytes(combined)));
441
+ }
442
+
443
+ return current === proof.root;
444
+ }
445
+
446
+ /**
447
+ * Get the current root
448
+ */
449
+ getRoot() {
450
+ return this.rebuild();
451
+ }
452
+
453
+ /**
454
+ * Get member count
455
+ */
456
+ get size() {
457
+ return this.members.size;
458
+ }
459
+
460
+ /**
461
+ * Export member list (for backup/sync)
462
+ */
463
+ export() {
464
+ return {
465
+ root: this.getRoot(),
466
+ members: Array.from(this.members.entries()).map(([dokoId, info]) => ({
467
+ dokoId,
468
+ ...info,
469
+ })),
470
+ };
471
+ }
472
+
473
+ /**
474
+ * Import member list
475
+ */
476
+ import(data) {
477
+ this.members.clear();
478
+ for (const member of data.members) {
479
+ this.members.set(member.dokoId, {
480
+ role: member.role,
481
+ addedAt: member.addedAt,
482
+ addedBy: member.addedBy,
483
+ });
484
+ }
485
+ this.dirty = true;
486
+ }
487
+ }
488
+
489
+ // ═══════════════════════════════════════════════════════════════════════════════
490
+ // GUMBA PROOF - Access proof generation and verification
491
+ // ═══════════════════════════════════════════════════════════════════════════════
492
+
493
+ /**
494
+ * GumbaProof - Creates and verifies access proofs
495
+ */
496
+ export class GumbaProof {
497
+ /**
498
+ * Create a challenge for proof-of-identity
499
+ */
500
+ static createChallenge(bundleId, targetDokoId) {
501
+ const nonce = bytesToHex(randomBytes(32));
502
+ const timestamp = Date.now();
503
+
504
+ return {
505
+ type: GUMBA_PROOF_TYPE.CHALLENGE,
506
+ bundleId,
507
+ targetDokoId,
508
+ nonce,
509
+ timestamp,
510
+ expiry: timestamp + GUMBA_CONFIG.challengeExpiry,
511
+ };
512
+ }
513
+
514
+ /**
515
+ * Sign a challenge response with DOKO private key
516
+ */
517
+ static signChallenge(challenge, secretKey) {
518
+ const payload = JSON.stringify({
519
+ type: challenge.type,
520
+ bundleId: challenge.bundleId,
521
+ nonce: challenge.nonce,
522
+ timestamp: challenge.timestamp,
523
+ });
524
+
525
+ const payloadBytes = utf8ToBytes(payload);
526
+ // API: sign(message, secretKey)
527
+ const signature = mlDsa65Sign(payloadBytes, secretKey);
528
+
529
+ return {
530
+ ...challenge,
531
+ signature: bytesToHex(signature),
532
+ signedAt: Date.now(),
533
+ };
534
+ }
535
+
536
+ /**
537
+ * Verify a challenge response
538
+ */
539
+ static verifyChallenge(response, publicKey) {
540
+ // Check expiry
541
+ if (Date.now() > response.expiry) {
542
+ return { valid: false, reason: 'EXPIRED' };
543
+ }
544
+
545
+ // Rebuild payload
546
+ const payload = JSON.stringify({
547
+ type: response.type,
548
+ bundleId: response.bundleId,
549
+ nonce: response.nonce,
550
+ timestamp: response.timestamp,
551
+ });
552
+
553
+ // Verify signature
554
+ try {
555
+ const payloadBytes = utf8ToBytes(payload);
556
+ const signatureBytes = hexToBytes(response.signature);
557
+ const publicKeyBytes = typeof publicKey === 'string'
558
+ ? hexToBytes(publicKey)
559
+ : publicKey;
560
+
561
+ // API: verify(signature, message, publicKey)
562
+ const valid = mlDsa65Verify(signatureBytes, payloadBytes, publicKeyBytes);
563
+
564
+ return { valid, reason: valid ? 'OK' : 'INVALID_SIGNATURE' };
565
+ } catch (err) {
566
+ return { valid: false, reason: 'VERIFICATION_ERROR', error: err.message };
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Create an attestation (one member vouches for another)
572
+ */
573
+ static createAttestation(options) {
574
+ const {
575
+ bundleId,
576
+ grantorDokoId,
577
+ granteeDokoId,
578
+ grantorSecretKey,
579
+ expiry = Date.now() + GUMBA_CONFIG.attestationMaxAge,
580
+ grantedRole = GUMBA_ROLE.READER,
581
+ } = options;
582
+
583
+ const attestation = {
584
+ type: GUMBA_PROOF_TYPE.ATTESTATION,
585
+ bundleId,
586
+ grantorDokoId,
587
+ granteeDokoId,
588
+ grantedRole,
589
+ createdAt: Date.now(),
590
+ expiry,
591
+ };
592
+
593
+ // Sign with grantor's key
594
+ const payload = JSON.stringify(attestation);
595
+ const payloadBytes = utf8ToBytes(payload);
596
+ // API: sign(message, secretKey)
597
+ const signature = mlDsa65Sign(payloadBytes, grantorSecretKey);
598
+
599
+ return {
600
+ ...attestation,
601
+ signature: bytesToHex(signature),
602
+ };
603
+ }
604
+
605
+ /**
606
+ * Verify an attestation
607
+ */
608
+ static verifyAttestation(attestation, grantorPublicKey, memberTree) {
609
+ // Check expiry
610
+ if (Date.now() > attestation.expiry) {
611
+ return { valid: false, reason: 'EXPIRED' };
612
+ }
613
+
614
+ // Check grantor is a member with sufficient role
615
+ if (!memberTree.hasRole(attestation.grantorDokoId, GUMBA_ROLE.MEMBER)) {
616
+ return { valid: false, reason: 'GRANTOR_NOT_AUTHORIZED' };
617
+ }
618
+
619
+ // Verify signature
620
+ try {
621
+ const payload = JSON.stringify({
622
+ type: attestation.type,
623
+ bundleId: attestation.bundleId,
624
+ grantorDokoId: attestation.grantorDokoId,
625
+ granteeDokoId: attestation.granteeDokoId,
626
+ grantedRole: attestation.grantedRole,
627
+ createdAt: attestation.createdAt,
628
+ expiry: attestation.expiry,
629
+ });
630
+
631
+ const payloadBytes = utf8ToBytes(payload);
632
+ const signatureBytes = hexToBytes(attestation.signature);
633
+ const publicKeyBytes = typeof grantorPublicKey === 'string'
634
+ ? hexToBytes(grantorPublicKey)
635
+ : grantorPublicKey;
636
+
637
+ // API: verify(signature, message, publicKey)
638
+ const valid = mlDsa65Verify(signatureBytes, payloadBytes, publicKeyBytes);
639
+
640
+ return { valid, reason: valid ? 'OK' : 'INVALID_SIGNATURE' };
641
+ } catch (err) {
642
+ return { valid: false, reason: 'VERIFICATION_ERROR', error: err.message };
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Create a Merkle membership proof
648
+ */
649
+ static createMerkleProof(dokoId, memberTree) {
650
+ const proof = memberTree.getProof(dokoId);
651
+ if (!proof) {
652
+ return null;
653
+ }
654
+
655
+ return {
656
+ type: GUMBA_PROOF_TYPE.MERKLE,
657
+ dokoId, // Could be hidden for true ZK
658
+ proof,
659
+ timestamp: Date.now(),
660
+ };
661
+ }
662
+
663
+ /**
664
+ * Verify a Merkle membership proof
665
+ */
666
+ static verifyMerkleProof(merkleProof, expectedRoot) {
667
+ if (!merkleProof || !merkleProof.proof) {
668
+ return { valid: false, reason: 'INVALID_PROOF' };
669
+ }
670
+
671
+ // Check root matches current tree
672
+ if (merkleProof.proof.root !== expectedRoot) {
673
+ return { valid: false, reason: 'ROOT_MISMATCH' };
674
+ }
675
+
676
+ // Verify the proof path
677
+ const valid = GumbaMemberTree.verifyProof(merkleProof.proof);
678
+
679
+ return { valid, reason: valid ? 'OK' : 'PROOF_INVALID' };
680
+ }
681
+ }
682
+
683
+ // ═══════════════════════════════════════════════════════════════════════════════
684
+ // GUMBA GATE - Access control and content gating
685
+ // ═══════════════════════════════════════════════════════════════════════════════
686
+
687
+ /**
688
+ * GumbaGate - The guardian at the door
689
+ *
690
+ * Verifies proofs and grants/denies access to bundles.
691
+ * "Show your proof. The guardian will decide."
692
+ */
693
+ export class GumbaGate {
694
+ constructor(memberTree, options = {}) {
695
+ this.memberTree = memberTree;
696
+ this.options = options;
697
+
698
+ // Pending challenges awaiting response
699
+ this.pendingChallenges = new Map(); // nonce -> challenge
700
+
701
+ // Proof cache (prevent replay, speed up repeated access)
702
+ this.proofCache = new Map(); // dokoId -> { proof, expiresAt }
703
+
704
+ // Stats
705
+ this.stats = {
706
+ challengesIssued: 0,
707
+ accessGranted: 0,
708
+ accessDenied: 0,
709
+ replaysBlocked: 0,
710
+ };
711
+ }
712
+
713
+ /**
714
+ * Issue a challenge to an access requester
715
+ */
716
+ issueChallenge(bundleId, requesterDokoId) {
717
+ const challenge = GumbaProof.createChallenge(bundleId, requesterDokoId);
718
+
719
+ // Store pending challenge
720
+ this.pendingChallenges.set(challenge.nonce, challenge);
721
+
722
+ // Set expiry cleanup
723
+ setTimeout(() => {
724
+ this.pendingChallenges.delete(challenge.nonce);
725
+ }, GUMBA_CONFIG.challengeExpiry);
726
+
727
+ this.stats.challengesIssued++;
728
+ log.debug('Challenge issued', {
729
+ bundleId,
730
+ requester: requesterDokoId.slice(0, 16),
731
+ nonce: challenge.nonce.slice(0, 16),
732
+ });
733
+
734
+ return challenge;
735
+ }
736
+
737
+ /**
738
+ * Verify an access attempt
739
+ * Returns access decision
740
+ */
741
+ async verifyAccess(proof, getPublicKey) {
742
+ // For challenge-response, check if nonce is valid BEFORE cache
743
+ // This prevents replay attacks where same signed response is used twice
744
+ if (proof.type === GUMBA_PROOF_TYPE.CHALLENGE) {
745
+ const pending = this.pendingChallenges.get(proof.nonce);
746
+ if (!pending) {
747
+ this.stats.replaysBlocked++;
748
+ this.stats.accessDenied++;
749
+ return { granted: false, reason: 'CHALLENGE_NOT_FOUND_OR_EXPIRED' };
750
+ }
751
+ }
752
+
753
+ // Check proof cache (for non-challenge types, or after nonce validation)
754
+ const cached = this._checkCache(proof);
755
+ if (cached) {
756
+ // For challenges, still consume the nonce even if cache hit
757
+ if (proof.type === GUMBA_PROOF_TYPE.CHALLENGE) {
758
+ this.pendingChallenges.delete(proof.nonce);
759
+ }
760
+ log.debug('Access granted from cache', { dokoId: cached.dokoId?.slice(0, 16) });
761
+ this.stats.accessGranted++;
762
+ return { granted: true, reason: 'CACHED', role: cached.role };
763
+ }
764
+
765
+ let result;
766
+
767
+ switch (proof.type) {
768
+ case GUMBA_PROOF_TYPE.CHALLENGE:
769
+ result = await this._verifyChallengeAccess(proof, getPublicKey);
770
+ break;
771
+
772
+ case GUMBA_PROOF_TYPE.ATTESTATION:
773
+ result = await this._verifyAttestationAccess(proof, getPublicKey);
774
+ break;
775
+
776
+ case GUMBA_PROOF_TYPE.MERKLE:
777
+ result = this._verifyMerkleAccess(proof);
778
+ break;
779
+
780
+ default:
781
+ result = { granted: false, reason: 'UNKNOWN_PROOF_TYPE' };
782
+ }
783
+
784
+ // Update stats and cache
785
+ if (result.granted) {
786
+ this.stats.accessGranted++;
787
+ this._cacheProof(proof, result);
788
+ } else {
789
+ this.stats.accessDenied++;
790
+ }
791
+
792
+ log.debug('Access decision', {
793
+ type: proof.type,
794
+ granted: result.granted,
795
+ reason: result.reason,
796
+ });
797
+
798
+ return result;
799
+ }
800
+
801
+ /**
802
+ * Verify challenge-response access
803
+ */
804
+ async _verifyChallengeAccess(response, getPublicKey) {
805
+ // Check if this challenge is pending
806
+ const pending = this.pendingChallenges.get(response.nonce);
807
+ if (!pending) {
808
+ this.stats.replaysBlocked++;
809
+ return { granted: false, reason: 'CHALLENGE_NOT_FOUND_OR_EXPIRED' };
810
+ }
811
+
812
+ // Remove from pending (one-time use)
813
+ this.pendingChallenges.delete(response.nonce);
814
+
815
+ // Check membership
816
+ const dokoId = response.targetDokoId;
817
+ if (!this.memberTree.isMember(dokoId)) {
818
+ return { granted: false, reason: 'NOT_A_MEMBER' };
819
+ }
820
+
821
+ // Get public key
822
+ const publicKey = await getPublicKey(dokoId);
823
+ if (!publicKey) {
824
+ return { granted: false, reason: 'PUBLIC_KEY_NOT_FOUND' };
825
+ }
826
+
827
+ // Verify signature
828
+ const verification = GumbaProof.verifyChallenge(response, publicKey);
829
+ if (!verification.valid) {
830
+ return { granted: false, reason: verification.reason };
831
+ }
832
+
833
+ const role = this.memberTree.getRole(dokoId);
834
+ return { granted: true, reason: 'CHALLENGE_VERIFIED', role, dokoId };
835
+ }
836
+
837
+ /**
838
+ * Verify attestation-based access
839
+ */
840
+ async _verifyAttestationAccess(attestation, getPublicKey) {
841
+ // Get grantor's public key
842
+ const grantorPublicKey = await getPublicKey(attestation.grantorDokoId);
843
+ if (!grantorPublicKey) {
844
+ return { granted: false, reason: 'GRANTOR_KEY_NOT_FOUND' };
845
+ }
846
+
847
+ // Verify attestation
848
+ const verification = GumbaProof.verifyAttestation(
849
+ attestation,
850
+ grantorPublicKey,
851
+ this.memberTree
852
+ );
853
+
854
+ if (!verification.valid) {
855
+ return { granted: false, reason: verification.reason };
856
+ }
857
+
858
+ // Attestation is valid - grantee gets the granted role
859
+ return {
860
+ granted: true,
861
+ reason: 'ATTESTATION_VERIFIED',
862
+ role: attestation.grantedRole,
863
+ dokoId: attestation.granteeDokoId,
864
+ attestedBy: attestation.grantorDokoId,
865
+ };
866
+ }
867
+
868
+ /**
869
+ * Verify Merkle proof access
870
+ */
871
+ _verifyMerkleAccess(merkleProof) {
872
+ const expectedRoot = this.memberTree.getRoot();
873
+ const verification = GumbaProof.verifyMerkleProof(merkleProof, expectedRoot);
874
+
875
+ if (!verification.valid) {
876
+ return { granted: false, reason: verification.reason };
877
+ }
878
+
879
+ const role = this.memberTree.getRole(merkleProof.dokoId);
880
+ return {
881
+ granted: true,
882
+ reason: 'MERKLE_VERIFIED',
883
+ role,
884
+ dokoId: merkleProof.dokoId,
885
+ };
886
+ }
887
+
888
+ /**
889
+ * Check proof cache
890
+ */
891
+ _checkCache(proof) {
892
+ const dokoId = proof.targetDokoId || proof.granteeDokoId || proof.dokoId;
893
+ if (!dokoId) return null;
894
+
895
+ const cached = this.proofCache.get(dokoId);
896
+ if (cached && cached.expiresAt > Date.now()) {
897
+ return cached;
898
+ }
899
+
900
+ // Expired - remove
901
+ if (cached) {
902
+ this.proofCache.delete(dokoId);
903
+ }
904
+
905
+ return null;
906
+ }
907
+
908
+ /**
909
+ * Cache a successful proof
910
+ */
911
+ _cacheProof(proof, result) {
912
+ const dokoId = result.dokoId;
913
+ if (!dokoId) return;
914
+
915
+ this.proofCache.set(dokoId, {
916
+ dokoId,
917
+ role: result.role,
918
+ expiresAt: Date.now() + GUMBA_CONFIG.proofCacheTime,
919
+ });
920
+ }
921
+
922
+ /**
923
+ * Clear proof cache for a DOKO (e.g., on revocation)
924
+ */
925
+ revokeAccess(dokoId) {
926
+ this.proofCache.delete(dokoId);
927
+ log.debug('Access revoked', { dokoId: dokoId.slice(0, 16) });
928
+ }
929
+
930
+ /**
931
+ * Get gate statistics
932
+ */
933
+ getStats() {
934
+ return {
935
+ ...this.stats,
936
+ pendingChallenges: this.pendingChallenges.size,
937
+ cachedProofs: this.proofCache.size,
938
+ memberCount: this.memberTree.size,
939
+ };
940
+ }
941
+ }
942
+
943
+ // ═══════════════════════════════════════════════════════════════════════════════
944
+ // GUMBA BUNDLE - The encrypted content container
945
+ // ═══════════════════════════════════════════════════════════════════════════════
946
+
947
+ /**
948
+ * GumbaBundle - Encrypted message storage
949
+ *
950
+ * "Like a treasure chest in the monastery vault.
951
+ * The key stays with the guardian. Visitors see only what they're shown."
952
+ */
953
+ export class GumbaBundle extends EventEmitter {
954
+ /**
955
+ * @param {string} bundleId - Unique bundle identifier
956
+ * @param {Object} options - Bundle configuration
957
+ */
958
+ constructor(bundleId, options = {}) {
959
+ super();
960
+
961
+ this.bundleId = bundleId;
962
+ this.name = options.name || bundleId;
963
+ this.description = options.description || '';
964
+ this.createdAt = Date.now();
965
+ this.ownerDokoId = options.ownerDokoId;
966
+
967
+ // The key - NEVER LEAVES THIS NODE
968
+ this.key = null;
969
+
970
+ // Membership
971
+ this.memberTree = new GumbaMemberTree();
972
+
973
+ // Access control
974
+ this.gate = new GumbaGate(this.memberTree);
975
+
976
+ // Encrypted content storage
977
+ this.messages = []; // Sealed messages
978
+ this.messageIndex = 0; // Next message ID
979
+
980
+ // Metadata (not encrypted)
981
+ this.metadata = {
982
+ version: GUMBA_CONFIG.bundleVersion,
983
+ createdAt: this.createdAt,
984
+ messageCount: 0,
985
+ lastActivity: this.createdAt,
986
+ };
987
+ }
988
+
989
+ /**
990
+ * Initialize the bundle with owner's secret
991
+ * This derives the bundle key (never transmitted)
992
+ */
993
+ initialize(ownerSecret) {
994
+ this.key = new GumbaKey(this.bundleId, ownerSecret);
995
+
996
+ // Add owner as first member
997
+ if (this.ownerDokoId) {
998
+ this.memberTree.addMember(this.ownerDokoId, GUMBA_ROLE.OWNER);
999
+ }
1000
+
1001
+ log.info('Bundle initialized', {
1002
+ bundleId: this.bundleId,
1003
+ owner: this.ownerDokoId?.slice(0, 16),
1004
+ });
1005
+
1006
+ return this;
1007
+ }
1008
+
1009
+ /**
1010
+ * Add a message to the bundle
1011
+ * @param {Object} content - Message content
1012
+ * @param {string} senderDokoId - Sender's DOKO ID
1013
+ * @returns {Object} Sealed message reference
1014
+ */
1015
+ addMessage(content, senderDokoId) {
1016
+ if (!this.key) {
1017
+ throw new Error('Bundle not initialized');
1018
+ }
1019
+
1020
+ // Verify sender is a member with write access
1021
+ if (!this.memberTree.hasRole(senderDokoId, GUMBA_ROLE.MEMBER)) {
1022
+ throw new Error('SENDER_NOT_AUTHORIZED');
1023
+ }
1024
+
1025
+ const messageId = ++this.messageIndex;
1026
+ const timestamp = Date.now();
1027
+
1028
+ // Create message envelope
1029
+ const envelope = {
1030
+ id: messageId,
1031
+ type: 'message',
1032
+ sender: senderDokoId,
1033
+ content,
1034
+ timestamp,
1035
+ };
1036
+
1037
+ // Seal it
1038
+ const sealed = this.key.seal(envelope);
1039
+
1040
+ // Store
1041
+ this.messages.push({
1042
+ id: messageId,
1043
+ sealed,
1044
+ timestamp,
1045
+ senderHint: senderDokoId.slice(0, 8), // Minimal hint for UI
1046
+ });
1047
+
1048
+ // Update metadata
1049
+ this.metadata.messageCount++;
1050
+ this.metadata.lastActivity = timestamp;
1051
+
1052
+ this.emit('message', { messageId, sender: senderDokoId, timestamp });
1053
+
1054
+ return { messageId, timestamp };
1055
+ }
1056
+
1057
+ /**
1058
+ * Get messages for an authorized accessor
1059
+ * Returns decrypted content (delivered via ANNEX)
1060
+ *
1061
+ * @param {Object} accessResult - Result from gate.verifyAccess()
1062
+ * @param {Object} options - Query options
1063
+ */
1064
+ getMessages(accessResult, options = {}) {
1065
+ if (!accessResult.granted) {
1066
+ throw new Error('ACCESS_DENIED');
1067
+ }
1068
+
1069
+ const { since = 0, limit = 50 } = options;
1070
+
1071
+ // Filter by time/ID
1072
+ let messages = this.messages.filter(m => m.id > since);
1073
+
1074
+ // Apply limit
1075
+ if (limit > 0) {
1076
+ messages = messages.slice(-limit);
1077
+ }
1078
+
1079
+ // Decrypt for accessor
1080
+ const decrypted = messages.map(m => {
1081
+ try {
1082
+ const plaintext = this.key.unseal(m.sealed);
1083
+ return JSON.parse(plaintext);
1084
+ } catch (err) {
1085
+ log.error('Decrypt error', { messageId: m.id, error: err.message });
1086
+ return { id: m.id, error: 'DECRYPT_FAILED' };
1087
+ }
1088
+ });
1089
+
1090
+ return {
1091
+ messages: decrypted,
1092
+ bundleId: this.bundleId,
1093
+ accessedAs: accessResult.role,
1094
+ count: decrypted.length,
1095
+ };
1096
+ }
1097
+
1098
+ /**
1099
+ * Add a member to the bundle
1100
+ */
1101
+ addMember(dokoId, role, adderDokoId) {
1102
+ // Verify adder has permission
1103
+ if (!this.memberTree.hasRole(adderDokoId, GUMBA_ROLE.ADMIN)) {
1104
+ throw new Error('ADDER_NOT_AUTHORIZED');
1105
+ }
1106
+
1107
+ // Can't grant higher role than your own
1108
+ const adderRole = this.memberTree.getRole(adderDokoId);
1109
+ const roleHierarchy = [GUMBA_ROLE.READER, GUMBA_ROLE.MEMBER, GUMBA_ROLE.ADMIN, GUMBA_ROLE.OWNER];
1110
+ if (roleHierarchy.indexOf(role) > roleHierarchy.indexOf(adderRole)) {
1111
+ throw new Error('CANNOT_GRANT_HIGHER_ROLE');
1112
+ }
1113
+
1114
+ this.memberTree.addMember(dokoId, role, adderDokoId);
1115
+ this.emit('member:added', { dokoId, role, addedBy: adderDokoId });
1116
+
1117
+ return true;
1118
+ }
1119
+
1120
+ /**
1121
+ * Remove a member from the bundle
1122
+ */
1123
+ removeMember(dokoId, removerDokoId) {
1124
+ // Verify remover has permission
1125
+ if (!this.memberTree.hasRole(removerDokoId, GUMBA_ROLE.ADMIN)) {
1126
+ throw new Error('REMOVER_NOT_AUTHORIZED');
1127
+ }
1128
+
1129
+ // Can't remove owner
1130
+ if (this.memberTree.getRole(dokoId) === GUMBA_ROLE.OWNER) {
1131
+ throw new Error('CANNOT_REMOVE_OWNER');
1132
+ }
1133
+
1134
+ // Can't remove someone with higher/equal role (unless owner)
1135
+ const removerRole = this.memberTree.getRole(removerDokoId);
1136
+ const targetRole = this.memberTree.getRole(dokoId);
1137
+ const roleHierarchy = [GUMBA_ROLE.READER, GUMBA_ROLE.MEMBER, GUMBA_ROLE.ADMIN, GUMBA_ROLE.OWNER];
1138
+
1139
+ if (removerRole !== GUMBA_ROLE.OWNER &&
1140
+ roleHierarchy.indexOf(targetRole) >= roleHierarchy.indexOf(removerRole)) {
1141
+ throw new Error('CANNOT_REMOVE_EQUAL_OR_HIGHER');
1142
+ }
1143
+
1144
+ this.memberTree.removeMember(dokoId);
1145
+ this.gate.revokeAccess(dokoId);
1146
+ this.emit('member:removed', { dokoId, removedBy: removerDokoId });
1147
+
1148
+ return true;
1149
+ }
1150
+
1151
+ /**
1152
+ * Get bundle info (public metadata)
1153
+ */
1154
+ getInfo() {
1155
+ return {
1156
+ bundleId: this.bundleId,
1157
+ name: this.name,
1158
+ description: this.description,
1159
+ memberCount: this.memberTree.size,
1160
+ messageCount: this.metadata.messageCount,
1161
+ createdAt: this.createdAt,
1162
+ lastActivity: this.metadata.lastActivity,
1163
+ };
1164
+ }
1165
+
1166
+ /**
1167
+ * Export bundle for backup (encrypted)
1168
+ */
1169
+ export() {
1170
+ return {
1171
+ bundleId: this.bundleId,
1172
+ name: this.name,
1173
+ description: this.description,
1174
+ ownerDokoId: this.ownerDokoId,
1175
+ metadata: this.metadata,
1176
+ members: this.memberTree.export(),
1177
+ messages: this.messages, // Still encrypted
1178
+ exportedAt: Date.now(),
1179
+ };
1180
+ }
1181
+
1182
+ /**
1183
+ * Import bundle from backup
1184
+ */
1185
+ static import(data, ownerSecret) {
1186
+ const bundle = new GumbaBundle(data.bundleId, {
1187
+ name: data.name,
1188
+ description: data.description,
1189
+ ownerDokoId: data.ownerDokoId,
1190
+ });
1191
+
1192
+ bundle.initialize(ownerSecret);
1193
+ bundle.memberTree.import(data.members);
1194
+ bundle.messages = data.messages;
1195
+ bundle.metadata = data.metadata;
1196
+ bundle.messageIndex = data.messages.length;
1197
+
1198
+ return bundle;
1199
+ }
1200
+
1201
+ /**
1202
+ * Clean up resources
1203
+ */
1204
+ destroy() {
1205
+ if (this.key) {
1206
+ this.key.destroy();
1207
+ this.key = null;
1208
+ }
1209
+ this.messages = [];
1210
+ this.removeAllListeners();
1211
+ }
1212
+ }
1213
+
1214
+ // ═══════════════════════════════════════════════════════════════════════════════
1215
+ // GUMBA HUB - Multi-bundle management
1216
+ // ═══════════════════════════════════════════════════════════════════════════════
1217
+
1218
+ /**
1219
+ * GumbaHub - Manages multiple GUMBA bundles on a node
1220
+ *
1221
+ * "The monastery courtyard - where all the sacred rooms are accessed"
1222
+ */
1223
+ export class GumbaHub extends EventEmitter {
1224
+ /**
1225
+ * @param {Object} identity - Node identity with signing capability
1226
+ * @param {Object} annex - ANNEX instance for secure delivery
1227
+ * @param {Object} options - Hub configuration
1228
+ */
1229
+ constructor(identity, annex, options = {}) {
1230
+ super();
1231
+
1232
+ this.identity = identity;
1233
+ this.annex = annex;
1234
+ this.options = options;
1235
+
1236
+ // Active bundles
1237
+ this.bundles = new Map(); // bundleId -> GumbaBundle
1238
+
1239
+ // Access sessions
1240
+ this.sessions = new Map(); // sessionId -> { dokoId, bundleIds, expiresAt }
1241
+
1242
+ // Public key registry (for testing/local lookups before KeyResolver integration)
1243
+ this.publicKeys = new Map(); // dokoId -> publicKey
1244
+
1245
+ // KeyResolver: unified key resolution (attached lazily)
1246
+ this.keyResolver = options.keyResolver || null;
1247
+
1248
+ // Stats
1249
+ this.stats = {
1250
+ bundlesCreated: 0,
1251
+ messagesProcessed: 0,
1252
+ accessAttempts: 0,
1253
+ };
1254
+
1255
+ log.info('GumbaHub initialized');
1256
+ }
1257
+
1258
+ /**
1259
+ * Register a DOKO public key for local lookups
1260
+ * Useful for testing or pre-cached keys
1261
+ */
1262
+ registerPublicKey(dokoId, publicKey) {
1263
+ this.publicKeys.set(dokoId, publicKey);
1264
+ }
1265
+
1266
+ /**
1267
+ * Create a new bundle
1268
+ */
1269
+ createBundle(bundleId, options = {}) {
1270
+ if (this.bundles.has(bundleId)) {
1271
+ throw new Error('BUNDLE_EXISTS');
1272
+ }
1273
+
1274
+ const bundle = new GumbaBundle(bundleId, {
1275
+ ...options,
1276
+ ownerDokoId: this.identity.identity.dokoId || this.identity.identity.nodeId,
1277
+ });
1278
+
1279
+ // Initialize with node's secret
1280
+ const ownerSecret = this.identity.identity.secretKey ||
1281
+ this._deriveOwnerSecret(bundleId);
1282
+ bundle.initialize(ownerSecret);
1283
+
1284
+ this.bundles.set(bundleId, bundle);
1285
+ this.stats.bundlesCreated++;
1286
+
1287
+ // Forward events
1288
+ bundle.on('message', (data) => this.emit('bundle:message', { bundleId, ...data }));
1289
+ bundle.on('member:added', (data) => this.emit('bundle:member:added', { bundleId, ...data }));
1290
+ bundle.on('member:removed', (data) => this.emit('bundle:member:removed', { bundleId, ...data }));
1291
+
1292
+ log.info('Bundle created', { bundleId, name: options.name });
1293
+
1294
+ return bundle.getInfo();
1295
+ }
1296
+
1297
+ /**
1298
+ * Get a bundle by ID
1299
+ */
1300
+ getBundle(bundleId) {
1301
+ return this.bundles.get(bundleId) || null;
1302
+ }
1303
+
1304
+ /**
1305
+ * List all bundles (public info only)
1306
+ */
1307
+ listBundles() {
1308
+ return Array.from(this.bundles.values()).map(b => b.getInfo());
1309
+ }
1310
+
1311
+ /**
1312
+ * Handle access request
1313
+ *
1314
+ * Flow:
1315
+ * 1. Visitor presents proof
1316
+ * 2. Gate verifies proof
1317
+ * 3. If granted, create session + deliver via ANNEX
1318
+ */
1319
+ async handleAccessRequest(bundleId, proof, visitorNodeId) {
1320
+ this.stats.accessAttempts++;
1321
+
1322
+ const bundle = this.bundles.get(bundleId);
1323
+ if (!bundle) {
1324
+ return { granted: false, reason: 'BUNDLE_NOT_FOUND' };
1325
+ }
1326
+
1327
+ // Verify access
1328
+ const accessResult = await bundle.gate.verifyAccess(proof, async (dokoId) => {
1329
+ // Get public key from mesh/KHATA
1330
+ return this._getDokoPublicKey(dokoId);
1331
+ });
1332
+
1333
+ if (!accessResult.granted) {
1334
+ log.debug('Access denied', { bundleId, reason: accessResult.reason });
1335
+ return accessResult;
1336
+ }
1337
+
1338
+ // Create session
1339
+ const sessionId = bytesToHex(randomBytes(16));
1340
+ this.sessions.set(sessionId, {
1341
+ dokoId: accessResult.dokoId,
1342
+ role: accessResult.role,
1343
+ bundleId,
1344
+ visitorNodeId,
1345
+ createdAt: Date.now(),
1346
+ expiresAt: Date.now() + GUMBA_CONFIG.proofCacheTime,
1347
+ });
1348
+
1349
+ log.info('Access granted', {
1350
+ bundleId,
1351
+ dokoId: accessResult.dokoId?.slice(0, 16),
1352
+ role: accessResult.role,
1353
+ });
1354
+
1355
+ return {
1356
+ granted: true,
1357
+ sessionId,
1358
+ role: accessResult.role,
1359
+ bundleInfo: bundle.getInfo(),
1360
+ };
1361
+ }
1362
+
1363
+ /**
1364
+ * Get messages for an active session
1365
+ */
1366
+ async getMessages(sessionId, options = {}) {
1367
+ const session = this.sessions.get(sessionId);
1368
+ if (!session) {
1369
+ return { error: 'SESSION_NOT_FOUND' };
1370
+ }
1371
+
1372
+ if (Date.now() > session.expiresAt) {
1373
+ this.sessions.delete(sessionId);
1374
+ return { error: 'SESSION_EXPIRED' };
1375
+ }
1376
+
1377
+ const bundle = this.bundles.get(session.bundleId);
1378
+ if (!bundle) {
1379
+ return { error: 'BUNDLE_NOT_FOUND' };
1380
+ }
1381
+
1382
+ // Get messages (decrypted locally)
1383
+ const accessResult = { granted: true, role: session.role };
1384
+ const result = bundle.getMessages(accessResult, options);
1385
+
1386
+ this.stats.messagesProcessed += result.count;
1387
+
1388
+ // Deliver via ANNEX for E2E encryption to remote visitor
1389
+ if (this.annex && session.visitorNodeId) {
1390
+ try {
1391
+ await this.annex.send(session.visitorNodeId, {
1392
+ type: 'gumba:messages',
1393
+ sessionId,
1394
+ bundleId: session.bundleId,
1395
+ ...result,
1396
+ });
1397
+ return { delivered: true, via: 'annex', count: result.count };
1398
+ } catch (err) {
1399
+ // HARD FAIL: No plaintext fallback. GUMBA content is encrypted for a reason.
1400
+ // Returning decrypted content in plaintext defeats the entire security model.
1401
+ log.error('ANNEX delivery failed — refusing plaintext return', {
1402
+ visitor: peerTag(session.visitorNodeId),
1403
+ error: err.message,
1404
+ });
1405
+ return { error: 'ENCRYPTION_REQUIRED', message: 'ANNEX session required for content delivery' };
1406
+ }
1407
+ }
1408
+
1409
+ return result;
1410
+ }
1411
+
1412
+ /**
1413
+ * Post a message to a bundle
1414
+ */
1415
+ async postMessage(sessionId, content) {
1416
+ const session = this.sessions.get(sessionId);
1417
+ if (!session) {
1418
+ return { error: 'SESSION_NOT_FOUND' };
1419
+ }
1420
+
1421
+ if (Date.now() > session.expiresAt) {
1422
+ this.sessions.delete(sessionId);
1423
+ return { error: 'SESSION_EXPIRED' };
1424
+ }
1425
+
1426
+ const bundle = this.bundles.get(session.bundleId);
1427
+ if (!bundle) {
1428
+ return { error: 'BUNDLE_NOT_FOUND' };
1429
+ }
1430
+
1431
+ try {
1432
+ const result = bundle.addMessage(content, session.dokoId);
1433
+ return { success: true, ...result };
1434
+ } catch (err) {
1435
+ return { error: err.message };
1436
+ }
1437
+ }
1438
+
1439
+ /**
1440
+ * Issue a challenge for a bundle
1441
+ */
1442
+ issueChallenge(bundleId, requesterDokoId) {
1443
+ const bundle = this.bundles.get(bundleId);
1444
+ if (!bundle) {
1445
+ return { error: 'BUNDLE_NOT_FOUND' };
1446
+ }
1447
+
1448
+ return bundle.gate.issueChallenge(bundleId, requesterDokoId);
1449
+ }
1450
+
1451
+ /**
1452
+ * Delete a bundle
1453
+ */
1454
+ deleteBundle(bundleId, requesterDokoId) {
1455
+ const bundle = this.bundles.get(bundleId);
1456
+ if (!bundle) {
1457
+ return { error: 'BUNDLE_NOT_FOUND' };
1458
+ }
1459
+
1460
+ // Only owner can delete
1461
+ if (!bundle.memberTree.hasRole(requesterDokoId, GUMBA_ROLE.OWNER)) {
1462
+ return { error: 'NOT_AUTHORIZED' };
1463
+ }
1464
+
1465
+ bundle.destroy();
1466
+ this.bundles.delete(bundleId);
1467
+
1468
+ // Invalidate related sessions
1469
+ for (const [sessionId, session] of this.sessions) {
1470
+ if (session.bundleId === bundleId) {
1471
+ this.sessions.delete(sessionId);
1472
+ }
1473
+ }
1474
+
1475
+ log.info('Bundle deleted', { bundleId });
1476
+ return { success: true };
1477
+ }
1478
+
1479
+ /**
1480
+ * Get hub statistics
1481
+ */
1482
+ getStats() {
1483
+ return {
1484
+ ...this.stats,
1485
+ activeBundles: this.bundles.size,
1486
+ activeSessions: this.sessions.size,
1487
+ };
1488
+ }
1489
+
1490
+ /**
1491
+ * Derive owner secret for a bundle
1492
+ * Uses node identity to derive deterministic secret
1493
+ */
1494
+ _deriveOwnerSecret(bundleId) {
1495
+ return createHash('sha3-256')
1496
+ .update('GUMBA-OWNER')
1497
+ .update(this.identity.identity.nodeId)
1498
+ .update(bundleId)
1499
+ .digest();
1500
+ }
1501
+
1502
+ /**
1503
+ * Get DOKO public key — unified resolution cascade
1504
+ *
1505
+ * Resolution order:
1506
+ * 1. Local publicKeys map (test/pre-cached)
1507
+ * 2. Own identity
1508
+ * 3. KeyResolver (DOKO cache, peers, SHERPA, etc.)
1509
+ */
1510
+ async _getDokoPublicKey(dokoId) {
1511
+ // Check local registry first (backwards compat)
1512
+ if (this.publicKeys.has(dokoId)) {
1513
+ return this.publicKeys.get(dokoId);
1514
+ }
1515
+
1516
+ // Check if it's our own identity
1517
+ if (this.identity.identity.dokoId === dokoId) {
1518
+ return this.identity.identity.publicKey;
1519
+ }
1520
+
1521
+ // KeyResolver: unified key resolution
1522
+ if (this.keyResolver) {
1523
+ const key = this.keyResolver.resolve(dokoId);
1524
+ if (key) return key;
1525
+ }
1526
+
1527
+ log.warn('DOKO public key not found', { dokoId: dokoId.slice(0, 16) });
1528
+ return null;
1529
+ }
1530
+
1531
+ /**
1532
+ * Cleanup expired sessions
1533
+ */
1534
+ cleanupSessions() {
1535
+ const now = Date.now();
1536
+ let cleaned = 0;
1537
+
1538
+ for (const [sessionId, session] of this.sessions) {
1539
+ if (now > session.expiresAt) {
1540
+ this.sessions.delete(sessionId);
1541
+ cleaned++;
1542
+ }
1543
+ }
1544
+
1545
+ if (cleaned > 0) {
1546
+ log.debug('Cleaned expired sessions', { count: cleaned });
1547
+ }
1548
+
1549
+ return cleaned;
1550
+ }
1551
+ }
1552
+
1553
+ // ═══════════════════════════════════════════════════════════════════════════════
1554
+ // EXPORTS
1555
+ // ═══════════════════════════════════════════════════════════════════════════════
1556
+
1557
+ export default {
1558
+ GumbaKey,
1559
+ GumbaMemberTree,
1560
+ GumbaProof,
1561
+ GumbaGate,
1562
+ GumbaBundle,
1563
+ GumbaHub,
1564
+ GUMBA_CONFIG,
1565
+ GUMBA_PROOF_TYPE,
1566
+ GUMBA_ROLE,
1567
+ };