enigma-memory 0.1.17 → 0.1.22

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 (290) hide show
  1. package/README.md +85 -27
  2. package/apps/cli/bin/enigma-desktop.mjs +140 -0
  3. package/apps/cli/bin/enigma-terminal.mjs +78 -0
  4. package/apps/cli/bin/enigma.mjs +4979 -3263
  5. package/apps/desktop/electron-main.cjs +217 -0
  6. package/apps/desktop/package.json +12 -0
  7. package/apps/desktop/src/app.js +264 -7
  8. package/apps/desktop/src/index.html +3514 -1373
  9. package/apps/desktop/src/launch-electron.mjs +51 -0
  10. package/apps/desktop/src/server.mjs +2914 -0
  11. package/apps/desktop/src/styles.css +2972 -260
  12. package/apps/desktop/src/zk-browser-prove.mjs +53 -0
  13. package/apps/desktop/src/zk-state.mjs +1789 -0
  14. package/apps/gateway/bin/enigma-gateway.mjs +102 -5
  15. package/apps/gateway/src/server.mjs +271 -8
  16. package/apps/ios/EnigmaCore/Package.swift +12 -0
  17. package/apps/ios/EnigmaCore/Sources/EnigmaCore/EnigmaAPIClient.swift +227 -0
  18. package/apps/ios/EnigmaCore/Sources/EnigmaCore/Models.swift +278 -0
  19. package/apps/ios/EnigmaCore/Sources/EnigmaCore/PKCE.swift +96 -0
  20. package/apps/ios/EnigmaCore/Sources/EnigmaCore/PrivacyMinimizer.swift +187 -0
  21. package/apps/ios/EnigmaCore/Sources/EnigmaCore/ToolModels.swift +129 -0
  22. package/apps/ios/EnigmaCore/Tests/EnigmaCoreTests/EnigmaCoreTests.swift +42 -0
  23. package/apps/ios/EnigmaIOS/Enigma/AppModel.swift +346 -0
  24. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AccentColor.colorset/Contents.json +12 -0
  25. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/Contents.json +11 -0
  26. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/EnigmaAppIcon.png +0 -0
  27. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/Contents.json +3 -0
  28. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/LaunchBackground.colorset/Contents.json +12 -0
  29. package/apps/ios/EnigmaIOS/Enigma/ChatView.swift +181 -0
  30. package/apps/ios/EnigmaIOS/Enigma/CouncilView.swift +78 -0
  31. package/apps/ios/EnigmaIOS/Enigma/CreateView.swift +152 -0
  32. package/apps/ios/EnigmaIOS/Enigma/EnigmaApp.swift +52 -0
  33. package/apps/ios/EnigmaIOS/Enigma/Info.plist +52 -0
  34. package/apps/ios/EnigmaIOS/Enigma/NaturalLanguagePrivacyTagger.swift +26 -0
  35. package/apps/ios/EnigmaIOS/Enigma/OAuthClient.swift +321 -0
  36. package/apps/ios/EnigmaIOS/Enigma/OnboardingView.swift +105 -0
  37. package/apps/ios/EnigmaIOS/Enigma/PrivateVaultView.swift +275 -0
  38. package/apps/ios/EnigmaIOS/Enigma/SecureStore.swift +76 -0
  39. package/apps/ios/EnigmaIOS/Enigma/SettingsView.swift +60 -0
  40. package/apps/ios/EnigmaIOS/Enigma/Theme.swift +80 -0
  41. package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/project.pbxproj +211 -0
  42. package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/xcshareddata/xcschemes/Enigma.xcscheme +23 -0
  43. package/apps/native-host/README.md +19 -8
  44. package/apps/native-host/bin/enigma-native-host.mjs +229 -13
  45. package/apps/relay/bin/enigma-relay.mjs +103 -5
  46. package/apps/relay/src/federation-runtime.mjs +618 -0
  47. package/apps/relay/src/server.mjs +310 -9
  48. package/apps/verifier/bin/enigma-verify.mjs +327 -11
  49. package/cortex-v3/circuits/build/intent_vk_bytes.json +35 -0
  50. package/cortex-v3/circuits/build/sale_vk_bytes.json +35 -0
  51. package/cortex-v3/circuits/build/vk_bytes.json +32 -0
  52. package/cortex-v3/proving-assets.json +64 -0
  53. package/cortex-v3/zk/BUILD-CONTRACT.md +87 -0
  54. package/cortex-v3/zk/action-transition-vk.json +119 -0
  55. package/cortex-v3/zk/alias-adversarial.test.mjs +220 -0
  56. package/cortex-v3/zk/groth16-verify-child.mjs +17 -0
  57. package/cortex-v3/zk/intent-witness.mjs +365 -0
  58. package/cortex-v3/zk/intent-witness.test.mjs +485 -0
  59. package/cortex-v3/zk/proving-assets.mjs +203 -0
  60. package/cortex-v3/zk/sale-witness.mjs +783 -0
  61. package/cortex-v3/zk/sale-witness.test.mjs +784 -0
  62. package/cortex-v3/zk/sealed-sale-release-vk.json +119 -0
  63. package/cortex-v3/zk/settlement-evidence.mjs +722 -0
  64. package/cortex-v3/zk/setup-intent.mjs +688 -0
  65. package/cortex-v3/zk/setup-sale.mjs +666 -0
  66. package/cortex-v3/zk/setup.mjs +594 -0
  67. package/cortex-v3/zk/witness.mjs +184 -0
  68. package/cortex-v3/zk/zk-codec.mjs +232 -0
  69. package/cortex-v3/zk/zk-codec.test.mjs +293 -0
  70. package/cortex-v3/zk/zk-settle.mjs +370 -0
  71. package/cortex-v3/zk/zk-tree.mjs +256 -0
  72. package/cortex-v3/zk/zk-tree.test.mjs +419 -0
  73. package/deploy/SIMULATION.md +14 -9
  74. package/deploy/docker-compose.local-production-simulation.yml +54 -12
  75. package/docs/benchmark-attestation-network.md +487 -487
  76. package/docs/benchmark-reproducibility.md +289 -289
  77. package/docs/blockchain-only-mechanisms.md +400 -400
  78. package/docs/browser-extension-install.md +8 -6
  79. package/docs/client-connectors.md +16 -12
  80. package/docs/demo-proof-network.md +275 -275
  81. package/docs/developer-ecosystem.md +15 -13
  82. package/docs/developer-proof-quickstart.md +325 -325
  83. package/docs/enigma-memory-ready-conformance.md +378 -376
  84. package/docs/install-anywhere.md +64 -30
  85. package/docs/installers-and-desktop.md +8 -7
  86. package/docs/memory-benchmarks.md +1 -1
  87. package/docs/memory-drive-health-model.md +690 -690
  88. package/docs/novelty-invention-candidates.md +161 -161
  89. package/docs/proof-network-build-notes.md +240 -240
  90. package/docs/proof-network-claim-boundaries.md +320 -318
  91. package/docs/proof-network.md +339 -339
  92. package/docs/sdk-api.md +324 -324
  93. package/docs/solana-proof-rail.md +453 -453
  94. package/examples/01-quickstart-agent/index.mjs +49 -0
  95. package/examples/01_agent_memory_quickstart.mjs +57 -0
  96. package/examples/02-multi-agent-swarm/index.mjs +57 -0
  97. package/examples/02_cross_model_passport.mjs +64 -0
  98. package/examples/03-langchain-memory/index.mjs +41 -0
  99. package/examples/03_poseidon_commitment_verification.mjs +71 -0
  100. package/examples/04-python-trading-agent/trader.py +49 -0
  101. package/examples/README.md +27 -0
  102. package/examples/ci/github-actions.yml +7 -2
  103. package/package.json +410 -278
  104. package/packages/adapters/PACKAGE_CONTRACT.md +1 -1
  105. package/packages/connectors/src/index.js +196 -4
  106. package/packages/connectors/swarm-router.mjs +168 -0
  107. package/packages/core/src/index.js +249 -2
  108. package/packages/core/src/version.mjs +7 -0
  109. package/packages/dev-tools/package.json +19 -0
  110. package/packages/dev-tools/src/index.js +4 -0
  111. package/packages/dev-tools/src/memory-benchmark-suite.js +112 -0
  112. package/packages/dev-tools/src/swarm-simulator.js +101 -0
  113. package/packages/dev-tools/src/vault-inspector.js +114 -0
  114. package/packages/dev-tools/src/vector-benchmark.js +100 -0
  115. package/packages/developer-platform/src/access-credentials.js +341 -0
  116. package/packages/developer-platform/src/http.js +132 -0
  117. package/packages/developer-platform/src/index.js +4 -0
  118. package/packages/developer-platform/src/usage-http.js +60 -0
  119. package/packages/developer-platform/src/usage.js +295 -0
  120. package/packages/enclave-runtime/attestation.mjs +159 -0
  121. package/packages/enclave-runtime/index.mjs +47 -0
  122. package/packages/enclave-runtime/session-manager.mjs +253 -0
  123. package/packages/enclave-runtime/zeroization-proof.mjs +227 -0
  124. package/packages/enigma-reflex/package.json +14 -0
  125. package/packages/enigma-reflex/src/index.js +204 -0
  126. package/packages/enigma-reflex/training/generate-dataset.mjs +40 -0
  127. package/packages/enigma-reflex/training/requirements.txt +8 -0
  128. package/packages/enigma-reflex/training/train.py +314 -0
  129. package/packages/enigma-weave/LICENSE +22 -0
  130. package/packages/enigma-weave/UPSTREAM.json +21 -0
  131. package/packages/enigma-weave/package.json +14 -0
  132. package/packages/enigma-weave/src/index.js +286 -0
  133. package/packages/hosted-cloud/src/index.js +80 -5
  134. package/packages/importers/src/index.js +432 -0
  135. package/packages/inference-runtime/src/browser.js +401 -0
  136. package/packages/inference-runtime/src/chat.js +265 -0
  137. package/packages/inference-runtime/src/code.js +407 -0
  138. package/packages/inference-runtime/src/contracts.js +162 -0
  139. package/packages/inference-runtime/src/http.js +232 -0
  140. package/packages/inference-runtime/src/image.js +186 -0
  141. package/packages/inference-runtime/src/index.js +10 -0
  142. package/packages/inference-runtime/src/model-router.js +320 -0
  143. package/packages/inference-runtime/src/platform.js +125 -0
  144. package/packages/inference-runtime/src/privacy.js +400 -0
  145. package/packages/inference-runtime/src/video.js +253 -0
  146. package/packages/mcp-server/README.md +22 -6
  147. package/packages/mcp-server/bin/enigma-mcp.mjs +2 -1
  148. package/packages/mcp-server/src/index.js +2498 -1185
  149. package/packages/mcp-server/src/oauth.js +561 -0
  150. package/packages/mcp-server/src/private-handoff.js +84 -0
  151. package/packages/mcp-server/src/remote-http.js +273 -0
  152. package/packages/mcp-server/src/remote-policy.js +72 -0
  153. package/packages/mcp-server/swarm-bridge.mjs +361 -0
  154. package/packages/mesh/index.d.ts +283 -0
  155. package/packages/mesh/package.json +23 -0
  156. package/packages/mesh/src/crypto.js +189 -0
  157. package/packages/mesh/src/federation-packets.js +353 -0
  158. package/packages/mesh/src/gossip.js +311 -0
  159. package/packages/mesh/src/index.js +6 -0
  160. package/packages/mesh/src/protocol.js +255 -0
  161. package/packages/mesh/src/router.js +279 -0
  162. package/packages/mesh/src/transport.js +306 -0
  163. package/packages/passport/src/index.js +436 -7
  164. package/packages/private-economy/src/credits-http.js +100 -0
  165. package/packages/private-economy/src/credits.js +447 -0
  166. package/packages/private-economy/src/index.js +5 -0
  167. package/packages/private-economy/src/payments-http.js +120 -0
  168. package/packages/private-economy/src/payments.js +509 -0
  169. package/packages/private-economy/src/x402.js +346 -0
  170. package/packages/proof-network/PACKAGE_CONTRACT.md +21 -0
  171. package/packages/rag/index.d.ts +182 -0
  172. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/THIRD_PARTY_LICENSES.txt +207 -0
  173. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/config.json +25 -0
  174. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/onnx/model_quantized.onnx +0 -0
  175. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/sha256-manifest.json +28 -0
  176. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer.json +30686 -0
  177. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer_config.json +15 -0
  178. package/packages/rag/package.json +27 -0
  179. package/packages/rag/src/blinded-search.js +109 -0
  180. package/packages/rag/src/bm25.js +169 -0
  181. package/packages/rag/src/embeddings.js +459 -0
  182. package/packages/rag/src/hybrid.js +76 -0
  183. package/packages/rag/src/index.js +38 -0
  184. package/packages/rag/src/reranker.js +61 -0
  185. package/packages/rag/src/research.js +107 -0
  186. package/packages/rag/src/vector-store.js +430 -0
  187. package/packages/rag/src/verify-model-artifacts.mjs +4 -0
  188. package/packages/sdk/index.d.ts +760 -0
  189. package/packages/sdk/package.json +33 -0
  190. package/packages/sdk/python/README.md +24 -0
  191. package/packages/sdk/python/enigma_sdk.py +250 -0
  192. package/packages/sdk/python/pyproject.toml +34 -0
  193. package/packages/sdk/python/requirements.txt +1 -0
  194. package/packages/sdk/python/setup.py +20 -0
  195. package/packages/sdk/src/federation/capability-grant.js +389 -0
  196. package/packages/sdk/src/federation/federation-router.js +360 -0
  197. package/packages/sdk/src/federation/ghostmesh-bridge.js +497 -0
  198. package/packages/sdk/src/federation/index.js +3 -0
  199. package/packages/sdk/src/index.js +1796 -0
  200. package/packages/sdk/src/intelligence/contradiction.js +337 -0
  201. package/packages/sdk/src/intelligence/decision-engine.js +155 -0
  202. package/packages/sdk/src/intelligence/index.js +4 -0
  203. package/packages/sdk/src/intelligence/ontology.js +122 -0
  204. package/packages/sdk/src/intelligence/temporal.js +123 -0
  205. package/packages/sdk/src/market-client.js +142 -0
  206. package/packages/sdk/src/mesh-client.js +110 -0
  207. package/packages/sdk/src/middleware/index.js +3 -0
  208. package/packages/sdk/src/middleware/langchain.js +159 -0
  209. package/packages/sdk/src/middleware/llamaindex.js +101 -0
  210. package/packages/sdk/src/middleware/vercel-ai.js +112 -0
  211. package/packages/sdk/src/rag-client.js +85 -0
  212. package/packages/sdk/src/swarm-orchestrator.js +260 -0
  213. package/packages/settlement/PACKAGE_CONTRACT.md +1 -1
  214. package/packages/snapcompact/THIRD_PARTY_LICENSES.txt +40 -0
  215. package/packages/snapcompact/assets/8x13-latin1.bdf +3837 -0
  216. package/packages/snapcompact/index.d.ts +284 -0
  217. package/packages/snapcompact/package.json +25 -0
  218. package/packages/snapcompact/src/index.js +716 -0
  219. package/packages/storage/PACKAGE_CONTRACT.md +1 -1
  220. package/packages/terminal-console/animations.mjs +240 -0
  221. package/packages/terminal-console/auto-anchor.mjs +220 -0
  222. package/packages/terminal-console/banner.mjs +91 -0
  223. package/packages/terminal-console/commands.mjs +459 -0
  224. package/packages/terminal-console/delegation.mjs +152 -0
  225. package/packages/terminal-console/index.mjs +5 -0
  226. package/packages/terminal-console/outbox.mjs +143 -0
  227. package/packages/terminal-console/phantom-bridge.mjs +637 -0
  228. package/packages/terminal-console/repl.mjs +136 -0
  229. package/packages/terminal-console/signer-store.mjs +130 -0
  230. package/packages/terminal-console/solana-rpc.mjs +214 -0
  231. package/packages/terminal-console/solana-transport.mjs +189 -0
  232. package/packages/terminal-tui/dashboard.mjs +214 -0
  233. package/packages/terminal-tui/index.mjs +28 -0
  234. package/packages/terminal-tui/merkle-tree-renderer.mjs +268 -0
  235. package/packages/terminal-tui/telemetry-hud.mjs +137 -0
  236. package/packages/vault/index.d.ts +449 -0
  237. package/packages/vault/package.json +27 -0
  238. package/packages/vault/src/e2ee.mjs +393 -0
  239. package/packages/vault/src/enclave.js +481 -0
  240. package/packages/vault/src/erasure.js +207 -0
  241. package/packages/vault/src/index.js +1150 -125
  242. package/packages/vault/src/persistence.js +307 -0
  243. package/packages/vault/src/poseidon.js +354 -0
  244. package/packages/vault/src/receipt.js +459 -0
  245. package/scripts/benchmark-optical-context.mjs +166 -0
  246. package/scripts/bootstrap-enigma.mjs +502 -0
  247. package/scripts/build-edge-backend-workers.mjs +20 -5
  248. package/scripts/build-goal-completion-audit.mjs +72 -25
  249. package/scripts/build-hosted-api-key-lifecycle.mjs +292 -274
  250. package/scripts/build-hosted-customer-lifecycle.mjs +493 -476
  251. package/scripts/build-hosted-probe-worker.mjs +19 -4
  252. package/scripts/build-installer-assets.mjs +409 -389
  253. package/scripts/build-operator-evidence-starter.mjs +59 -1
  254. package/scripts/build-production-backend-env-kit.mjs +2 -0
  255. package/scripts/build-production-unblocker.mjs +4 -1
  256. package/scripts/build-proof-network-packet.mjs +213 -213
  257. package/scripts/check.mjs +25 -4
  258. package/scripts/collect-hosted-backend-live-evidence.mjs +49 -12
  259. package/scripts/install-enigma-local.mjs +18 -5
  260. package/scripts/release-audit.mjs +74 -115
  261. package/scripts/release-provenance.mjs +12 -2
  262. package/scripts/run-backend-readiness-smoke.mjs +112 -10
  263. package/scripts/run-standard-memory-benchmarks.mjs +1354 -1352
  264. package/scripts/scan-secrets.mjs +178 -0
  265. package/scripts/simulate-production-env.mjs +71 -10
  266. package/scripts/validate-hosted-backend-live.mjs +112 -1
  267. package/specs/antibody-pack-v1.schema.json +95 -0
  268. package/specs/antigen-envelope-v1.schema.json +81 -0
  269. package/specs/boundary-manifest-v1.schema.json +35 -35
  270. package/specs/capsule-v1.schema.json +55 -55
  271. package/specs/claim-boundary-manifest-v1.schema.json +22 -22
  272. package/specs/claim-ledger-v1.schema.json +291 -0
  273. package/specs/context-passport-v1.schema.json +59 -0
  274. package/specs/deletion-tombstone-v1.schema.json +26 -26
  275. package/specs/evidence-packet-v1.schema.json +177 -0
  276. package/specs/hosted-backend-live-evidence-v1.schema.json +72 -3
  277. package/specs/immune-scan-report-v1.schema.json +112 -0
  278. package/specs/lifecycle-receipt-log-v1.schema.json +67 -0
  279. package/specs/memory-atom-v1.schema.json +59 -0
  280. package/specs/memory-event-v1.schema.json +42 -42
  281. package/specs/passport-v1.schema.json +50 -50
  282. package/specs/proof-of-non-use-v1.schema.json +65 -0
  283. package/specs/quarantine-record-v1.schema.json +126 -0
  284. package/specs/receipt-v1.schema.json +61 -61
  285. package/specs/state-checkpoint-v1.schema.json +37 -37
  286. package/specs/trust-bundle-v1.schema.json +56 -56
  287. package/specs/trust-card-v1.schema.json +119 -0
  288. package/docs/proof-network-launch-plan.md +0 -421
  289. package/packages/metering/PACKAGE_CONTRACT.md +0 -20
  290. package/scripts/build-ai-orchestration-plan.mjs +0 -248
@@ -3,24 +3,32 @@ import {
3
3
  createDecipheriv,
4
4
  createHash,
5
5
  createHmac,
6
+ createPrivateKey,
7
+ createPublicKey,
6
8
  generateKeyPairSync,
9
+ pbkdf2Sync,
7
10
  randomBytes,
8
11
  randomUUID,
12
+ scryptSync,
9
13
  } from 'node:crypto';
10
14
  import {
11
15
  canonicalize,
12
- createMemoryAddress,
13
16
  createReceipt,
14
17
  generateSigningKeyPair,
15
18
  MerkleSet,
16
19
  hmacSha256Hex,
17
20
  receiptHash,
18
21
  sha256Hex,
22
+ verifyReceiptChain,
19
23
  } from '../../core/src/index.js';
20
24
 
21
- const VAULT_SCHEMA = 'enigma.vault.local.v1';
22
- const EVENT_SCHEMA = 'enigma.memory_event.v1';
23
- const MEMORY_DOMAIN = 'enigma.memory.v1';
25
+ export const VAULT_SCHEMA = 'enigma.vault.local.v1';
26
+ export const VAULT_BUNDLE_SCHEMA = 'enigma.vault_bundle.v1';
27
+ export const MEMORY_OBJECT_SCHEMA = 'enigma.memory_object.v1';
28
+ export const EVENT_SCHEMA = 'enigma.memory_event.v1';
29
+ const MEMORY_DOMAIN = 'enigma.memory.v2';
30
+ const CONTENT_AAD_DOMAIN = 'enigma.memory.content_aad.v1';
31
+ const PRIVATE_MEMORY_SCHEMA = 'enigma.memory_private.v1';
24
32
  const ZERO_ROOT = new MerkleSet().root();
25
33
  const RECEIPT_PLAINTEXT_KEYS = new Set(['content', 'plaintext', 'text', 'memory', 'prompt', 'response']);
26
34
  const PUBLIC_PLAINTEXT_KEYS = new Set([
@@ -45,7 +53,26 @@ const PUBLIC_PLAINTEXT_KEYS = new Set([
45
53
  ]);
46
54
  const SAFE_METADATA_DIGEST_RE = /^(?:sha256:)?[a-f0-9]{64}$|^hmac-sha256:[a-f0-9]{64}$/;
47
55
  const SOURCE_REF_ROOT_RE = /^sha256:[a-f0-9]{64}$/;
48
-
56
+ export const KEYRING_ENCRYPTED_TYPE = 'local_secret_encrypted';
57
+ export const KEYRING_PLAINTEXT_TYPE = 'local_secret';
58
+ export const DEFAULT_KDF_SPEC = {
59
+ algorithm: 'scrypt',
60
+ n: 16384,
61
+ r: 8,
62
+ p: 1,
63
+ maxmem: 64 * 1024 * 1024,
64
+ };
65
+ export const PBKDF2_KDF_SPEC = {
66
+ algorithm: 'pbkdf2',
67
+ iterations: 600000,
68
+ hash: 'sha256',
69
+ };
70
+ const DERIVED_KEY_LENGTH = 128; // 32 bytes each: KEK, vaultKey, addressKey, signingSeed
71
+ const SALT_LENGTH = 16;
72
+ const IV_LENGTH = 12;
73
+ const PLAINTEXT_KEYRING_WARNING = 'WARNING: This bundle stores vault keys in plaintext. Anyone with file access can decrypt memory contents. Use --passphrase to encrypt the keyring.';
74
+ const ENCRYPTED_KEYRING_WARNING = 'Keyring is encrypted with AES-256-GCM; passphrase required on import.';
75
+ const DEPRECATION_PLAINTEXT_KEYRING = 'Plaintext keyrings are deprecated; use --passphrase to encrypt new bundles.';
49
76
  function normalizedPublicKey(key) {
50
77
  return String(key).toLowerCase().replace(/[^a-z0-9]/g, '');
51
78
  }
@@ -87,44 +114,54 @@ function isSafeMetadataPrimitive(key, value) {
87
114
  return false;
88
115
  }
89
116
 
90
- function commitMetadataValue(out, key, value) {
91
- out[metadataCommitmentKey(key)] = asSha256(isMetadataPrimitive(value) ? String(value) : canonical(value));
117
+ function privateValueCommitment(key, value) {
118
+ const encoded = typeof value === 'string' ? value : canonical(value);
119
+ return key ? `sha256:${asHmacSha256(key, encoded)}` : asSha256(encoded);
120
+ }
121
+
122
+ function commitMetadataValue(out, key, value, commitmentKey) {
123
+ out[metadataCommitmentKey(key)] = privateValueCommitment(
124
+ commitmentKey,
125
+ isMetadataPrimitive(value) ? String(value) : value,
126
+ );
92
127
  }
93
128
 
94
- function sanitizeSourceRefValue(value) {
129
+ function sanitizeSourceRefValue(value, commitmentKey) {
95
130
  if (value === undefined || value === null) return undefined;
96
- if (typeof value === 'string') return asSha256(value);
131
+ if (typeof value === 'string') {
132
+ return SAFE_METADATA_DIGEST_RE.test(value) ? value : privateValueCommitment(commitmentKey, value);
133
+ }
97
134
  if (['number', 'boolean'].includes(typeof value)) return value;
98
- if (Array.isArray(value)) return value.map(sanitizeSourceRefValue).filter((item) => item !== undefined);
135
+ if (Array.isArray(value)) return value.map((item) => sanitizeSourceRefValue(item, commitmentKey)).filter((item) => item !== undefined);
99
136
  if (typeof value !== 'object') return undefined;
100
137
 
101
138
  const out = {};
102
139
  for (const [key, item] of Object.entries(value)) {
103
140
  if (isSourceRefPlaintextLikeKey(key)) {
104
141
  if (item !== undefined && item !== null && ['string', 'number', 'boolean'].includes(typeof item)) {
105
- out[privateCommitmentKey(key)] = asSha256(String(item));
142
+ out[privateCommitmentKey(key)] = privateValueCommitment(commitmentKey, String(item));
106
143
  }
107
144
  continue;
108
145
  }
109
- if (key === 'source_hash' && typeof item === 'string' && SOURCE_REF_ROOT_RE.test(item)) {
146
+ if (key === 'source_hash' && typeof item === 'string' && SAFE_METADATA_DIGEST_RE.test(item)) {
110
147
  out[key] = item;
111
148
  continue;
112
149
  }
113
- const sanitized = sanitizeSourceRefValue(item);
150
+ const sanitized = sanitizeSourceRefValue(item, commitmentKey);
114
151
  if (sanitized !== undefined) out[key] = sanitized;
115
152
  }
116
153
  return Object.keys(out).length > 0 ? out : undefined;
117
154
  }
118
155
 
119
- function sanitizeSourceRefs(refs) {
156
+ function sanitizeSourceRefs(refs, commitmentKey) {
120
157
  if (!Array.isArray(refs)) return [];
121
158
  const out = [];
122
159
  for (const ref of refs) {
123
160
  if (typeof ref === 'string') {
124
- out.push({ source_hash: asSha256(ref) });
161
+ out.push({ source_hash: privateValueCommitment(commitmentKey, ref) });
125
162
  continue;
126
163
  }
127
- const sanitized = sanitizeSourceRefValue(ref);
164
+ const sanitized = sanitizeSourceRefValue(ref, commitmentKey);
128
165
  if (sanitized !== undefined) out.push(sanitized);
129
166
  }
130
167
  return out;
@@ -175,12 +212,15 @@ function receiptEvent(receipt) {
175
212
  function deriveReceiptLifecycle(receipts) {
176
213
  const active = new Set();
177
214
  const tombstones = new Map();
215
+ let activeRoot = computeSetRoot(active);
178
216
  for (const receipt of receipts) {
179
217
  const event = receiptEvent(receipt);
180
218
  if (!event) continue;
219
+ let activeChanged = false;
181
220
  if (event.operation === 'create' && event.memory_addr) {
182
221
  active.add(event.memory_addr);
183
222
  tombstones.delete(event.memory_addr);
223
+ activeChanged = true;
184
224
  } else if (event.operation === 'update' && event.memory_addr) {
185
225
  if (event.source_addr) {
186
226
  active.delete(event.source_addr);
@@ -195,6 +235,7 @@ function deriveReceiptLifecycle(receipts) {
195
235
  }
196
236
  active.add(event.memory_addr);
197
237
  tombstones.delete(event.memory_addr);
238
+ activeChanged = true;
198
239
  } else if (event.operation === 'delete' && event.memory_addr) {
199
240
  active.delete(event.memory_addr);
200
241
  tombstones.set(event.memory_addr, {
@@ -204,8 +245,14 @@ function deriveReceiptLifecycle(receipts) {
204
245
  tombstoned_at: event.timestamp,
205
246
  reason: 'deleted',
206
247
  });
248
+ activeChanged = true;
249
+ } else if (event.operation === 'key_reset') {
250
+ active.clear();
251
+ tombstones.clear();
252
+ activeChanged = true;
207
253
  }
208
- if (typeof receipt.active_set_root === 'string' && receipt.active_set_root !== computeSetRoot(active)) {
254
+ if (activeChanged) activeRoot = computeSetRoot(active);
255
+ if (typeof receipt.active_set_root === 'string' && receipt.active_set_root !== activeRoot) {
209
256
  throw new Error('importBundle receipt active_set_root mismatch');
210
257
  }
211
258
  }
@@ -313,7 +360,169 @@ function keyBytes(key, label) {
313
360
  throw new Error(`${label} must be a Buffer or base64 string`);
314
361
  }
315
362
 
316
- function signingPair() {
363
+ function buildEd25519Pkcs8(seed) {
364
+ if (!Buffer.isBuffer(seed) || seed.length !== 32) throw new Error('Ed25519 seed must be 32 bytes');
365
+ // PKCS#8 PrivateKeyInfo for Ed25519 (OID 1.3.101.112): SEQUENCE { INTEGER 0, AlgorithmIdentifier, OCTET STRING { OCTET STRING seed } }
366
+ return Buffer.from([
367
+ 0x30, 0x2e,
368
+ 0x02, 0x01, 0x00,
369
+ 0x30, 0x05,
370
+ 0x06, 0x03, 0x2b, 0x65, 0x70,
371
+ 0x04, 0x22,
372
+ 0x04, 0x20,
373
+ ...seed,
374
+ ]);
375
+ }
376
+
377
+ export function validateKdfSpec(kdfSpec = DEFAULT_KDF_SPEC) {
378
+ const algorithm = kdfSpec?.algorithm ?? 'scrypt';
379
+ if (algorithm === 'scrypt') {
380
+ const n = kdfSpec?.n ?? 16384;
381
+ const r = kdfSpec?.r ?? 8;
382
+ const p = kdfSpec?.p ?? 1;
383
+ const maxmem = kdfSpec?.maxmem ?? 64 * 1024 * 1024;
384
+ if (!Number.isInteger(n) || n < 16384 || n > 131072 || (n & (n - 1)) !== 0) {
385
+ throw new Error('scrypt n must be a power of two from 16384 through 131072');
386
+ }
387
+ if (!Number.isInteger(r) || r < 1 || r > 32) throw new Error('scrypt r must be an integer from 1 through 32');
388
+ if (!Number.isInteger(p) || p < 1 || p > 8) throw new Error('scrypt p must be an integer from 1 through 8');
389
+ const requiredMemory = 128 * n * r + 1024 * 1024;
390
+ if (!Number.isInteger(maxmem) || maxmem < requiredMemory || maxmem > 256 * 1024 * 1024) {
391
+ throw new Error(`scrypt maxmem must be an integer from ${requiredMemory} through 268435456`);
392
+ }
393
+ return { ...kdfSpec, algorithm, n, r, p, maxmem };
394
+ }
395
+ if (algorithm === 'pbkdf2') {
396
+ const iterations = kdfSpec?.iterations ?? 600000;
397
+ const hash = kdfSpec?.hash ?? 'sha256';
398
+ if (!Number.isInteger(iterations) || iterations < 100000 || iterations > 2000000) {
399
+ throw new Error('PBKDF2 iterations must be an integer from 100000 through 2000000');
400
+ }
401
+ if (!['sha256', 'sha512'].includes(hash)) throw new Error('PBKDF2 hash must be sha256 or sha512');
402
+ return { ...kdfSpec, algorithm, iterations, hash };
403
+ }
404
+ throw new Error(`Unsupported KDF algorithm: ${algorithm}`);
405
+ }
406
+
407
+ export function deriveMasterKeyFromPassphrase(passphrase, salt, kdfSpec = DEFAULT_KDF_SPEC) {
408
+ const normalized = Buffer.from(String(passphrase), 'utf8');
409
+ if (typeof salt === 'string' && salt.length > 128) throw new Error('Encoded salt is too large');
410
+ const saltBuf = typeof salt === 'string' ? Buffer.from(salt, 'base64') : Buffer.from(salt);
411
+ if (saltBuf.length < 8 || saltBuf.length > 64) throw new Error('Salt must be from 8 through 64 bytes');
412
+ const validated = validateKdfSpec(kdfSpec);
413
+ if (validated.algorithm === 'scrypt') {
414
+ return scryptSync(normalized, saltBuf, DERIVED_KEY_LENGTH, {
415
+ N: validated.n,
416
+ r: validated.r,
417
+ p: validated.p,
418
+ maxmem: validated.maxmem,
419
+ });
420
+ }
421
+ return pbkdf2Sync(normalized, saltBuf, validated.iterations, DERIVED_KEY_LENGTH, validated.hash);
422
+ }
423
+
424
+ export function deriveKeysFromPassphrase(passphrase, salt, kdfSpec = DEFAULT_KDF_SPEC) {
425
+ const master = deriveMasterKeyFromPassphrase(passphrase, salt, kdfSpec);
426
+ return {
427
+ kek: master.subarray(0, 32),
428
+ vaultKey: master.subarray(32, 64),
429
+ addressKey: master.subarray(64, 96),
430
+ signingSeed: master.subarray(96, 128),
431
+ };
432
+ }
433
+
434
+ export function canonicalKeyringAad(meta = {}) {
435
+ const aad = {
436
+ schema: meta.schema ?? 'enigma.vault_keyring.v2',
437
+ type: meta.type ?? KEYRING_ENCRYPTED_TYPE,
438
+ kdf: meta.kdf ?? DEFAULT_KDF_SPEC,
439
+ salt: String(meta.salt ?? ''),
440
+ vault_schema: meta.vault_schema ?? VAULT_SCHEMA,
441
+ vault_id: meta.vault_id ?? null,
442
+ active_key_epoch: meta.active_key_epoch ?? 1,
443
+ key_epochs: Array.isArray(meta.key_epochs) ? meta.key_epochs : [],
444
+ old_epoch_read_policy: meta.old_epoch_read_policy ?? 'retained_keys_allowed',
445
+ signer: meta.signer ?? 'ed25519',
446
+ publicKey: meta.publicKey ?? null,
447
+ };
448
+ return Buffer.from(canonical(aad), 'utf8');
449
+ }
450
+
451
+ function encryptKeyring(plaintextKeyring, kek, aadMeta = undefined) {
452
+ const iv = randomBytes(IV_LENGTH);
453
+ const cipher = createCipheriv('aes-256-gcm', kek, iv);
454
+ if (aadMeta) {
455
+ cipher.setAAD(canonicalKeyringAad(aadMeta));
456
+ }
457
+ const plaintext = Buffer.from(JSON.stringify(plaintextKeyring), 'utf8');
458
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
459
+ const tag = cipher.getAuthTag();
460
+ return {
461
+ iv: iv.toString('base64'),
462
+ tag: tag.toString('base64'),
463
+ ciphertext: ciphertext.toString('base64'),
464
+ };
465
+ }
466
+
467
+ function zeroDerivedKeys(keys) {
468
+ for (const value of Object.values(keys ?? {})) value?.fill?.(0);
469
+ }
470
+
471
+ export function decryptKeyring(keyring, passphrase) {
472
+ if (keyring?.type !== KEYRING_ENCRYPTED_TYPE) throw new Error('Keyring is not encrypted');
473
+ if (passphrase === undefined || passphrase === null || String(passphrase).trim().length === 0) {
474
+ throw new Error('Encrypted keyring requires passphrase');
475
+ }
476
+ const kdf = keyring.kdf || DEFAULT_KDF_SPEC;
477
+ const salt = keyring.kdf?.salt ?? keyring.salt;
478
+ const derived = deriveKeysFromPassphrase(passphrase, salt, kdf);
479
+ try {
480
+ if (keyring.kdf) {
481
+ const decipher = createDecipheriv('aes-256-gcm', derived.kek, Buffer.from(keyring.iv, 'base64'));
482
+ decipher.setAuthTag(Buffer.from(keyring.tag, 'base64'));
483
+ decipher.setAAD(canonicalKeyringAad(keyring));
484
+ const plaintext = Buffer.concat([
485
+ decipher.update(Buffer.from(keyring.ciphertext, 'base64')),
486
+ decipher.final(),
487
+ ]);
488
+ try {
489
+ return JSON.parse(plaintext.toString('utf8'));
490
+ } finally {
491
+ plaintext.fill(0);
492
+ }
493
+ }
494
+
495
+ const legacyDecipher = createDecipheriv('aes-256-gcm', derived.kek, Buffer.from(keyring.iv, 'base64'));
496
+ legacyDecipher.setAuthTag(Buffer.from(keyring.tag, 'base64'));
497
+ const plaintext = Buffer.concat([
498
+ legacyDecipher.update(Buffer.from(keyring.ciphertext, 'base64')),
499
+ legacyDecipher.final(),
500
+ ]);
501
+ try {
502
+ return JSON.parse(plaintext.toString('utf8'));
503
+ } finally {
504
+ plaintext.fill(0);
505
+ }
506
+ } finally {
507
+ zeroDerivedKeys(derived);
508
+ }
509
+ }
510
+
511
+ function signingPair(seed) {
512
+ if (seed !== undefined && seed !== null) {
513
+ try {
514
+ const privateKey = createPrivateKey({ key: buildEd25519Pkcs8(seed), format: 'der', type: 'pkcs8' });
515
+ const publicKey = createPublicKey(privateKey);
516
+ const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
517
+ return {
518
+ key_id: `ed25519:${sha256Hex(publicKeyPem).slice(0, 32)}`,
519
+ publicKey,
520
+ privateKey,
521
+ };
522
+ } catch {
523
+ // Fall through to random signing pair if deterministic construction is unsupported.
524
+ }
525
+ }
317
526
  try {
318
527
  const pair = generateSigningKeyPair();
319
528
  if (pair && typeof pair.then !== 'function') return pair;
@@ -367,7 +576,7 @@ function nowIso(now) {
367
576
  return new Date().toISOString();
368
577
  }
369
578
 
370
- function metadata(input) {
579
+ function metadata(input, commitmentKey) {
371
580
  const out = {};
372
581
  if (!input || typeof input !== 'object') return out;
373
582
  for (const [key, value] of Object.entries(input)) {
@@ -376,12 +585,12 @@ function metadata(input) {
376
585
  if (isSafeMetadataPrimitive(key, value)) {
377
586
  out[key] = value;
378
587
  } else {
379
- commitMetadataValue(out, key, value);
588
+ commitMetadataValue(out, key, value, commitmentKey);
380
589
  }
381
590
  continue;
382
591
  }
383
592
  if (Array.isArray(value) || typeof value === 'object') {
384
- commitMetadataValue(out, key, value);
593
+ commitMetadataValue(out, key, value, commitmentKey);
385
594
  }
386
595
  }
387
596
  return out;
@@ -389,7 +598,7 @@ function metadata(input) {
389
598
 
390
599
  function pruneUndefined(value) {
391
600
  if (Array.isArray(value)) return value.map((item) => pruneUndefined(item));
392
- if (value && typeof value === 'object') {
601
+ if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
393
602
  const out = {};
394
603
  for (const [key, item] of Object.entries(value)) {
395
604
  if (item !== undefined) out[key] = pruneUndefined(item);
@@ -469,7 +678,7 @@ function appendEvent(vault, fields) {
469
678
  sequence,
470
679
  policy_id: fields.policy_id ?? vault.policy_id,
471
680
  decision: fields.decision,
472
- metadata: metadata(fields.metadata),
681
+ metadata: metadata(fields.metadata, vault.addressKey),
473
682
  });
474
683
  const eventCanonical = canonical(event);
475
684
  const eventHash = asSha256(eventCanonical);
@@ -481,45 +690,75 @@ function appendEvent(vault, fields) {
481
690
  vault.receipts.push(receipt);
482
691
  vault.sequence += 1;
483
692
  vault.updated_at = event.timestamp;
484
- vault.active_set_root = activeSetRoot;
485
- vault.receipt_log_root = computeReceiptLogRootFrom(vault.receipts);
693
+ Object.assign(vault, lifecycleRoots(vault));
486
694
  return { event, receipt };
487
695
  }
488
696
 
489
- function memoryAddress(vault, subjectId, memoryId, contentHash) {
490
- const payload = canonical({ domain: MEMORY_DOMAIN, subject_id: subjectId, memory_id: memoryId, content_hash: contentHash });
491
- try {
492
- const addr = createMemoryAddress({
493
- vault_addr_key: vault.addressKey,
494
- vaultAddressKey: vault.addressKey,
495
- key: vault.addressKey,
496
- value: payload,
497
- components: { domain: MEMORY_DOMAIN, subject_id: subjectId, memory_id: memoryId, content_hash: contentHash },
498
- domain: MEMORY_DOMAIN,
499
- subject_id: subjectId,
500
- subjectId,
501
- memory_id: memoryId,
502
- memoryId,
503
- content_hash: contentHash,
504
- contentHash,
505
- });
506
- if (typeof addr === 'string' && addr.length > 0) return addr;
507
- } catch {
508
- // Local HMAC construction below follows the protocol domain separation.
509
- }
697
+ function contentCommitment(vault, plaintext) {
698
+ return `hmac-sha256:${asHmacSha256(vault.addressKey, canonical({
699
+ domain: `${MEMORY_DOMAIN}/content_commitment`,
700
+ vault_id: vault.vault_id,
701
+ content: String(plaintext),
702
+ }))}`;
703
+ }
704
+
705
+ function memoryAddress(vault, subjectId, memoryId, commitment) {
706
+ const payload = canonical({
707
+ domain: MEMORY_DOMAIN,
708
+ vault_id: vault.vault_id,
709
+ subject_id: subjectId,
710
+ memory_id: memoryId,
711
+ content_commitment: commitment,
712
+ });
510
713
  return `mem:${asHmacSha256(vault.addressKey, payload)}`;
511
714
  }
512
715
 
513
- function encryptContent(vault, memoryAddr, plaintext) {
716
+ function canonicalContentAad(vault, memoryAddr, epoch, keyId) {
717
+ return canonical({
718
+ domain: CONTENT_AAD_DOMAIN,
719
+ schema: MEMORY_OBJECT_SCHEMA,
720
+ vault_id: vault.vault_id,
721
+ memory_addr: memoryAddr,
722
+ key_epoch: epoch,
723
+ key_id: keyId,
724
+ });
725
+ }
726
+
727
+ function normalizedPrivateTags(value) {
728
+ if (!Array.isArray(value)) return [];
729
+ return value.map((tag) => String(tag)).filter((tag) => tag.length > 0);
730
+ }
731
+
732
+ function setPrivateTags(record, tags) {
733
+ Object.defineProperty(record, 'purpose_tags', {
734
+ value: normalizedPrivateTags(tags),
735
+ enumerable: false,
736
+ configurable: true,
737
+ writable: true,
738
+ });
739
+ }
740
+
741
+ function privateTags(record) {
742
+ const descriptor = Object.getOwnPropertyDescriptor(record, 'purpose_tags');
743
+ return descriptor && descriptor.enumerable === false ? normalizedPrivateTags(descriptor.value) : [];
744
+ }
745
+
746
+ function encryptContent(vault, memoryAddr, plaintext, epochEntry = activeContentEpoch(vault), privateFields = {}) {
514
747
  const iv = randomBytes(12);
515
- const cipher = createCipheriv('aes-256-gcm', vault.vaultKey, iv);
516
- cipher.setAAD(Buffer.from(memoryAddr));
517
- const ciphertext = Buffer.concat([cipher.update(String(plaintext), 'utf8'), cipher.final()]);
748
+ const aad = canonicalContentAad(vault, memoryAddr, epochEntry.epoch, epochEntry.key_id);
749
+ const cipher = createCipheriv('aes-256-gcm', epochEntry.key, iv);
750
+ cipher.setAAD(Buffer.from(aad, 'utf8'));
751
+ const privatePayload = JSON.stringify({
752
+ schema: PRIVATE_MEMORY_SCHEMA,
753
+ content: String(plaintext),
754
+ purpose_tags: normalizedPrivateTags(privateFields.purpose_tags),
755
+ });
756
+ const ciphertext = Buffer.concat([cipher.update(privatePayload, 'utf8'), cipher.final()]);
518
757
  const tag = cipher.getAuthTag();
519
758
  return {
520
759
  alg: 'AES-256-GCM',
521
760
  iv: iv.toString('base64'),
522
- aad: memoryAddr,
761
+ aad,
523
762
  ciphertext: ciphertext.toString('base64'),
524
763
  tag: tag.toString('base64'),
525
764
  };
@@ -546,16 +785,31 @@ function getPlaintextCache(record) {
546
785
  }
547
786
 
548
787
  function decryptContent(vault, record) {
788
+ if (!record) throw new Error('memory record is unavailable');
789
+ const epochEntry = contentKeyForRecord(vault, record);
549
790
  const cached = getPlaintextCache(record);
550
791
  if (cached !== undefined) return cached;
551
792
  const encrypted = record.content_ciphertext;
552
- const decipher = createDecipheriv('aes-256-gcm', vault.vaultKey, Buffer.from(encrypted.iv, 'base64'));
553
- decipher.setAAD(Buffer.from(encrypted.aad ?? record.memory_addr));
793
+ if (!encrypted || encrypted.alg !== 'AES-256-GCM') throw new Error('memory ciphertext envelope is invalid');
794
+ const expectedAad = canonicalContentAad(vault, record.memory_addr, epochEntry.epoch, epochEntry.key_id);
795
+ if (encrypted.aad !== expectedAad) throw new Error('memory ciphertext AAD mismatch');
796
+ const decipher = createDecipheriv('aes-256-gcm', epochEntry.key, Buffer.from(encrypted.iv, 'base64'));
797
+ decipher.setAAD(Buffer.from(expectedAad, 'utf8'));
554
798
  decipher.setAuthTag(Buffer.from(encrypted.tag, 'base64'));
555
- const plaintext = Buffer.concat([
799
+ const decrypted = Buffer.concat([
556
800
  decipher.update(Buffer.from(encrypted.ciphertext, 'base64')),
557
801
  decipher.final(),
558
802
  ]).toString('utf8');
803
+ let plaintext = decrypted;
804
+ try {
805
+ const payload = JSON.parse(decrypted);
806
+ if (payload?.schema === PRIVATE_MEMORY_SCHEMA && typeof payload.content === 'string') {
807
+ plaintext = payload.content;
808
+ setPrivateTags(record, payload.purpose_tags);
809
+ }
810
+ } catch {
811
+ // Pre-private-envelope ciphertext stores the content string directly.
812
+ }
559
813
  return setPlaintextCache(record, plaintext);
560
814
  }
561
815
 
@@ -563,25 +817,315 @@ function publicRecord(record) {
563
817
  return pruneUndefined(sanitizePublicValue(record));
564
818
  }
565
819
 
820
+ function requireVault(input, functionName) {
821
+ const vault = input?.vault ?? input;
822
+ if (!vault) throw new Error(`${functionName} requires a vault`);
823
+ return vault;
824
+ }
825
+
826
+ function signerRef(signer) {
827
+ if (!signer || typeof signer !== 'object') return undefined;
828
+ return pruneUndefined({
829
+ key_id: signer.key_id ?? signer.keyId,
830
+ alg: signer.alg,
831
+ publicKey: signer.publicKey,
832
+ });
833
+ }
834
+
835
+ function signatureRef(signature) {
836
+ if (!signature || typeof signature !== 'object') return undefined;
837
+ return pruneUndefined({
838
+ alg: signature.alg,
839
+ value: signature.value,
840
+ });
841
+ }
842
+
843
+ function sortedMapValues(mapLike) {
844
+ if (!mapLike) return [];
845
+ if (typeof mapLike.values === 'function') return [...mapLike.values()];
846
+ if (Array.isArray(mapLike)) return mapLike;
847
+ return [];
848
+ }
849
+
850
+ function quarantineAddresses(vault) {
851
+ const out = new Set(vault.quarantineAddresses ?? []);
852
+ for (const [memoryAddr, record] of vault.memories ?? []) {
853
+ if (record?.state === 'quarantine' || record?.state === 'quarantined') out.add(memoryAddr);
854
+ }
855
+ return sortedStrings(out);
856
+ }
857
+
858
+ function publicTombstone(tombstone) {
859
+ if (!tombstone || typeof tombstone !== 'object') return undefined;
860
+ return pruneUndefined({
861
+ schema: tombstone.schema ?? 'enigma.deletion_tombstone.v1',
862
+ memory_addr: tombstone.memory_addr,
863
+ operation: tombstone.operation,
864
+ successor_addr: tombstone.successor_addr,
865
+ tombstoned_at: tombstone.tombstoned_at,
866
+ reason_hash: tombstone.reason ? asSha256(String(tombstone.reason)) : undefined,
867
+ });
868
+ }
869
+
870
+ function tombstoneRootItems(vault) {
871
+ return sortedMapValues(vault.tombstones)
872
+ .map(publicTombstone)
873
+ .filter(Boolean)
874
+ .sort((a, b) => String(a.memory_addr).localeCompare(String(b.memory_addr)))
875
+ .map((tombstone) => asSha256(canonical(tombstone)));
876
+ }
877
+
878
+ function lifecycleRoots(vault) {
879
+ const active = sortedStrings(vault.activeAddresses ?? []);
880
+ const quarantine = quarantineAddresses(vault);
881
+ const tombstones = tombstoneRootItems(vault);
882
+ const activeSetRoot = computeSetRoot(active);
883
+ const quarantineSetRoot = computeSetRoot(quarantine);
884
+ const tombstoneSetRoot = computeSetRoot(tombstones);
885
+ const receiptLogRoot = computeReceiptLogRootFrom(vault.receipts ?? []);
886
+ return {
887
+ active_set_root: activeSetRoot,
888
+ quarantine_set_root: quarantineSetRoot,
889
+ tombstone_set_root: tombstoneSetRoot,
890
+ receipt_log_root: receiptLogRoot,
891
+ lifecycle_root: computeSetRoot([activeSetRoot, quarantineSetRoot, tombstoneSetRoot, receiptLogRoot]),
892
+ };
893
+ }
894
+
895
+ function applyLifecycleRoots(vault) {
896
+ return Object.assign(vault, lifecycleRoots(vault));
897
+ }
898
+
899
+ function projectLifecycleReceipt(receipt) {
900
+ if (!receipt || typeof receipt !== 'object') return undefined;
901
+ return pruneUndefined({
902
+ schema: 'enigma.lifecycle_receipt_projection.v1',
903
+ receipt_id: receipt.receipt_id,
904
+ receipt_hash: hashReceipt(receipt),
905
+ event_hash: receipt.event_hash,
906
+ operation: receipt.operation,
907
+ tenant_id: receipt.tenant_id,
908
+ subject_id: receipt.subject_id,
909
+ memory_addr: receipt.memory_addr,
910
+ source_addr: receipt.source_addr,
911
+ policy_id: receipt.policy_id,
912
+ sequence: receipt.sequence,
913
+ previous_receipt_hash: receipt.previous_receipt_hash,
914
+ active_set_root: receipt.active_set_root,
915
+ receipt_log_root: receipt.receipt_log_root,
916
+ timestamp: receipt.timestamp,
917
+ signer: signerRef(receipt.signer),
918
+ signature: signatureRef(receipt.signature),
919
+ });
920
+ }
921
+
922
+ function lifecycleStateForReceipt(receipt) {
923
+ if (receipt?.operation === 'delete') return 'tombstone';
924
+ if (receipt?.operation === 'quarantine') return 'quarantine';
925
+ return 'active';
926
+ }
927
+
928
+ function projectLifecycleReceiptRef(receipt) {
929
+ if (!receipt || typeof receipt !== 'object') return undefined;
930
+ const receiptHash = hashReceipt(receipt);
931
+ return pruneUndefined({
932
+ sequence: receipt.sequence,
933
+ receipt_ref: receipt.receipt_id ?? receiptHash,
934
+ receipt_hash: receiptHash,
935
+ previous_receipt_hash: receipt.previous_receipt_hash,
936
+ lifecycle_state: lifecycleStateForReceipt(receipt),
937
+ });
938
+ }
939
+
940
+ function lifecycleLogId(vault, roots, issuedAt) {
941
+ return `log_${sha256Hex(canonical({
942
+ schema: 'enigma.lifecycle_receipt_log.id.v1',
943
+ vault_id: vault.vault_id,
944
+ receipt_log_root: roots.receipt_log_root,
945
+ lifecycle_root: roots.lifecycle_root,
946
+ issued_at: issuedAt,
947
+ })).slice(0, 32)}`;
948
+ }
949
+
950
+ function refHash(value) {
951
+ return asSha256(canonical(value));
952
+ }
953
+
954
+
955
+ export function buildLifecycleRootSummary(args = {}) {
956
+ const vault = requireVault(args, 'buildLifecycleRootSummary');
957
+ const roots = lifecycleRoots(vault);
958
+ const active = sortedStrings(vault.activeAddresses ?? []);
959
+ const quarantine = quarantineAddresses(vault);
960
+ const tombstones = tombstoneRootItems(vault);
961
+ return pruneUndefined({
962
+ schema: 'enigma.lifecycle_root_summary.v1',
963
+ vault_id: vault.vault_id,
964
+ tenant_id: vault.tenant_id,
965
+ subject_id: vault.subject_id,
966
+ policy_id: vault.policy_id,
967
+ sequence: vault.sequence,
968
+ active_count: active.length,
969
+ quarantine_count: quarantine.length,
970
+ tombstone_count: tombstones.length,
971
+ receipt_count: Array.isArray(vault.receipts) ? vault.receipts.length : 0,
972
+ active_set_root: roots.active_set_root,
973
+ quarantine_set_root: roots.quarantine_set_root,
974
+ tombstone_set_root: roots.tombstone_set_root,
975
+ active_root: roots.active_set_root,
976
+ quarantine_root: roots.quarantine_set_root,
977
+ tombstone_root: roots.tombstone_set_root,
978
+ receipt_log_root: roots.receipt_log_root,
979
+ lifecycle_root: roots.lifecycle_root,
980
+ updated_at: vault.updated_at,
981
+ signer: signerRef(vault.signer),
982
+ });
983
+ }
984
+
985
+ export function buildMemoryBoundaryTransactionSummary(args = {}) {
986
+ const vault = requireVault(args, 'buildMemoryBoundaryTransactionSummary');
987
+ const receipt = args.receipt ?? args.transaction ?? vault.receipts?.[vault.receipts.length - 1];
988
+ const projection = projectLifecycleReceipt(receipt);
989
+ if (!projection) throw new Error('buildMemoryBoundaryTransactionSummary requires at least one receipt');
990
+ return pruneUndefined({
991
+ schema: 'enigma.memory_boundary_transaction_summary.v1',
992
+ vault_id: vault.vault_id,
993
+ tenant_id: vault.tenant_id,
994
+ subject_id: vault.subject_id,
995
+ operation: projection.operation,
996
+ sequence: projection.sequence,
997
+ timestamp: projection.timestamp,
998
+ memory_addr: projection.memory_addr,
999
+ source_addr: projection.source_addr,
1000
+ policy_id: projection.policy_id,
1001
+ event_hash: projection.event_hash,
1002
+ receipt_hash: projection.receipt_hash,
1003
+ previous_receipt_hash: projection.previous_receipt_hash,
1004
+ active_set_root: projection.active_set_root,
1005
+ receipt_log_root: projection.receipt_log_root,
1006
+ lifecycle_root: buildLifecycleRootSummary({ vault }).lifecycle_root,
1007
+ signer: projection.signer,
1008
+ signature: projection.signature,
1009
+ public_safe: true,
1010
+ });
1011
+ }
1012
+
1013
+ export function buildPublicLifecycleReceiptLog(args = {}) {
1014
+ const vault = requireVault(args, 'buildPublicLifecycleReceiptLog');
1015
+ const receiptRefs = (vault.receipts ?? []).map(projectLifecycleReceiptRef).filter(Boolean);
1016
+ if (receiptRefs.length === 0) throw new Error('buildPublicLifecycleReceiptLog requires at least one receipt');
1017
+ const roots = buildLifecycleRootSummary({ vault });
1018
+ const issuedAt = args.now ? nowIso(args.now) : vault.updated_at;
1019
+ const latestReceipt = vault.receipts?.[vault.receipts.length - 1];
1020
+ const logId = lifecycleLogId(vault, roots, issuedAt);
1021
+ return pruneUndefined({
1022
+ schema: 'enigma.lifecycle_receipt_log.v1',
1023
+ log_id: logId,
1024
+ log_ref: refHash({ schema: 'enigma.lifecycle_receipt_log.ref.v1', log_id: logId, receipt_log_root: roots.receipt_log_root }),
1025
+ subject_ref: refHash({ schema: 'enigma.subject_ref.v1', subject_id: vault.subject_id }),
1026
+ tenant_ref: refHash({ schema: 'enigma.tenant_ref.v1', tenant_id: vault.tenant_id }),
1027
+ epoch: vault.sequence,
1028
+ previous_log_root: latestReceipt?.receipt_log_root ?? ZERO_ROOT,
1029
+ receipt_refs: receiptRefs,
1030
+ active_root: roots.active_set_root,
1031
+ quarantine_root: roots.quarantine_set_root,
1032
+ tombstone_root: roots.tombstone_set_root,
1033
+ receipt_log_root: roots.receipt_log_root,
1034
+ issued_at: issuedAt,
1035
+ signer_ref: refHash(signerRef(vault.signer) ?? { key_id: vault.signer?.key_id ?? null }),
1036
+ signature_ref: latestReceipt?.signature ? refHash(signatureRef(latestReceipt.signature)) : undefined,
1037
+ });
1038
+ }
1039
+
1040
+
566
1041
  function attachInternals(vault) {
567
1042
  Object.defineProperties(vault, {
568
1043
  __recordEvent: { value: (fields) => appendEvent(vault, fields), enumerable: false },
569
1044
  __getRecord: { value: (memoryAddr) => vault.memories.get(memoryAddr), enumerable: false },
570
1045
  __getPlaintext: { value: (memoryAddr) => decryptContent(vault, vault.memories.get(memoryAddr)), enumerable: false },
571
1046
  __computeRoots: {
572
- value: () => ({
573
- active_set_root: computeSetRoot(vault.activeAddresses),
574
- receipt_log_root: computeReceiptLogRootFrom(vault.receipts),
575
- }),
1047
+ value: () => lifecycleRoots(vault),
576
1048
  enumerable: false,
577
1049
  },
578
1050
  });
579
1051
  return vault;
580
1052
  }
581
1053
 
1054
+ function normalizeKeyEpoch(entry, fallbackKey, fallbackKeyId, createdAt) {
1055
+ const epoch = Number(entry?.epoch ?? 1);
1056
+ if (!Number.isInteger(epoch) || epoch < 1) throw new Error('content key epoch must be a positive integer');
1057
+ const key = keyBytes(entry?.key ?? entry?.key_b64 ?? fallbackKey, `content key for epoch ${epoch}`);
1058
+ const keyId = entry?.key_id ?? entry?.keyId ?? fallbackKeyId;
1059
+ if (typeof keyId !== 'string' || keyId.length === 0) throw new Error(`content key epoch ${epoch} requires key_id`);
1060
+ const status = entry?.status ?? 'active';
1061
+ if (!['active', 'retained', 'sealed'].includes(status)) throw new Error(`unsupported content key epoch status: ${status}`);
1062
+ return {
1063
+ epoch,
1064
+ key_id: keyId,
1065
+ key,
1066
+ status,
1067
+ created_at: entry?.created_at ?? createdAt,
1068
+ sealed_at: entry?.sealed_at,
1069
+ };
1070
+ }
1071
+
1072
+ function activeContentEpoch(vault) {
1073
+ const entry = vault.keyEpochs.get(vault.activeKeyEpoch);
1074
+ if (!entry) throw new Error(`active content key epoch ${vault.activeKeyEpoch} is unavailable`);
1075
+ if (entry.status === 'sealed') throw new Error(`content key epoch ${entry.epoch} is sealed and rejects writes`);
1076
+ if (entry.status !== 'active') throw new Error(`content key epoch ${entry.epoch} is not active`);
1077
+ return entry;
1078
+ }
1079
+
1080
+ function contentKeyForRecord(vault, record) {
1081
+ const epoch = Number(record.key_epoch ?? 1);
1082
+ const entry = vault.keyEpochs.get(epoch);
1083
+ if (!entry || !entry.key) throw new Error(`content key epoch ${epoch} has been destroyed or is unavailable`);
1084
+ if (entry.key_id !== record.key_id) throw new Error(`content key id mismatch for epoch ${epoch}`);
1085
+ if (epoch !== vault.activeKeyEpoch && vault.allowOldEpochReads !== true) {
1086
+ throw new Error(`read policy rejects old content key epoch ${epoch}`);
1087
+ }
1088
+ return entry;
1089
+ }
1090
+
582
1091
  export function createVault(args = {}) {
583
1092
  const createdAt = nowIso(args.now);
584
- const signingKeyPair = args.signingKeyPair ?? signingPair();
1093
+ const passphrase = args.passphrase;
1094
+ const salt = args.salt ?? randomBytes(SALT_LENGTH).toString('base64');
1095
+ let vaultKey;
1096
+ let addressKey;
1097
+ let signingSeed;
1098
+ let keyringType = KEYRING_PLAINTEXT_TYPE;
1099
+ if (passphrase !== undefined && passphrase !== null) {
1100
+ const rawPass = String(passphrase);
1101
+ if (rawPass.length === 0 || rawPass.trim().length === 0) {
1102
+ throw new Error('Passphrase provided to createVault cannot be empty.');
1103
+ }
1104
+ const kdf = args.kdf || DEFAULT_KDF_SPEC;
1105
+ const derived = deriveKeysFromPassphrase(rawPass, salt, kdf);
1106
+ vaultKey = derived.vaultKey;
1107
+ addressKey = derived.addressKey;
1108
+ signingSeed = derived.signingSeed;
1109
+ keyringType = KEYRING_ENCRYPTED_TYPE;
1110
+ } else {
1111
+ vaultKey = keyBytes(args.vault_key ?? args.vaultKey, 'vault key');
1112
+ addressKey = keyBytes(args.address_key ?? args.addressKey, 'address key');
1113
+ }
1114
+ const signingKeyPair = args.signingKeyPair ?? signingPair(signingSeed);
1115
+ const activeKeyEpoch = Number(args.active_key_epoch ?? args.activeKeyEpoch ?? 1);
1116
+ const initialKeyId = args.key_id ?? args.keyId ?? `vault-key-${randomUUID()}`;
1117
+ const suppliedEpochs = args.key_epochs ?? args.keyEpochs;
1118
+ const epochEntries = Array.isArray(suppliedEpochs) && suppliedEpochs.length > 0
1119
+ ? suppliedEpochs.map((entry) => normalizeKeyEpoch(entry, undefined, undefined, createdAt))
1120
+ : [normalizeKeyEpoch({ epoch: activeKeyEpoch, key_id: initialKeyId, key: vaultKey, status: 'active' }, undefined, undefined, createdAt)];
1121
+ const keyEpochs = new Map(epochEntries.map((entry) => [entry.epoch, entry]));
1122
+ if (keyEpochs.size !== epochEntries.length) throw new Error('duplicate content key epoch');
1123
+ const activeEpochEntry = keyEpochs.get(activeKeyEpoch);
1124
+ if (!activeEpochEntry) throw new Error(`active content key epoch ${activeKeyEpoch} is missing`);
1125
+ if (!['active', 'sealed'].includes(activeEpochEntry.status)) {
1126
+ throw new Error(`active content key epoch ${activeKeyEpoch} cannot have ${activeEpochEntry.status} status`);
1127
+ }
1128
+ vaultKey = activeEpochEntry.key;
585
1129
  const vault = {
586
1130
  schema: VAULT_SCHEMA,
587
1131
  vault_id: args.vault_id ?? args.vaultId ?? `vault_${randomUUID()}`,
@@ -592,9 +1136,12 @@ export function createVault(args = {}) {
592
1136
  created_at: createdAt,
593
1137
  updated_at: createdAt,
594
1138
  encryption: 'local_aead',
595
- key_id: args.key_id ?? args.keyId ?? `vault-key-${randomUUID()}`,
596
- vaultKey: keyBytes(args.vault_key ?? args.vaultKey, 'vault key'),
597
- addressKey: keyBytes(args.address_key ?? args.addressKey, 'address key'),
1139
+ key_id: activeEpochEntry.key_id,
1140
+ activeKeyEpoch,
1141
+ keyEpochs,
1142
+ allowOldEpochReads: args.allow_old_epoch_reads ?? args.allowOldEpochReads ?? true,
1143
+ vaultKey,
1144
+ addressKey,
598
1145
  signingKeyPair,
599
1146
  signer: args.signer ?? signerFor(signingKeyPair),
600
1147
  memories: new Map(),
@@ -602,13 +1149,134 @@ export function createVault(args = {}) {
602
1149
  receipts: [],
603
1150
  activeAddresses: new Set(),
604
1151
  tombstones: new Map(),
1152
+ quarantineAddresses: new Set(),
605
1153
  sequence: 0,
606
1154
  active_set_root: ZERO_ROOT,
1155
+ quarantine_set_root: ZERO_ROOT,
1156
+ tombstone_set_root: ZERO_ROOT,
607
1157
  receipt_log_root: ZERO_ROOT,
1158
+ lifecycle_root: ZERO_ROOT,
1159
+ keyringType,
608
1160
  };
1161
+ applyLifecycleRoots(vault);
609
1162
  return attachInternals(vault);
610
1163
  }
611
1164
 
1165
+ export function rotateContentKey(args = {}) {
1166
+ const vault = requireVault(args, 'rotateContentKey');
1167
+ const previous = vault.keyEpochs.get(vault.activeKeyEpoch);
1168
+ if (previous.status === 'active') previous.status = 'retained';
1169
+ const epoch = Math.max(...vault.keyEpochs.keys()) + 1;
1170
+ const createdAt = nowIso(args.now);
1171
+ const next = normalizeKeyEpoch({
1172
+ epoch,
1173
+ key_id: args.key_id ?? args.keyId ?? `vault-key-${randomUUID()}`,
1174
+ key: args.key,
1175
+ status: 'active',
1176
+ created_at: createdAt,
1177
+ }, undefined, undefined, createdAt);
1178
+ vault.keyEpochs.set(epoch, next);
1179
+ vault.activeKeyEpoch = epoch;
1180
+ vault.key_id = next.key_id;
1181
+ vault.vaultKey = next.key;
1182
+ const { event, receipt } = appendEvent(vault, {
1183
+ operation: 'key_rotate',
1184
+ actor_id: args.actor_id ?? args.actorId,
1185
+ now: args.now,
1186
+ metadata: { prior_epoch: previous.epoch, new_epoch: epoch, prior_key_id: previous.key_id, new_key_id: next.key_id },
1187
+ });
1188
+ return {
1189
+ epoch,
1190
+ key_id: next.key_id,
1191
+ prior_epoch: previous.epoch,
1192
+ prior_key_status: previous.status,
1193
+ old_key_retained: true,
1194
+ external_copies_destroyed: false,
1195
+ event,
1196
+ receipt,
1197
+ };
1198
+ }
1199
+
1200
+ export function sealContentKeyEpoch(args = {}) {
1201
+ const vault = requireVault(args, 'sealContentKeyEpoch');
1202
+ const epoch = Number(args.epoch ?? vault.activeKeyEpoch);
1203
+ const entry = vault.keyEpochs.get(epoch);
1204
+ if (!entry) throw new Error(`unknown content key epoch ${epoch}`);
1205
+ if (entry.status === 'sealed') throw new Error(`content key epoch ${epoch} is already sealed`);
1206
+ entry.status = 'sealed';
1207
+ entry.sealed_at = nowIso(args.now);
1208
+ const { event, receipt } = appendEvent(vault, {
1209
+ operation: 'key_seal',
1210
+ actor_id: args.actor_id ?? args.actorId,
1211
+ now: args.now,
1212
+ metadata: { epoch, key_id: entry.key_id, key_retained: true },
1213
+ });
1214
+ return {
1215
+ epoch,
1216
+ key_id: entry.key_id,
1217
+ status: entry.status,
1218
+ key_retained_for_reads: true,
1219
+ cryptographic_erasure_performed: false,
1220
+ external_copies_destroyed: false,
1221
+ event,
1222
+ receipt,
1223
+ };
1224
+ }
1225
+
1226
+ export function resetVaultContentKeys(args = {}) {
1227
+ const vault = requireVault(args, 'resetVaultContentKeys');
1228
+ const destroyedEpochs = [...vault.keyEpochs.keys()].sort((a, b) => a - b);
1229
+ const oldEpochEntries = [...vault.keyEpochs.values()];
1230
+ const removedRecords = vault.memories.size;
1231
+ const nextEpoch = Math.max(0, ...destroyedEpochs) + 1;
1232
+ const createdAt = nowIso(args.now);
1233
+ const next = normalizeKeyEpoch({
1234
+ epoch: nextEpoch,
1235
+ key_id: args.key_id ?? args.keyId ?? `vault-key-${randomUUID()}`,
1236
+ key: args.key,
1237
+ status: 'active',
1238
+ created_at: createdAt,
1239
+ }, undefined, undefined, createdAt);
1240
+ const oldAddressKey = vault.addressKey;
1241
+ vault.memories.clear();
1242
+ vault.activeAddresses.clear();
1243
+ vault.tombstones.clear();
1244
+ vault.quarantineAddresses.clear();
1245
+ vault.keyEpochs = new Map([[nextEpoch, next]]);
1246
+ vault.activeKeyEpoch = nextEpoch;
1247
+ vault.key_id = next.key_id;
1248
+ vault.vaultKey = next.key;
1249
+ vault.addressKey = keyBytes(args.address_key ?? args.addressKey, 'address key');
1250
+ applyLifecycleRoots(vault);
1251
+ const { event, receipt } = appendEvent(vault, {
1252
+ operation: 'key_reset',
1253
+ actor_id: args.actor_id ?? args.actorId,
1254
+ now: args.now,
1255
+ metadata: { destroyed_epoch_count: destroyedEpochs.length, removed_record_count: removedRecords, new_epoch: nextEpoch, new_key_id: next.key_id },
1256
+ });
1257
+ for (const entry of oldEpochEntries) entry.key?.fill?.(0);
1258
+ oldAddressKey?.fill?.(0);
1259
+ return {
1260
+ destroyed_epochs: destroyedEpochs,
1261
+ removed_record_count: removedRecords,
1262
+ epoch: nextEpoch,
1263
+ key_id: next.key_id,
1264
+ software_key_material_zeroized_in_process: true,
1265
+ process_memory_overwrite_guaranteed: false,
1266
+ backups_and_external_copies_destroyed: false,
1267
+ physical_media_erasure_guaranteed: false,
1268
+ event,
1269
+ receipt,
1270
+ };
1271
+ }
1272
+
1273
+ function normalizeImportance(value) {
1274
+ if (value === undefined || value === null) return undefined;
1275
+ const num = Number(value);
1276
+ if (!Number.isFinite(num)) return undefined;
1277
+ return Math.max(0, Math.min(1, num));
1278
+ }
1279
+
612
1280
  export function remember(args = {}) {
613
1281
  const vault = args.vault;
614
1282
  if (!vault) throw new Error('remember requires a vault');
@@ -618,29 +1286,38 @@ export function remember(args = {}) {
618
1286
  const memoryId = args.memory_id ?? args.memoryId ?? `mem_${randomUUID()}`;
619
1287
  const subjectId = args.subject_id ?? args.subjectId ?? vault.subject_id;
620
1288
  const createdAt = nowIso(args.now);
621
- const contentHash = asSha256(String(plaintext));
622
- const memoryAddr = memoryAddress(vault, subjectId, memoryId, contentHash);
1289
+ const epochEntry = activeContentEpoch(vault);
1290
+ const requestedEpoch = args.key_epoch ?? args.keyEpoch;
1291
+ if (requestedEpoch !== undefined && Number(requestedEpoch) !== epochEntry.epoch) {
1292
+ throw new Error(`writes require active content key epoch ${epochEntry.epoch}`);
1293
+ }
1294
+ const commitment = contentCommitment(vault, plaintext);
1295
+ const memoryAddr = memoryAddress(vault, subjectId, memoryId, commitment);
623
1296
  if (vault.tombstones.has(memoryAddr)) throw new Error(`memory address is tombstoned: ${memoryAddr}`);
1297
+ const rawPurposeTags = normalizedPrivateTags(args.purpose_tags ?? args.purposeTags);
624
1298
 
625
1299
  const record = pruneUndefined({
626
- schema: 'enigma.memory_object.v1',
1300
+ schema: MEMORY_OBJECT_SCHEMA,
627
1301
  memory_id: memoryId,
628
1302
  memory_addr: memoryAddr,
629
1303
  subject_id: subjectId,
630
- kind: args.kind ?? 'fact',
631
- content_ciphertext: encryptContent(vault, memoryAddr, plaintext),
632
- content_hash: contentHash,
633
- content_commitment: `hmac-sha256:${asHmacSha256(vault.addressKey, contentHash)}`,
634
- source_refs: sanitizeSourceRefs(args.source_refs ?? args.sourceRefs),
635
- confidence: args.confidence ?? 'user_confirmed',
636
- sensitivity: args.sensitivity ?? 'normal',
637
- purpose_tags: Array.isArray(args.purpose_tags ?? args.purposeTags) ? (args.purpose_tags ?? args.purposeTags) : [],
638
- retention: args.retention ?? 'durable',
1304
+ kind: ['fact', 'preference', 'goal', 'instruction', 'episode'].includes(args.kind) ? args.kind : 'fact',
1305
+ key_epoch: epochEntry.epoch,
1306
+ key_id: epochEntry.key_id,
1307
+ content_ciphertext: encryptContent(vault, memoryAddr, plaintext, epochEntry, { purpose_tags: rawPurposeTags }),
1308
+ content_commitment: commitment,
1309
+ source_refs: sanitizeSourceRefs(args.source_refs ?? args.sourceRefs, vault.addressKey),
1310
+ confidence: ['user_confirmed', 'inferred', 'verified'].includes(args.confidence) ? args.confidence : 'user_confirmed',
1311
+ sensitivity: ['normal', 'sensitive', 'restricted'].includes(args.sensitivity) ? args.sensitivity : 'normal',
1312
+ purpose_tag_commitments: rawPurposeTags.map((tag) => privateValueCommitment(vault.addressKey, tag)),
1313
+ retention: ['durable', 'session', 'ephemeral'].includes(args.retention) ? args.retention : 'durable',
1314
+ importance: normalizeImportance(args.importance),
639
1315
  state: 'active',
640
- metadata: metadata(args.metadata),
1316
+ metadata: metadata(args.metadata, vault.addressKey),
641
1317
  created_at: createdAt,
642
1318
  updated_at: createdAt,
643
1319
  });
1320
+ setPrivateTags(record, rawPurposeTags);
644
1321
 
645
1322
  vault.memories.set(memoryAddr, record);
646
1323
  vault.activeAddresses.add(memoryAddr);
@@ -651,12 +1328,13 @@ export function remember(args = {}) {
651
1328
  actor_id: args.actor_id ?? args.actorId,
652
1329
  policy_id: args.policy_id ?? args.policyId,
653
1330
  now: args.now,
654
- metadata: {
1331
+ metadata: pruneUndefined({
655
1332
  memory_id: memoryId,
656
1333
  kind: record.kind,
657
1334
  sensitivity: record.sensitivity,
1335
+ importance: record.importance,
658
1336
  content_commitment: record.content_commitment,
659
- },
1337
+ }),
660
1338
  });
661
1339
 
662
1340
  return { memory: publicRecord(record), memory_addr: memoryAddr, event, receipt };
@@ -699,22 +1377,34 @@ export function updateMemory(args = {}) {
699
1377
 
700
1378
  const oldRecord = vault.memories.get(oldAddr);
701
1379
  if (!oldRecord) throw new Error(`unknown memory: ${oldAddr}`);
1380
+ const epochEntry = activeContentEpoch(vault);
1381
+ const requestedEpoch = args.key_epoch ?? args.keyEpoch;
1382
+ if (requestedEpoch !== undefined && Number(requestedEpoch) !== epochEntry.epoch) {
1383
+ throw new Error(`writes require active content key epoch ${epochEntry.epoch}`);
1384
+ }
1385
+ decryptContent(vault, oldRecord);
1386
+ const rawPurposeTags = args.purpose_tags !== undefined || args.purposeTags !== undefined
1387
+ ? normalizedPrivateTags(args.purpose_tags ?? args.purposeTags)
1388
+ : privateTags(oldRecord);
702
1389
  oldRecord.state = 'superseded';
703
1390
  oldRecord.updated_at = nowIso(args.now);
704
1391
  vault.activeAddresses.delete(oldAddr);
705
1392
 
706
- const contentHash = asSha256(String(plaintext));
707
- const newAddr = memoryAddress(vault, oldRecord.subject_id, oldRecord.memory_id, contentHash);
1393
+ const commitment = contentCommitment(vault, plaintext);
1394
+ const newAddr = memoryAddress(vault, oldRecord.subject_id, oldRecord.memory_id, commitment);
708
1395
  const updatedAt = nowIso(args.now);
709
1396
  const newRecord = pruneUndefined({
710
1397
  ...publicRecord(oldRecord),
711
1398
  memory_addr: newAddr,
712
- content_ciphertext: encryptContent(vault, newAddr, plaintext),
713
- content_hash: contentHash,
714
- content_commitment: `hmac-sha256:${asHmacSha256(vault.addressKey, contentHash)}`,
1399
+ key_epoch: epochEntry.epoch,
1400
+ key_id: epochEntry.key_id,
1401
+ content_ciphertext: encryptContent(vault, newAddr, plaintext, epochEntry, { purpose_tags: rawPurposeTags }),
1402
+ content_commitment: commitment,
1403
+ purpose_tag_commitments: rawPurposeTags.map((tag) => privateValueCommitment(vault.addressKey, tag)),
715
1404
  state: 'active',
716
1405
  updated_at: updatedAt,
717
1406
  });
1407
+ setPrivateTags(newRecord, rawPurposeTags);
718
1408
 
719
1409
  vault.memories.set(newAddr, newRecord);
720
1410
  vault.activeAddresses.add(newAddr);
@@ -788,6 +1478,86 @@ export function deleteMemory(args = {}) {
788
1478
  return { memory_addr: memoryAddr, tombstone, event, receipt };
789
1479
  }
790
1480
 
1481
+ function publicKeyEpochs(vault) {
1482
+ return [...vault.keyEpochs.values()]
1483
+ .sort((left, right) => left.epoch - right.epoch)
1484
+ .map((entry) => pruneUndefined({
1485
+ epoch: entry.epoch,
1486
+ key_id: entry.key_id,
1487
+ status: entry.status,
1488
+ created_at: entry.created_at,
1489
+ sealed_at: entry.sealed_at,
1490
+ }));
1491
+ }
1492
+
1493
+ function privateKeyEpochs(vault) {
1494
+ return [...vault.keyEpochs.values()]
1495
+ .sort((left, right) => left.epoch - right.epoch)
1496
+ .map((entry) => ({
1497
+ epoch: entry.epoch,
1498
+ key_id: entry.key_id,
1499
+ status: entry.status,
1500
+ created_at: entry.created_at,
1501
+ sealed_at: entry.sealed_at,
1502
+ key_b64: entry.key.toString('base64'),
1503
+ }));
1504
+ }
1505
+
1506
+ function buildKeyring(vault, passphrase, options = {}) {
1507
+ const signer = vault.signer;
1508
+ const publicKey = exportPublicKey(vault.signingKeyPair);
1509
+ const keyEpochs = publicKeyEpochs(vault);
1510
+ const common = {
1511
+ schema: 'enigma.vault_keyring.v2',
1512
+ vault_schema: vault.schema,
1513
+ vault_id: vault.vault_id,
1514
+ active_key_epoch: vault.activeKeyEpoch,
1515
+ key_epochs: keyEpochs,
1516
+ old_epoch_read_policy: vault.allowOldEpochReads === true ? 'retained_keys_allowed' : 'active_epoch_only',
1517
+ signer,
1518
+ publicKey,
1519
+ };
1520
+ if (passphrase !== undefined && passphrase !== null) {
1521
+ const rawPass = String(passphrase);
1522
+ if (rawPass.length === 0 || rawPass.trim().length === 0) {
1523
+ throw new Error('Passphrase provided to buildKeyring cannot be empty.');
1524
+ }
1525
+ const salt = randomBytes(SALT_LENGTH).toString('base64');
1526
+ const kdf = { ...(options.kdf || DEFAULT_KDF_SPEC), salt };
1527
+ const derived = deriveKeysFromPassphrase(rawPass, salt, kdf);
1528
+ const aadMeta = { ...common, type: KEYRING_ENCRYPTED_TYPE, kdf, salt };
1529
+ const plaintextKeyring = {
1530
+ vault_key_b64: vault.vaultKey.toString('base64'),
1531
+ address_key_b64: vault.addressKey.toString('base64'),
1532
+ privateKey: exportPrivateKey(vault.signingKeyPair),
1533
+ content_keys: privateKeyEpochs(vault),
1534
+ };
1535
+ let encrypted;
1536
+ try {
1537
+ encrypted = encryptKeyring(plaintextKeyring, derived.kek, aadMeta);
1538
+ } finally {
1539
+ zeroDerivedKeys(derived);
1540
+ }
1541
+ return {
1542
+ ...aadMeta,
1543
+ type: KEYRING_ENCRYPTED_TYPE,
1544
+ warning: ENCRYPTED_KEYRING_WARNING,
1545
+ iv: encrypted.iv,
1546
+ tag: encrypted.tag,
1547
+ ciphertext: encrypted.ciphertext,
1548
+ };
1549
+ }
1550
+ return {
1551
+ ...common,
1552
+ type: KEYRING_PLAINTEXT_TYPE,
1553
+ unencrypted: true,
1554
+ warning: PLAINTEXT_KEYRING_WARNING,
1555
+ vault_key_b64: vault.vaultKey.toString('base64'),
1556
+ address_key_b64: vault.addressKey.toString('base64'),
1557
+ content_keys: privateKeyEpochs(vault),
1558
+ privateKey: exportPrivateKey(vault.signingKeyPair),
1559
+ };
1560
+ }
791
1561
  export function exportBundle(args = {}) {
792
1562
  const vault = args.vault;
793
1563
  if (!vault) throw new Error('exportBundle requires a vault');
@@ -805,7 +1575,7 @@ export function exportBundle(args = {}) {
805
1575
 
806
1576
  const roots = vault.__computeRoots();
807
1577
  const bundle = {
808
- schema: 'enigma.vault_bundle.v1',
1578
+ schema: VAULT_BUNDLE_SCHEMA,
809
1579
  exported_at: nowIso(args.now),
810
1580
  vault: {
811
1581
  schema: vault.schema,
@@ -818,19 +1588,15 @@ export function exportBundle(args = {}) {
818
1588
  updated_at: vault.updated_at,
819
1589
  encryption: vault.encryption,
820
1590
  key_id: vault.key_id,
1591
+ active_key_epoch: vault.activeKeyEpoch,
1592
+ key_epochs: publicKeyEpochs(vault),
1593
+ old_epoch_read_policy: vault.allowOldEpochReads === true ? 'retained_keys_allowed' : 'active_epoch_only',
1594
+ key_erasure_scope: 'software_process_and_persisted_bundle_only',
821
1595
  active_set_root: roots.active_set_root,
822
1596
  receipt_log_root: roots.receipt_log_root,
823
1597
  sequence: vault.sequence,
824
1598
  },
825
- keyring: {
826
- type: 'local_secret',
827
- warning: 'Contains local vault keys for offline import; memory payloads remain AES-256-GCM encrypted.',
828
- vault_key_b64: vault.vaultKey.toString('base64'),
829
- address_key_b64: vault.addressKey.toString('base64'),
830
- signer: vault.signer,
831
- publicKey: exportPublicKey(vault.signingKeyPair),
832
- privateKey: exportPrivateKey(vault.signingKeyPair),
833
- },
1599
+ keyring: buildKeyring(vault, args.passphrase, { kdf: args.kdf }),
834
1600
  memory_objects: [...vault.memories.values()].map(publicRecord),
835
1601
  active_memory_addresses: [...vault.activeAddresses].sort(),
836
1602
  tombstones: [...vault.tombstones.values()].sort((a, b) => a.memory_addr.localeCompare(b.memory_addr)),
@@ -846,59 +1612,318 @@ export function exportBundle(args = {}) {
846
1612
  return bundle;
847
1613
  }
848
1614
 
1615
+ function isPlainObject(value) {
1616
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
1617
+ }
1618
+
1619
+ function assertExactKeys(value, allowed, label) {
1620
+ if (!isPlainObject(value)) throw new Error(`${label} must be an object`);
1621
+ for (const key of Object.keys(value)) {
1622
+ if (!allowed.has(key)) throw new Error(`${label} contains unsupported field: ${key}`);
1623
+ }
1624
+ }
1625
+
1626
+ function assertNonEmptyString(value, label) {
1627
+ if (typeof value !== 'string' || value.length === 0) throw new Error(`${label} must be a non-empty string`);
1628
+ }
1629
+
1630
+ function derivedPublicKeyPem(privateKey) {
1631
+ return createPublicKey(createPrivateKey(privateKey)).export({ type: 'spki', format: 'pem' }).trim();
1632
+ }
1633
+
1634
+ function validateRecordCiphertext(bundle, record, epochEntries) {
1635
+ if (record?.schema !== MEMORY_OBJECT_SCHEMA) throw new Error(`unsupported memory object schema: ${record?.schema ?? '<missing>'}`);
1636
+ assertNonEmptyString(record.memory_addr, 'memory_object.memory_addr');
1637
+ assertNonEmptyString(record.key_id, 'memory_object.key_id');
1638
+ if (Object.prototype.hasOwnProperty.call(record, 'content_hash')) throw new Error('memory object exposes a forbidden unkeyed content hash');
1639
+ for (const key of Object.keys(record)) {
1640
+ if (isPlaintextLikeKey(key)) throw new Error(`memory object exposes forbidden plaintext field: ${key}`);
1641
+ }
1642
+ if (!/^hmac-sha256:[a-f0-9]{64}$/.test(record.content_commitment ?? '')) {
1643
+ throw new Error('memory object content commitment must be keyed');
1644
+ }
1645
+ if (!Number.isInteger(record.key_epoch) || record.key_epoch < 1) throw new Error('memory_object.key_epoch must be a positive integer');
1646
+ const epoch = epochEntries.get(record.key_epoch);
1647
+ if (!epoch || epoch.key_id !== record.key_id) throw new Error(`memory object references unknown key epoch/id: ${record.key_epoch}/${record.key_id}`);
1648
+ const encrypted = record.content_ciphertext;
1649
+ if (!isPlainObject(encrypted) || encrypted.alg !== 'AES-256-GCM') throw new Error('memory object ciphertext envelope is invalid');
1650
+ const expectedAad = canonicalContentAad({ vault_id: bundle.vault.vault_id }, record.memory_addr, record.key_epoch, record.key_id);
1651
+ if (encrypted.aad !== expectedAad) throw new Error(`memory object AAD mismatch: ${record.memory_addr}`);
1652
+ let plaintext;
1653
+ try {
1654
+ const decipher = createDecipheriv('aes-256-gcm', epoch.key, Buffer.from(encrypted.iv, 'base64'));
1655
+ decipher.setAAD(Buffer.from(expectedAad, 'utf8'));
1656
+ decipher.setAuthTag(Buffer.from(encrypted.tag, 'base64'));
1657
+ plaintext = Buffer.concat([
1658
+ decipher.update(Buffer.from(encrypted.ciphertext, 'base64')),
1659
+ decipher.final(),
1660
+ ]);
1661
+ } catch {
1662
+ throw new Error(`memory object authentication failed: ${record.memory_addr}`);
1663
+ } finally {
1664
+ plaintext?.fill(0);
1665
+ }
1666
+ }
1667
+
1668
+ function validateStrictBundle(bundle, keyring, decrypted, epochEntries) {
1669
+ assertExactKeys(bundle, new Set([
1670
+ 'schema', 'exported_at', 'vault', 'keyring', 'memory_objects',
1671
+ 'active_memory_addresses', 'tombstones', 'events', 'receipts', 'export_receipt',
1672
+ ]), 'vault bundle');
1673
+ assertExactKeys(bundle.vault, new Set([
1674
+ 'schema', 'vault_id', 'tenant_id', 'subject_id', 'actor_id', 'policy_id',
1675
+ 'created_at', 'updated_at', 'encryption', 'key_id', 'active_key_epoch',
1676
+ 'key_epochs', 'old_epoch_read_policy', 'key_erasure_scope', 'active_set_root',
1677
+ 'receipt_log_root', 'sequence',
1678
+ ]), 'vault bundle manifest');
1679
+ const encryptedKeyringFields = new Set([
1680
+ 'schema', 'vault_schema', 'vault_id', 'active_key_epoch', 'key_epochs',
1681
+ 'old_epoch_read_policy', 'signer', 'publicKey', 'type', 'warning', 'kdf', 'salt', 'iv', 'tag', 'ciphertext',
1682
+ ]);
1683
+ const plaintextKeyringFields = new Set([
1684
+ 'schema', 'vault_schema', 'vault_id', 'active_key_epoch', 'key_epochs',
1685
+ 'old_epoch_read_policy', 'signer', 'publicKey', 'type', 'warning', 'unencrypted', 'vault_key_b64',
1686
+ 'address_key_b64', 'content_keys', 'privateKey',
1687
+ ]);
1688
+ assertExactKeys(
1689
+ keyring,
1690
+ keyring.type === KEYRING_ENCRYPTED_TYPE ? encryptedKeyringFields : plaintextKeyringFields,
1691
+ 'vault keyring',
1692
+ );
1693
+ assertExactKeys(
1694
+ decrypted,
1695
+ keyring.type === KEYRING_ENCRYPTED_TYPE
1696
+ ? new Set(['vault_key_b64', 'address_key_b64', 'privateKey', 'content_keys'])
1697
+ : new Set(['vault_key_b64', 'address_key_b64', 'privateKey', 'content_keys']),
1698
+ 'decrypted vault keyring',
1699
+ );
1700
+ if (bundle.vault.schema !== VAULT_SCHEMA) throw new Error('vault manifest schema mismatch');
1701
+ if (bundle.vault.encryption !== 'local_aead') throw new Error('vault manifest encryption mismatch');
1702
+ assertNonEmptyString(bundle.vault.vault_id, 'vault.vault_id');
1703
+ if (!Number.isInteger(bundle.vault.active_key_epoch) || bundle.vault.active_key_epoch < 1) {
1704
+ throw new Error('vault.active_key_epoch must be a positive integer');
1705
+ }
1706
+ if (!Number.isInteger(bundle.vault.sequence) || bundle.vault.sequence < 0) throw new Error('vault.sequence must be a non-negative integer');
1707
+ if (keyring.schema !== 'enigma.vault_keyring.v2') throw new Error('keyring schema mismatch');
1708
+ if (keyring.vault_schema !== bundle.vault.schema || keyring.vault_id !== bundle.vault.vault_id) {
1709
+ throw new Error('keyring vault binding mismatch');
1710
+ }
1711
+ if (keyring.active_key_epoch !== bundle.vault.active_key_epoch) throw new Error('keyring active key epoch mismatch');
1712
+ if (keyring.old_epoch_read_policy !== bundle.vault.old_epoch_read_policy) {
1713
+ throw new Error('keyring old-epoch read policy mismatch');
1714
+ }
1715
+ const decryptedEpochDescriptors = [...epochEntries.values()]
1716
+ .sort((left, right) => left.epoch - right.epoch)
1717
+ .map((entry) => pruneUndefined({
1718
+ epoch: entry.epoch,
1719
+ key_id: entry.key_id,
1720
+ status: entry.status,
1721
+ created_at: entry.created_at,
1722
+ sealed_at: entry.sealed_at,
1723
+ }));
1724
+ if (canonical(decryptedEpochDescriptors) !== canonical(keyring.key_epochs)) {
1725
+ throw new Error('decrypted content keys do not match signed key epoch descriptors');
1726
+ }
1727
+ if (bundle.vault.key_id !== epochEntries.get(bundle.vault.active_key_epoch)?.key_id) throw new Error('vault active key id mismatch');
1728
+ if (canonical(keyring.key_epochs) !== canonical(bundle.vault.key_epochs)) throw new Error('key epoch manifest mismatch');
1729
+ if (derivedPublicKeyPem(decrypted.privateKey) !== String(keyring.publicKey).trim()) throw new Error('keyring public/private signing key mismatch');
1730
+ if (keyring.signer?.key_id === undefined) throw new Error('keyring signer key id is missing');
1731
+ if (!Array.isArray(keyring.key_epochs) || keyring.key_epochs.length !== epochEntries.size) {
1732
+ throw new Error('keyring key epoch descriptor count mismatch');
1733
+ }
1734
+
1735
+ if (!Array.isArray(bundle.receipts) || !Array.isArray(bundle.events)) throw new Error('bundle receipts and events must be arrays');
1736
+ if (bundle.receipts.length !== bundle.events.length || bundle.vault.sequence !== bundle.receipts.length) {
1737
+ throw new Error('bundle event/receipt sequence length mismatch');
1738
+ }
1739
+ for (let index = 0; index < bundle.events.length; index += 1) {
1740
+ const entry = bundle.events[index];
1741
+ assertExactKeys(entry, new Set(['event', 'canonical', 'hash']), `event entry ${index}`);
1742
+ if (entry.event?.schema !== EVENT_SCHEMA || entry.event?.sequence !== index) throw new Error(`event ${index} schema or sequence mismatch`);
1743
+ const expectedCanonical = canonical(entry.event);
1744
+ const expectedHash = asSha256(expectedCanonical);
1745
+ if (entry.canonical !== expectedCanonical || entry.hash !== expectedHash) throw new Error(`event ${index} canonical/hash mismatch`);
1746
+ const receipt = bundle.receipts[index];
1747
+ if (receipt?.tenant_id !== bundle.vault.tenant_id || receipt?.subject_id !== bundle.vault.subject_id) {
1748
+ throw new Error(`receipt ${index} vault identity binding mismatch`);
1749
+ }
1750
+ if (receipt?.event_hash !== expectedHash || receipt?.operation !== entry.event.operation) throw new Error(`event ${index} receipt binding mismatch`);
1751
+ }
1752
+ const chain = verifyReceiptChain({
1753
+ receipts: bundle.receipts,
1754
+ publicKey: keyring.publicKey,
1755
+ expectedSignerKeyId: keyring.signer.key_id,
1756
+ expectedReceiptLogRoot: bundle.vault.receipt_log_root,
1757
+ expectedActiveSetRoot: bundle.vault.active_set_root,
1758
+ verifyEmbeddedReceiptLogRoot: false,
1759
+ });
1760
+ if (!chain.ok) throw new Error(`receipt chain verification failed: ${chain.errors.join('; ')}`);
1761
+ if (bundle.receipts.length === 0 || canonical(bundle.export_receipt) !== canonical(bundle.receipts.at(-1))) {
1762
+ throw new Error('export receipt does not match the signed receipt chain');
1763
+ }
1764
+ if (computeSetRoot(bundle.active_memory_addresses ?? []) !== bundle.vault.active_set_root) {
1765
+ throw new Error('active memory address root mismatch');
1766
+ }
1767
+
1768
+ if (!Array.isArray(bundle.memory_objects) || !Array.isArray(bundle.active_memory_addresses) || !Array.isArray(bundle.tombstones)) {
1769
+ throw new Error('bundle lifecycle collections must be arrays');
1770
+ }
1771
+ const recordByAddress = new Map();
1772
+ const activeAddressSet = new Set(bundle.active_memory_addresses);
1773
+ if (activeAddressSet.size !== bundle.active_memory_addresses.length) throw new Error('duplicate active memory address');
1774
+ for (const record of bundle.memory_objects) {
1775
+ if (recordByAddress.has(record.memory_addr)) throw new Error(`duplicate memory address: ${record.memory_addr}`);
1776
+ recordByAddress.set(record.memory_addr, record);
1777
+ validateRecordCiphertext(bundle, record, epochEntries);
1778
+ }
1779
+ for (const address of bundle.active_memory_addresses) {
1780
+ if (!recordByAddress.has(address)) throw new Error(`active memory address has no record: ${address}`);
1781
+ const record = recordByAddress.get(address);
1782
+ if (record?.state !== 'active') throw new Error(`active memory address references non-active record: ${address}`);
1783
+ }
1784
+ for (const record of bundle.memory_objects) {
1785
+ if (record.state === 'active' && !activeAddressSet.has(record.memory_addr)) {
1786
+ throw new Error(`active memory record is absent from active address set: ${record.memory_addr}`);
1787
+ }
1788
+ }
1789
+ }
1790
+
849
1791
  export function importBundle(args = {}) {
850
- const source = args.schema === 'enigma.vault_bundle.v1' ? args : (args.bundle?.bundle ?? args.bundle ?? args);
1792
+ const source = args.schema === VAULT_BUNDLE_SCHEMA ? args : (args.bundle?.bundle ?? args.bundle ?? args);
851
1793
  const bundle = typeof source === 'string' ? JSON.parse(source) : source;
852
- if (!bundle || bundle.schema !== 'enigma.vault_bundle.v1') throw new Error('importBundle requires an enigma.vault_bundle.v1 bundle');
853
- const restoredSigningKeyPair = bundle.keyring?.privateKey
854
- ? {
855
- key_id: bundle.keyring?.signer?.key_id,
856
- signer: bundle.keyring?.signer,
857
- publicKey: bundle.keyring?.publicKey,
858
- privateKey: bundle.keyring?.privateKey,
859
- }
860
- : undefined;
1794
+ if (!bundle || bundle.schema !== VAULT_BUNDLE_SCHEMA) throw new Error(`importBundle requires an ${VAULT_BUNDLE_SCHEMA} bundle`);
1795
+ const strict = args.strict === true;
1796
+ const insecureDevMode = args.insecureDevMode === true || args.insecure_dev_mode === true;
1797
+ const warnings = [];
1798
+ const keyring = bundle.keyring;
1799
+ let decrypted;
1800
+ if (keyring?.type === KEYRING_ENCRYPTED_TYPE) {
1801
+ if (args.passphrase === undefined || args.passphrase === null || String(args.passphrase).trim().length === 0) {
1802
+ throw new Error('Encrypted keyring requires passphrase');
1803
+ }
1804
+ decrypted = decryptKeyring(keyring, args.passphrase);
1805
+ } else if (keyring?.type === KEYRING_PLAINTEXT_TYPE) {
1806
+ if (strict && !insecureDevMode) throw new Error('Strict vault import rejects plaintext keyrings outside explicit insecure-dev mode');
1807
+ warnings.push(DEPRECATION_PLAINTEXT_KEYRING);
1808
+ decrypted = {
1809
+ vault_key_b64: keyring.vault_key_b64,
1810
+ address_key_b64: keyring.address_key_b64,
1811
+ privateKey: keyring.privateKey,
1812
+ content_keys: keyring.content_keys,
1813
+ };
1814
+ } else {
1815
+ throw new Error(`Unsupported vault keyring type: ${keyring?.type ?? '<missing>'}`);
1816
+ }
1817
+ assertNonEmptyString(decrypted?.address_key_b64, 'keyring address key');
1818
+ assertNonEmptyString(decrypted?.privateKey, 'keyring private signing key');
1819
+ keyBytes(decrypted.address_key_b64, 'address key');
1820
+
1821
+ const activeKeyEpoch = Number(bundle.vault?.active_key_epoch ?? keyring.active_key_epoch ?? 1);
1822
+ const rawContentKeys = Array.isArray(decrypted.content_keys) && decrypted.content_keys.length > 0
1823
+ ? decrypted.content_keys
1824
+ : [{
1825
+ epoch: activeKeyEpoch,
1826
+ key_id: bundle.vault?.key_id,
1827
+ status: 'active',
1828
+ created_at: bundle.vault?.created_at,
1829
+ key_b64: decrypted.vault_key_b64,
1830
+ }];
1831
+ const epochs = rawContentKeys.map((entry) => normalizeKeyEpoch(entry, undefined, undefined, bundle.vault?.created_at));
1832
+ const epochEntries = new Map(epochs.map((entry) => [entry.epoch, entry]));
1833
+ if (epochEntries.size !== epochs.length) throw new Error('duplicate content key epoch');
1834
+ if (!epochEntries.has(activeKeyEpoch)) throw new Error(`active content key epoch ${activeKeyEpoch} is unavailable`);
1835
+ if (decrypted.vault_key_b64 !== undefined
1836
+ && decrypted.vault_key_b64 !== epochEntries.get(activeKeyEpoch).key.toString('base64')) {
1837
+ throw new Error('active vault key alias does not match the active content key epoch');
1838
+ }
1839
+ if (strict) validateStrictBundle(bundle, keyring, decrypted, epochEntries);
1840
+
1841
+ const restoredSigningKeyPair = {
1842
+ key_id: keyring.signer?.key_id,
1843
+ signer: keyring.signer,
1844
+ publicKey: keyring.publicKey,
1845
+ privateKey: decrypted.privateKey,
1846
+ };
861
1847
  const vault = createVault({
862
1848
  vault_id: bundle.vault?.vault_id,
863
1849
  tenant_id: bundle.vault?.tenant_id,
864
1850
  subject_id: bundle.vault?.subject_id,
865
1851
  actor_id: args.actor_id ?? args.actorId ?? bundle.vault?.actor_id,
866
1852
  policy_id: bundle.vault?.policy_id,
867
- vault_key: bundle.keyring?.vault_key_b64,
868
- address_key: bundle.keyring?.address_key_b64,
1853
+ vault_key: epochEntries.get(activeKeyEpoch).key,
1854
+ address_key: decrypted.address_key_b64,
1855
+ key_epochs: epochs,
1856
+ active_key_epoch: activeKeyEpoch,
1857
+ allow_old_epoch_reads: args.allowOldEpochReads ?? args.allow_old_epoch_reads
1858
+ ?? (keyring.old_epoch_read_policy ?? bundle.vault?.old_epoch_read_policy) !== 'active_epoch_only',
869
1859
  signingKeyPair: restoredSigningKeyPair,
870
- signer: bundle.keyring?.signer,
1860
+ signer: keyring.signer,
871
1861
  now: bundle.vault?.created_at,
872
1862
  });
873
1863
  vault.created_at = bundle.vault?.created_at ?? vault.created_at;
874
1864
  vault.updated_at = bundle.vault?.updated_at ?? vault.updated_at;
875
1865
  vault.sequence = Number.isInteger(bundle.vault?.sequence) ? bundle.vault.sequence : 0;
876
- vault.events = Array.isArray(bundle.events) ? bundle.events.map(sanitizeImportedEventEntry).filter(Boolean) : [];
1866
+ vault.events = Array.isArray(bundle.events)
1867
+ ? (strict
1868
+ ? bundle.events.map((entry) => ({ ...entry, event: { ...entry.event } }))
1869
+ : bundle.events.map(sanitizeImportedEventEntry).filter(Boolean))
1870
+ : [];
877
1871
  vault.receipts = Array.isArray(bundle.receipts) ? bundle.receipts.map((receipt) => ({
878
- ...sanitizePublicValue(receipt),
879
- signer: receipt.signer ? { ...sanitizePublicValue(receipt.signer) } : receipt.signer,
880
- signature: receipt.signature ? { ...sanitizePublicValue(receipt.signature) } : receipt.signature,
1872
+ ...(strict ? receipt : sanitizePublicValue(receipt)),
1873
+ signer: receipt.signer ? { ...(strict ? receipt.signer : sanitizePublicValue(receipt.signer)) } : receipt.signer,
1874
+ signature: receipt.signature ? { ...(strict ? receipt.signature : sanitizePublicValue(receipt.signature)) } : receipt.signature,
881
1875
  })) : [];
882
1876
  vault.memories = new Map();
883
1877
  for (const record of bundle.memory_objects ?? []) {
884
- vault.memories.set(record.memory_addr, publicRecord(record));
1878
+ const imported = strict ? record : publicRecord(record);
1879
+ vault.memories.set(record.memory_addr, { ...imported, content_ciphertext: { ...imported.content_ciphertext } });
885
1880
  }
886
1881
  const lifecycle = reconcileImportedLifecycle(bundle, vault.memories, vault.receipts);
887
1882
  vault.activeAddresses = lifecycle.active;
888
1883
  vault.tombstones = lifecycle.tombstones;
889
- const roots = vault.__computeRoots();
890
- vault.active_set_root = roots.active_set_root;
891
- vault.receipt_log_root = roots.receipt_log_root;
892
- appendEvent(vault, {
893
- operation: 'import',
894
- actor_id: args.actor_id ?? args.actorId,
895
- policy_id: args.policy_id ?? args.policyId,
896
- now: args.now,
897
- metadata: {
898
- imported_memory_count: vault.memories.size,
899
- imported_active_count: vault.activeAddresses.size,
900
- imported_tombstone_count: vault.tombstones.size,
901
- },
902
- });
903
- return { vault, active_memory_addresses: [...vault.activeAddresses].sort(), tombstones: [...vault.tombstones.values()] };
1884
+ applyLifecycleRoots(vault);
1885
+ if (args.recordImport !== false) {
1886
+ appendEvent(vault, {
1887
+ operation: 'import',
1888
+ actor_id: args.actor_id ?? args.actorId,
1889
+ policy_id: args.policy_id ?? args.policyId,
1890
+ now: args.now,
1891
+ metadata: {
1892
+ imported_memory_count: vault.memories.size,
1893
+ imported_active_count: vault.activeAddresses.size,
1894
+ imported_tombstone_count: vault.tombstones.size,
1895
+ },
1896
+ });
1897
+ }
1898
+ return {
1899
+ vault,
1900
+ active_memory_addresses: [...vault.activeAddresses].sort(),
1901
+ tombstones: [...vault.tombstones.values()],
1902
+ warnings,
1903
+ verified: strict,
1904
+ };
904
1905
  }
1906
+
1907
+ export * from './poseidon.js';
1908
+ export * from './enclave.js';
1909
+ // Named (not star) export: e2ee.mjs also defines poseidon(), which would conflict
1910
+ // with poseidon.js under export *.
1911
+ export {
1912
+ E2EE_SCHEMA,
1913
+ PAYLOAD_COMMIT_DOMAIN,
1914
+ HPKE_WRAP_INFO,
1915
+ BN254_SCALAR_FIELD,
1916
+ canonicalJson,
1917
+ normalizeFieldElement32,
1918
+ deriveScopeCommit,
1919
+ deriveNoteContext,
1920
+ deriveAad,
1921
+ generateRecipientKeyPair,
1922
+ derivePayloadCommit,
1923
+ deriveFinalNote,
1924
+ e2eeEncrypt,
1925
+ e2eeDecrypt,
1926
+ } from './e2ee.mjs';
1927
+ export * from './receipt.js';
1928
+ export * from './erasure.js';
1929
+ export * from './persistence.js';