enigma-memory 0.1.18 → 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 (276) hide show
  1. package/README.md +76 -24
  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 +1923 -285
  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/docker-compose.local-production-simulation.yml +36 -0
  74. package/docs/browser-extension-install.md +8 -6
  75. package/docs/client-connectors.md +15 -11
  76. package/docs/developer-ecosystem.md +15 -13
  77. package/docs/enigma-memory-ready-conformance.md +11 -9
  78. package/docs/install-anywhere.md +61 -28
  79. package/docs/installers-and-desktop.md +8 -7
  80. package/docs/novelty-invention-candidates.md +161 -161
  81. package/docs/proof-network-claim-boundaries.md +320 -318
  82. package/examples/01-quickstart-agent/index.mjs +49 -0
  83. package/examples/01_agent_memory_quickstart.mjs +57 -0
  84. package/examples/02-multi-agent-swarm/index.mjs +57 -0
  85. package/examples/02_cross_model_passport.mjs +64 -0
  86. package/examples/03-langchain-memory/index.mjs +41 -0
  87. package/examples/03_poseidon_commitment_verification.mjs +71 -0
  88. package/examples/04-python-trading-agent/trader.py +49 -0
  89. package/examples/README.md +27 -0
  90. package/examples/ci/github-actions.yml +7 -2
  91. package/package.json +142 -11
  92. package/packages/adapters/PACKAGE_CONTRACT.md +1 -1
  93. package/packages/connectors/src/index.js +196 -4
  94. package/packages/connectors/swarm-router.mjs +168 -0
  95. package/packages/core/src/index.js +248 -1
  96. package/packages/core/src/version.mjs +7 -0
  97. package/packages/dev-tools/package.json +19 -0
  98. package/packages/dev-tools/src/index.js +4 -0
  99. package/packages/dev-tools/src/memory-benchmark-suite.js +112 -0
  100. package/packages/dev-tools/src/swarm-simulator.js +101 -0
  101. package/packages/dev-tools/src/vault-inspector.js +114 -0
  102. package/packages/dev-tools/src/vector-benchmark.js +100 -0
  103. package/packages/developer-platform/src/access-credentials.js +341 -0
  104. package/packages/developer-platform/src/http.js +132 -0
  105. package/packages/developer-platform/src/index.js +4 -0
  106. package/packages/developer-platform/src/usage-http.js +60 -0
  107. package/packages/developer-platform/src/usage.js +295 -0
  108. package/packages/enclave-runtime/attestation.mjs +159 -0
  109. package/packages/enclave-runtime/index.mjs +47 -0
  110. package/packages/enclave-runtime/session-manager.mjs +253 -0
  111. package/packages/enclave-runtime/zeroization-proof.mjs +227 -0
  112. package/packages/enigma-reflex/package.json +14 -0
  113. package/packages/enigma-reflex/src/index.js +204 -0
  114. package/packages/enigma-reflex/training/generate-dataset.mjs +40 -0
  115. package/packages/enigma-reflex/training/requirements.txt +8 -0
  116. package/packages/enigma-reflex/training/train.py +314 -0
  117. package/packages/enigma-weave/LICENSE +22 -0
  118. package/packages/enigma-weave/UPSTREAM.json +21 -0
  119. package/packages/enigma-weave/package.json +14 -0
  120. package/packages/enigma-weave/src/index.js +286 -0
  121. package/packages/hosted-cloud/src/index.js +80 -5
  122. package/packages/importers/src/index.js +432 -0
  123. package/packages/inference-runtime/src/browser.js +401 -0
  124. package/packages/inference-runtime/src/chat.js +265 -0
  125. package/packages/inference-runtime/src/code.js +407 -0
  126. package/packages/inference-runtime/src/contracts.js +162 -0
  127. package/packages/inference-runtime/src/http.js +232 -0
  128. package/packages/inference-runtime/src/image.js +186 -0
  129. package/packages/inference-runtime/src/index.js +10 -0
  130. package/packages/inference-runtime/src/model-router.js +320 -0
  131. package/packages/inference-runtime/src/platform.js +125 -0
  132. package/packages/inference-runtime/src/privacy.js +400 -0
  133. package/packages/inference-runtime/src/video.js +253 -0
  134. package/packages/mcp-server/README.md +22 -6
  135. package/packages/mcp-server/bin/enigma-mcp.mjs +2 -1
  136. package/packages/mcp-server/src/index.js +1418 -105
  137. package/packages/mcp-server/src/oauth.js +561 -0
  138. package/packages/mcp-server/src/private-handoff.js +84 -0
  139. package/packages/mcp-server/src/remote-http.js +273 -0
  140. package/packages/mcp-server/src/remote-policy.js +72 -0
  141. package/packages/mcp-server/swarm-bridge.mjs +361 -0
  142. package/packages/mesh/index.d.ts +283 -0
  143. package/packages/mesh/package.json +23 -0
  144. package/packages/mesh/src/crypto.js +189 -0
  145. package/packages/mesh/src/federation-packets.js +353 -0
  146. package/packages/mesh/src/gossip.js +311 -0
  147. package/packages/mesh/src/index.js +6 -0
  148. package/packages/mesh/src/protocol.js +255 -0
  149. package/packages/mesh/src/router.js +279 -0
  150. package/packages/mesh/src/transport.js +306 -0
  151. package/packages/passport/src/index.js +426 -1
  152. package/packages/private-economy/src/credits-http.js +100 -0
  153. package/packages/private-economy/src/credits.js +447 -0
  154. package/packages/private-economy/src/index.js +5 -0
  155. package/packages/private-economy/src/payments-http.js +120 -0
  156. package/packages/private-economy/src/payments.js +509 -0
  157. package/packages/private-economy/src/x402.js +346 -0
  158. package/packages/proof-network/PACKAGE_CONTRACT.md +21 -0
  159. package/packages/rag/index.d.ts +182 -0
  160. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/THIRD_PARTY_LICENSES.txt +207 -0
  161. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/config.json +25 -0
  162. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/onnx/model_quantized.onnx +0 -0
  163. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/sha256-manifest.json +28 -0
  164. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer.json +30686 -0
  165. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer_config.json +15 -0
  166. package/packages/rag/package.json +27 -0
  167. package/packages/rag/src/blinded-search.js +109 -0
  168. package/packages/rag/src/bm25.js +169 -0
  169. package/packages/rag/src/embeddings.js +459 -0
  170. package/packages/rag/src/hybrid.js +76 -0
  171. package/packages/rag/src/index.js +38 -0
  172. package/packages/rag/src/reranker.js +61 -0
  173. package/packages/rag/src/research.js +107 -0
  174. package/packages/rag/src/vector-store.js +430 -0
  175. package/packages/rag/src/verify-model-artifacts.mjs +4 -0
  176. package/packages/sdk/index.d.ts +760 -0
  177. package/packages/sdk/package.json +33 -0
  178. package/packages/sdk/python/README.md +24 -0
  179. package/packages/sdk/python/enigma_sdk.py +250 -0
  180. package/packages/sdk/python/pyproject.toml +34 -0
  181. package/packages/sdk/python/requirements.txt +1 -0
  182. package/packages/sdk/python/setup.py +20 -0
  183. package/packages/sdk/src/federation/capability-grant.js +389 -0
  184. package/packages/sdk/src/federation/federation-router.js +360 -0
  185. package/packages/sdk/src/federation/ghostmesh-bridge.js +497 -0
  186. package/packages/sdk/src/federation/index.js +3 -0
  187. package/packages/sdk/src/index.js +1796 -0
  188. package/packages/sdk/src/intelligence/contradiction.js +337 -0
  189. package/packages/sdk/src/intelligence/decision-engine.js +155 -0
  190. package/packages/sdk/src/intelligence/index.js +4 -0
  191. package/packages/sdk/src/intelligence/ontology.js +122 -0
  192. package/packages/sdk/src/intelligence/temporal.js +123 -0
  193. package/packages/sdk/src/market-client.js +142 -0
  194. package/packages/sdk/src/mesh-client.js +110 -0
  195. package/packages/sdk/src/middleware/index.js +3 -0
  196. package/packages/sdk/src/middleware/langchain.js +159 -0
  197. package/packages/sdk/src/middleware/llamaindex.js +101 -0
  198. package/packages/sdk/src/middleware/vercel-ai.js +112 -0
  199. package/packages/sdk/src/rag-client.js +85 -0
  200. package/packages/sdk/src/swarm-orchestrator.js +260 -0
  201. package/packages/settlement/PACKAGE_CONTRACT.md +1 -1
  202. package/packages/snapcompact/THIRD_PARTY_LICENSES.txt +40 -0
  203. package/packages/snapcompact/assets/8x13-latin1.bdf +3837 -0
  204. package/packages/snapcompact/index.d.ts +284 -0
  205. package/packages/snapcompact/package.json +25 -0
  206. package/packages/snapcompact/src/index.js +716 -0
  207. package/packages/storage/PACKAGE_CONTRACT.md +1 -1
  208. package/packages/terminal-console/animations.mjs +240 -0
  209. package/packages/terminal-console/auto-anchor.mjs +220 -0
  210. package/packages/terminal-console/banner.mjs +91 -0
  211. package/packages/terminal-console/commands.mjs +459 -0
  212. package/packages/terminal-console/delegation.mjs +152 -0
  213. package/packages/terminal-console/index.mjs +5 -0
  214. package/packages/terminal-console/outbox.mjs +143 -0
  215. package/packages/terminal-console/phantom-bridge.mjs +637 -0
  216. package/packages/terminal-console/repl.mjs +136 -0
  217. package/packages/terminal-console/signer-store.mjs +130 -0
  218. package/packages/terminal-console/solana-rpc.mjs +214 -0
  219. package/packages/terminal-console/solana-transport.mjs +189 -0
  220. package/packages/terminal-tui/dashboard.mjs +214 -0
  221. package/packages/terminal-tui/index.mjs +28 -0
  222. package/packages/terminal-tui/merkle-tree-renderer.mjs +268 -0
  223. package/packages/terminal-tui/telemetry-hud.mjs +137 -0
  224. package/packages/vault/index.d.ts +449 -0
  225. package/packages/vault/package.json +27 -0
  226. package/packages/vault/src/e2ee.mjs +393 -0
  227. package/packages/vault/src/enclave.js +481 -0
  228. package/packages/vault/src/erasure.js +207 -0
  229. package/packages/vault/src/index.js +1018 -155
  230. package/packages/vault/src/persistence.js +307 -0
  231. package/packages/vault/src/poseidon.js +354 -0
  232. package/packages/vault/src/receipt.js +459 -0
  233. package/scripts/benchmark-optical-context.mjs +166 -0
  234. package/scripts/bootstrap-enigma.mjs +502 -0
  235. package/scripts/build-edge-backend-workers.mjs +20 -5
  236. package/scripts/build-goal-completion-audit.mjs +72 -25
  237. package/scripts/build-hosted-api-key-lifecycle.mjs +26 -8
  238. package/scripts/build-hosted-customer-lifecycle.mjs +20 -3
  239. package/scripts/build-hosted-probe-worker.mjs +19 -4
  240. package/scripts/build-installer-assets.mjs +41 -21
  241. package/scripts/build-operator-evidence-starter.mjs +59 -1
  242. package/scripts/build-production-backend-env-kit.mjs +2 -0
  243. package/scripts/build-production-unblocker.mjs +3 -0
  244. package/scripts/check.mjs +17 -3
  245. package/scripts/collect-hosted-backend-live-evidence.mjs +49 -12
  246. package/scripts/install-enigma-local.mjs +18 -5
  247. package/scripts/release-audit.mjs +65 -109
  248. package/scripts/release-provenance.mjs +6 -0
  249. package/scripts/run-backend-readiness-smoke.mjs +112 -10
  250. package/scripts/scan-secrets.mjs +1 -0
  251. package/scripts/simulate-production-env.mjs +7 -2
  252. package/scripts/validate-hosted-backend-live.mjs +112 -1
  253. package/specs/antibody-pack-v1.schema.json +95 -0
  254. package/specs/antigen-envelope-v1.schema.json +81 -0
  255. package/specs/boundary-manifest-v1.schema.json +35 -35
  256. package/specs/capsule-v1.schema.json +55 -55
  257. package/specs/claim-boundary-manifest-v1.schema.json +22 -22
  258. package/specs/claim-ledger-v1.schema.json +291 -0
  259. package/specs/context-passport-v1.schema.json +59 -0
  260. package/specs/deletion-tombstone-v1.schema.json +26 -26
  261. package/specs/evidence-packet-v1.schema.json +177 -0
  262. package/specs/hosted-backend-live-evidence-v1.schema.json +72 -3
  263. package/specs/immune-scan-report-v1.schema.json +112 -0
  264. package/specs/lifecycle-receipt-log-v1.schema.json +67 -0
  265. package/specs/memory-atom-v1.schema.json +59 -0
  266. package/specs/memory-event-v1.schema.json +42 -42
  267. package/specs/passport-v1.schema.json +50 -50
  268. package/specs/proof-of-non-use-v1.schema.json +65 -0
  269. package/specs/quarantine-record-v1.schema.json +126 -0
  270. package/specs/receipt-v1.schema.json +61 -61
  271. package/specs/state-checkpoint-v1.schema.json +37 -37
  272. package/specs/trust-bundle-v1.schema.json +56 -56
  273. package/specs/trust-card-v1.schema.json +119 -0
  274. package/docs/proof-network-launch-plan.md +0 -421
  275. package/packages/metering/PACKAGE_CONTRACT.md +0 -20
  276. package/scripts/build-ai-orchestration-plan.mjs +0 -248
@@ -13,18 +13,22 @@ import {
13
13
  } from 'node:crypto';
14
14
  import {
15
15
  canonicalize,
16
- createMemoryAddress,
17
16
  createReceipt,
18
17
  generateSigningKeyPair,
19
18
  MerkleSet,
20
19
  hmacSha256Hex,
21
20
  receiptHash,
22
21
  sha256Hex,
22
+ verifyReceiptChain,
23
23
  } from '../../core/src/index.js';
24
24
 
25
- const VAULT_SCHEMA = 'enigma.vault.local.v1';
26
- const EVENT_SCHEMA = 'enigma.memory_event.v1';
27
- 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';
28
32
  const ZERO_ROOT = new MerkleSet().root();
29
33
  const RECEIPT_PLAINTEXT_KEYS = new Set(['content', 'plaintext', 'text', 'memory', 'prompt', 'response']);
30
34
  const PUBLIC_PLAINTEXT_KEYS = new Set([
@@ -49,9 +53,20 @@ const PUBLIC_PLAINTEXT_KEYS = new Set([
49
53
  ]);
50
54
  const SAFE_METADATA_DIGEST_RE = /^(?:sha256:)?[a-f0-9]{64}$|^hmac-sha256:[a-f0-9]{64}$/;
51
55
  const SOURCE_REF_ROOT_RE = /^sha256:[a-f0-9]{64}$/;
52
-
53
56
  export const KEYRING_ENCRYPTED_TYPE = 'local_secret_encrypted';
54
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
+ };
55
70
  const DERIVED_KEY_LENGTH = 128; // 32 bytes each: KEK, vaultKey, addressKey, signingSeed
56
71
  const SALT_LENGTH = 16;
57
72
  const IV_LENGTH = 12;
@@ -99,44 +114,54 @@ function isSafeMetadataPrimitive(key, value) {
99
114
  return false;
100
115
  }
101
116
 
102
- function commitMetadataValue(out, key, value) {
103
- 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
+ );
104
127
  }
105
128
 
106
- function sanitizeSourceRefValue(value) {
129
+ function sanitizeSourceRefValue(value, commitmentKey) {
107
130
  if (value === undefined || value === null) return undefined;
108
- 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
+ }
109
134
  if (['number', 'boolean'].includes(typeof value)) return value;
110
- 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);
111
136
  if (typeof value !== 'object') return undefined;
112
137
 
113
138
  const out = {};
114
139
  for (const [key, item] of Object.entries(value)) {
115
140
  if (isSourceRefPlaintextLikeKey(key)) {
116
141
  if (item !== undefined && item !== null && ['string', 'number', 'boolean'].includes(typeof item)) {
117
- out[privateCommitmentKey(key)] = asSha256(String(item));
142
+ out[privateCommitmentKey(key)] = privateValueCommitment(commitmentKey, String(item));
118
143
  }
119
144
  continue;
120
145
  }
121
- 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)) {
122
147
  out[key] = item;
123
148
  continue;
124
149
  }
125
- const sanitized = sanitizeSourceRefValue(item);
150
+ const sanitized = sanitizeSourceRefValue(item, commitmentKey);
126
151
  if (sanitized !== undefined) out[key] = sanitized;
127
152
  }
128
153
  return Object.keys(out).length > 0 ? out : undefined;
129
154
  }
130
155
 
131
- function sanitizeSourceRefs(refs) {
156
+ function sanitizeSourceRefs(refs, commitmentKey) {
132
157
  if (!Array.isArray(refs)) return [];
133
158
  const out = [];
134
159
  for (const ref of refs) {
135
160
  if (typeof ref === 'string') {
136
- out.push({ source_hash: asSha256(ref) });
161
+ out.push({ source_hash: privateValueCommitment(commitmentKey, ref) });
137
162
  continue;
138
163
  }
139
- const sanitized = sanitizeSourceRefValue(ref);
164
+ const sanitized = sanitizeSourceRefValue(ref, commitmentKey);
140
165
  if (sanitized !== undefined) out.push(sanitized);
141
166
  }
142
167
  return out;
@@ -187,12 +212,15 @@ function receiptEvent(receipt) {
187
212
  function deriveReceiptLifecycle(receipts) {
188
213
  const active = new Set();
189
214
  const tombstones = new Map();
215
+ let activeRoot = computeSetRoot(active);
190
216
  for (const receipt of receipts) {
191
217
  const event = receiptEvent(receipt);
192
218
  if (!event) continue;
219
+ let activeChanged = false;
193
220
  if (event.operation === 'create' && event.memory_addr) {
194
221
  active.add(event.memory_addr);
195
222
  tombstones.delete(event.memory_addr);
223
+ activeChanged = true;
196
224
  } else if (event.operation === 'update' && event.memory_addr) {
197
225
  if (event.source_addr) {
198
226
  active.delete(event.source_addr);
@@ -207,6 +235,7 @@ function deriveReceiptLifecycle(receipts) {
207
235
  }
208
236
  active.add(event.memory_addr);
209
237
  tombstones.delete(event.memory_addr);
238
+ activeChanged = true;
210
239
  } else if (event.operation === 'delete' && event.memory_addr) {
211
240
  active.delete(event.memory_addr);
212
241
  tombstones.set(event.memory_addr, {
@@ -216,8 +245,14 @@ function deriveReceiptLifecycle(receipts) {
216
245
  tombstoned_at: event.timestamp,
217
246
  reason: 'deleted',
218
247
  });
248
+ activeChanged = true;
249
+ } else if (event.operation === 'key_reset') {
250
+ active.clear();
251
+ tombstones.clear();
252
+ activeChanged = true;
219
253
  }
220
- 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) {
221
256
  throw new Error('importBundle receipt active_set_root mismatch');
222
257
  }
223
258
  }
@@ -339,19 +374,55 @@ function buildEd25519Pkcs8(seed) {
339
374
  ]);
340
375
  }
341
376
 
342
- function deriveMasterKeyFromPassphrase(passphrase, salt) {
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) {
343
408
  const normalized = Buffer.from(String(passphrase), 'utf8');
409
+ if (typeof salt === 'string' && salt.length > 128) throw new Error('Encoded salt is too large');
344
410
  const saltBuf = typeof salt === 'string' ? Buffer.from(salt, 'base64') : Buffer.from(salt);
345
- if (saltBuf.length < 8) throw new Error('Salt must be at least 8 bytes');
346
- try {
347
- return scryptSync(normalized, saltBuf, DERIVED_KEY_LENGTH);
348
- } catch {
349
- return pbkdf2Sync(normalized, saltBuf, 600000, DERIVED_KEY_LENGTH, 'sha256');
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
+ });
350
420
  }
421
+ return pbkdf2Sync(normalized, saltBuf, validated.iterations, DERIVED_KEY_LENGTH, validated.hash);
351
422
  }
352
423
 
353
- function deriveKeysFromPassphrase(passphrase, salt) {
354
- const master = deriveMasterKeyFromPassphrase(passphrase, salt);
424
+ export function deriveKeysFromPassphrase(passphrase, salt, kdfSpec = DEFAULT_KDF_SPEC) {
425
+ const master = deriveMasterKeyFromPassphrase(passphrase, salt, kdfSpec);
355
426
  return {
356
427
  kek: master.subarray(0, 32),
357
428
  vaultKey: master.subarray(32, 64),
@@ -360,9 +431,29 @@ function deriveKeysFromPassphrase(passphrase, salt) {
360
431
  };
361
432
  }
362
433
 
363
- function encryptKeyring(plaintextKeyring, kek) {
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) {
364
452
  const iv = randomBytes(IV_LENGTH);
365
453
  const cipher = createCipheriv('aes-256-gcm', kek, iv);
454
+ if (aadMeta) {
455
+ cipher.setAAD(canonicalKeyringAad(aadMeta));
456
+ }
366
457
  const plaintext = Buffer.from(JSON.stringify(plaintextKeyring), 'utf8');
367
458
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
368
459
  const tag = cipher.getAuthTag();
@@ -373,19 +464,48 @@ function encryptKeyring(plaintextKeyring, kek) {
373
464
  };
374
465
  }
375
466
 
467
+ function zeroDerivedKeys(keys) {
468
+ for (const value of Object.values(keys ?? {})) value?.fill?.(0);
469
+ }
470
+
376
471
  export function decryptKeyring(keyring, passphrase) {
377
472
  if (keyring?.type !== KEYRING_ENCRYPTED_TYPE) throw new Error('Keyring is not encrypted');
378
- if (passphrase === undefined || passphrase === null || String(passphrase).length === 0) {
473
+ if (passphrase === undefined || passphrase === null || String(passphrase).trim().length === 0) {
379
474
  throw new Error('Encrypted keyring requires passphrase');
380
475
  }
381
- const { kek } = deriveKeysFromPassphrase(passphrase, keyring.salt);
382
- const decipher = createDecipheriv('aes-256-gcm', kek, Buffer.from(keyring.iv, 'base64'));
383
- decipher.setAuthTag(Buffer.from(keyring.tag, 'base64'));
384
- const plaintext = Buffer.concat([
385
- decipher.update(Buffer.from(keyring.ciphertext, 'base64')),
386
- decipher.final(),
387
- ]).toString('utf8');
388
- return JSON.parse(plaintext);
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
+ }
389
509
  }
390
510
 
391
511
  function signingPair(seed) {
@@ -456,7 +576,7 @@ function nowIso(now) {
456
576
  return new Date().toISOString();
457
577
  }
458
578
 
459
- function metadata(input) {
579
+ function metadata(input, commitmentKey) {
460
580
  const out = {};
461
581
  if (!input || typeof input !== 'object') return out;
462
582
  for (const [key, value] of Object.entries(input)) {
@@ -465,12 +585,12 @@ function metadata(input) {
465
585
  if (isSafeMetadataPrimitive(key, value)) {
466
586
  out[key] = value;
467
587
  } else {
468
- commitMetadataValue(out, key, value);
588
+ commitMetadataValue(out, key, value, commitmentKey);
469
589
  }
470
590
  continue;
471
591
  }
472
592
  if (Array.isArray(value) || typeof value === 'object') {
473
- commitMetadataValue(out, key, value);
593
+ commitMetadataValue(out, key, value, commitmentKey);
474
594
  }
475
595
  }
476
596
  return out;
@@ -558,7 +678,7 @@ function appendEvent(vault, fields) {
558
678
  sequence,
559
679
  policy_id: fields.policy_id ?? vault.policy_id,
560
680
  decision: fields.decision,
561
- metadata: metadata(fields.metadata),
681
+ metadata: metadata(fields.metadata, vault.addressKey),
562
682
  });
563
683
  const eventCanonical = canonical(event);
564
684
  const eventHash = asSha256(eventCanonical);
@@ -570,45 +690,75 @@ function appendEvent(vault, fields) {
570
690
  vault.receipts.push(receipt);
571
691
  vault.sequence += 1;
572
692
  vault.updated_at = event.timestamp;
573
- vault.active_set_root = activeSetRoot;
574
- vault.receipt_log_root = computeReceiptLogRootFrom(vault.receipts);
693
+ Object.assign(vault, lifecycleRoots(vault));
575
694
  return { event, receipt };
576
695
  }
577
696
 
578
- function memoryAddress(vault, subjectId, memoryId, contentHash) {
579
- const payload = canonical({ domain: MEMORY_DOMAIN, subject_id: subjectId, memory_id: memoryId, content_hash: contentHash });
580
- try {
581
- const addr = createMemoryAddress({
582
- vault_addr_key: vault.addressKey,
583
- vaultAddressKey: vault.addressKey,
584
- key: vault.addressKey,
585
- value: payload,
586
- components: { domain: MEMORY_DOMAIN, subject_id: subjectId, memory_id: memoryId, content_hash: contentHash },
587
- domain: MEMORY_DOMAIN,
588
- subject_id: subjectId,
589
- subjectId,
590
- memory_id: memoryId,
591
- memoryId,
592
- content_hash: contentHash,
593
- contentHash,
594
- });
595
- if (typeof addr === 'string' && addr.length > 0) return addr;
596
- } catch {
597
- // Local HMAC construction below follows the protocol domain separation.
598
- }
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
+ });
599
713
  return `mem:${asHmacSha256(vault.addressKey, payload)}`;
600
714
  }
601
715
 
602
- 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 = {}) {
603
747
  const iv = randomBytes(12);
604
- const cipher = createCipheriv('aes-256-gcm', vault.vaultKey, iv);
605
- cipher.setAAD(Buffer.from(memoryAddr));
606
- 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()]);
607
757
  const tag = cipher.getAuthTag();
608
758
  return {
609
759
  alg: 'AES-256-GCM',
610
760
  iv: iv.toString('base64'),
611
- aad: memoryAddr,
761
+ aad,
612
762
  ciphertext: ciphertext.toString('base64'),
613
763
  tag: tag.toString('base64'),
614
764
  };
@@ -635,16 +785,31 @@ function getPlaintextCache(record) {
635
785
  }
636
786
 
637
787
  function decryptContent(vault, record) {
788
+ if (!record) throw new Error('memory record is unavailable');
789
+ const epochEntry = contentKeyForRecord(vault, record);
638
790
  const cached = getPlaintextCache(record);
639
791
  if (cached !== undefined) return cached;
640
792
  const encrypted = record.content_ciphertext;
641
- const decipher = createDecipheriv('aes-256-gcm', vault.vaultKey, Buffer.from(encrypted.iv, 'base64'));
642
- 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'));
643
798
  decipher.setAuthTag(Buffer.from(encrypted.tag, 'base64'));
644
- const plaintext = Buffer.concat([
799
+ const decrypted = Buffer.concat([
645
800
  decipher.update(Buffer.from(encrypted.ciphertext, 'base64')),
646
801
  decipher.final(),
647
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
+ }
648
813
  return setPlaintextCache(record, plaintext);
649
814
  }
650
815
 
@@ -652,22 +817,277 @@ function publicRecord(record) {
652
817
  return pruneUndefined(sanitizePublicValue(record));
653
818
  }
654
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
+
655
1041
  function attachInternals(vault) {
656
1042
  Object.defineProperties(vault, {
657
1043
  __recordEvent: { value: (fields) => appendEvent(vault, fields), enumerable: false },
658
1044
  __getRecord: { value: (memoryAddr) => vault.memories.get(memoryAddr), enumerable: false },
659
1045
  __getPlaintext: { value: (memoryAddr) => decryptContent(vault, vault.memories.get(memoryAddr)), enumerable: false },
660
1046
  __computeRoots: {
661
- value: () => ({
662
- active_set_root: computeSetRoot(vault.activeAddresses),
663
- receipt_log_root: computeReceiptLogRootFrom(vault.receipts),
664
- }),
1047
+ value: () => lifecycleRoots(vault),
665
1048
  enumerable: false,
666
1049
  },
667
1050
  });
668
1051
  return vault;
669
1052
  }
670
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
+
671
1091
  export function createVault(args = {}) {
672
1092
  const createdAt = nowIso(args.now);
673
1093
  const passphrase = args.passphrase;
@@ -676,8 +1096,13 @@ export function createVault(args = {}) {
676
1096
  let addressKey;
677
1097
  let signingSeed;
678
1098
  let keyringType = KEYRING_PLAINTEXT_TYPE;
679
- if (passphrase !== undefined && passphrase !== null && String(passphrase).length > 0) {
680
- const derived = deriveKeysFromPassphrase(passphrase, salt);
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);
681
1106
  vaultKey = derived.vaultKey;
682
1107
  addressKey = derived.addressKey;
683
1108
  signingSeed = derived.signingSeed;
@@ -687,6 +1112,20 @@ export function createVault(args = {}) {
687
1112
  addressKey = keyBytes(args.address_key ?? args.addressKey, 'address key');
688
1113
  }
689
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;
690
1129
  const vault = {
691
1130
  schema: VAULT_SCHEMA,
692
1131
  vault_id: args.vault_id ?? args.vaultId ?? `vault_${randomUUID()}`,
@@ -697,7 +1136,10 @@ export function createVault(args = {}) {
697
1136
  created_at: createdAt,
698
1137
  updated_at: createdAt,
699
1138
  encryption: 'local_aead',
700
- key_id: args.key_id ?? args.keyId ?? `vault-key-${randomUUID()}`,
1139
+ key_id: activeEpochEntry.key_id,
1140
+ activeKeyEpoch,
1141
+ keyEpochs,
1142
+ allowOldEpochReads: args.allow_old_epoch_reads ?? args.allowOldEpochReads ?? true,
701
1143
  vaultKey,
702
1144
  addressKey,
703
1145
  signingKeyPair,
@@ -707,14 +1149,127 @@ export function createVault(args = {}) {
707
1149
  receipts: [],
708
1150
  activeAddresses: new Set(),
709
1151
  tombstones: new Map(),
1152
+ quarantineAddresses: new Set(),
710
1153
  sequence: 0,
711
1154
  active_set_root: ZERO_ROOT,
1155
+ quarantine_set_root: ZERO_ROOT,
1156
+ tombstone_set_root: ZERO_ROOT,
712
1157
  receipt_log_root: ZERO_ROOT,
1158
+ lifecycle_root: ZERO_ROOT,
713
1159
  keyringType,
714
1160
  };
1161
+ applyLifecycleRoots(vault);
715
1162
  return attachInternals(vault);
716
1163
  }
717
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
+
718
1273
  function normalizeImportance(value) {
719
1274
  if (value === undefined || value === null) return undefined;
720
1275
  const num = Number(value);
@@ -731,30 +1286,38 @@ export function remember(args = {}) {
731
1286
  const memoryId = args.memory_id ?? args.memoryId ?? `mem_${randomUUID()}`;
732
1287
  const subjectId = args.subject_id ?? args.subjectId ?? vault.subject_id;
733
1288
  const createdAt = nowIso(args.now);
734
- const contentHash = asSha256(String(plaintext));
735
- 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);
736
1296
  if (vault.tombstones.has(memoryAddr)) throw new Error(`memory address is tombstoned: ${memoryAddr}`);
1297
+ const rawPurposeTags = normalizedPrivateTags(args.purpose_tags ?? args.purposeTags);
737
1298
 
738
1299
  const record = pruneUndefined({
739
- schema: 'enigma.memory_object.v1',
1300
+ schema: MEMORY_OBJECT_SCHEMA,
740
1301
  memory_id: memoryId,
741
1302
  memory_addr: memoryAddr,
742
1303
  subject_id: subjectId,
743
- kind: args.kind ?? 'fact',
744
- content_ciphertext: encryptContent(vault, memoryAddr, plaintext),
745
- content_hash: contentHash,
746
- content_commitment: `hmac-sha256:${asHmacSha256(vault.addressKey, contentHash)}`,
747
- source_refs: sanitizeSourceRefs(args.source_refs ?? args.sourceRefs),
748
- confidence: args.confidence ?? 'user_confirmed',
749
- sensitivity: args.sensitivity ?? 'normal',
750
- purpose_tags: Array.isArray(args.purpose_tags ?? args.purposeTags) ? (args.purpose_tags ?? args.purposeTags) : [],
751
- 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',
752
1314
  importance: normalizeImportance(args.importance),
753
1315
  state: 'active',
754
- metadata: metadata(args.metadata),
1316
+ metadata: metadata(args.metadata, vault.addressKey),
755
1317
  created_at: createdAt,
756
1318
  updated_at: createdAt,
757
1319
  });
1320
+ setPrivateTags(record, rawPurposeTags);
758
1321
 
759
1322
  vault.memories.set(memoryAddr, record);
760
1323
  vault.activeAddresses.add(memoryAddr);
@@ -814,22 +1377,34 @@ export function updateMemory(args = {}) {
814
1377
 
815
1378
  const oldRecord = vault.memories.get(oldAddr);
816
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);
817
1389
  oldRecord.state = 'superseded';
818
1390
  oldRecord.updated_at = nowIso(args.now);
819
1391
  vault.activeAddresses.delete(oldAddr);
820
1392
 
821
- const contentHash = asSha256(String(plaintext));
822
- 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);
823
1395
  const updatedAt = nowIso(args.now);
824
1396
  const newRecord = pruneUndefined({
825
1397
  ...publicRecord(oldRecord),
826
1398
  memory_addr: newAddr,
827
- content_ciphertext: encryptContent(vault, newAddr, plaintext),
828
- content_hash: contentHash,
829
- 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)),
830
1404
  state: 'active',
831
1405
  updated_at: updatedAt,
832
1406
  });
1407
+ setPrivateTags(newRecord, rawPurposeTags);
833
1408
 
834
1409
  vault.memories.set(newAddr, newRecord);
835
1410
  vault.activeAddresses.add(newAddr);
@@ -903,39 +1478,86 @@ export function deleteMemory(args = {}) {
903
1478
  return { memory_addr: memoryAddr, tombstone, event, receipt };
904
1479
  }
905
1480
 
906
- function buildKeyring(vault, passphrase) {
907
- if (passphrase !== undefined && passphrase !== null && String(passphrase).length > 0) {
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
+ }
908
1525
  const salt = randomBytes(SALT_LENGTH).toString('base64');
909
- const { kek } = deriveKeysFromPassphrase(passphrase, salt);
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 };
910
1529
  const plaintextKeyring = {
911
1530
  vault_key_b64: vault.vaultKey.toString('base64'),
912
1531
  address_key_b64: vault.addressKey.toString('base64'),
913
1532
  privateKey: exportPrivateKey(vault.signingKeyPair),
1533
+ content_keys: privateKeyEpochs(vault),
914
1534
  };
915
- const encrypted = encryptKeyring(plaintextKeyring, kek);
1535
+ let encrypted;
1536
+ try {
1537
+ encrypted = encryptKeyring(plaintextKeyring, derived.kek, aadMeta);
1538
+ } finally {
1539
+ zeroDerivedKeys(derived);
1540
+ }
916
1541
  return {
1542
+ ...aadMeta,
917
1543
  type: KEYRING_ENCRYPTED_TYPE,
918
1544
  warning: ENCRYPTED_KEYRING_WARNING,
919
- salt,
920
1545
  iv: encrypted.iv,
921
1546
  tag: encrypted.tag,
922
1547
  ciphertext: encrypted.ciphertext,
923
- signer: vault.signer,
924
- publicKey: exportPublicKey(vault.signingKeyPair),
925
1548
  };
926
1549
  }
927
1550
  return {
1551
+ ...common,
928
1552
  type: KEYRING_PLAINTEXT_TYPE,
929
1553
  unencrypted: true,
930
1554
  warning: PLAINTEXT_KEYRING_WARNING,
931
1555
  vault_key_b64: vault.vaultKey.toString('base64'),
932
1556
  address_key_b64: vault.addressKey.toString('base64'),
933
- signer: vault.signer,
934
- publicKey: exportPublicKey(vault.signingKeyPair),
1557
+ content_keys: privateKeyEpochs(vault),
935
1558
  privateKey: exportPrivateKey(vault.signingKeyPair),
936
1559
  };
937
1560
  }
938
-
939
1561
  export function exportBundle(args = {}) {
940
1562
  const vault = args.vault;
941
1563
  if (!vault) throw new Error('exportBundle requires a vault');
@@ -953,7 +1575,7 @@ export function exportBundle(args = {}) {
953
1575
 
954
1576
  const roots = vault.__computeRoots();
955
1577
  const bundle = {
956
- schema: 'enigma.vault_bundle.v1',
1578
+ schema: VAULT_BUNDLE_SCHEMA,
957
1579
  exported_at: nowIso(args.now),
958
1580
  vault: {
959
1581
  schema: vault.schema,
@@ -966,11 +1588,15 @@ export function exportBundle(args = {}) {
966
1588
  updated_at: vault.updated_at,
967
1589
  encryption: vault.encryption,
968
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',
969
1595
  active_set_root: roots.active_set_root,
970
1596
  receipt_log_root: roots.receipt_log_root,
971
1597
  sequence: vault.sequence,
972
1598
  },
973
- keyring: buildKeyring(vault, args.passphrase),
1599
+ keyring: buildKeyring(vault, args.passphrase, { kdf: args.kdf }),
974
1600
  memory_objects: [...vault.memories.values()].map(publicRecord),
975
1601
  active_memory_addresses: [...vault.activeAddresses].sort(),
976
1602
  tombstones: [...vault.tombstones.values()].sort((a, b) => a.memory_addr.localeCompare(b.memory_addr)),
@@ -986,81 +1612,318 @@ export function exportBundle(args = {}) {
986
1612
  return bundle;
987
1613
  }
988
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
+
989
1791
  export function importBundle(args = {}) {
990
- 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);
991
1793
  const bundle = typeof source === 'string' ? JSON.parse(source) : source;
992
- if (!bundle || bundle.schema !== 'enigma.vault_bundle.v1') throw new Error('importBundle requires an enigma.vault_bundle.v1 bundle');
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;
993
1797
  const warnings = [];
994
- let vaultKey;
995
- let addressKey;
996
- let privateKey;
997
1798
  const keyring = bundle.keyring;
1799
+ let decrypted;
998
1800
  if (keyring?.type === KEYRING_ENCRYPTED_TYPE) {
999
- if (args.passphrase === undefined || args.passphrase === null || String(args.passphrase).length === 0) {
1801
+ if (args.passphrase === undefined || args.passphrase === null || String(args.passphrase).trim().length === 0) {
1000
1802
  throw new Error('Encrypted keyring requires passphrase');
1001
1803
  }
1002
- const decrypted = decryptKeyring(keyring, args.passphrase);
1003
- vaultKey = decrypted.vault_key_b64;
1004
- addressKey = decrypted.address_key_b64;
1005
- privateKey = decrypted.privateKey;
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
+ };
1006
1814
  } else {
1007
- if (keyring?.type === KEYRING_PLAINTEXT_TYPE) {
1008
- warnings.push(DEPRECATION_PLAINTEXT_KEYRING);
1009
- }
1010
- vaultKey = keyring?.vault_key_b64;
1011
- addressKey = keyring?.address_key_b64;
1012
- privateKey = keyring?.privateKey;
1815
+ throw new Error(`Unsupported vault keyring type: ${keyring?.type ?? '<missing>'}`);
1013
1816
  }
1014
- const restoredSigningKeyPair = privateKey
1015
- ? {
1016
- key_id: keyring?.signer?.key_id,
1017
- signer: keyring?.signer,
1018
- publicKey: keyring?.publicKey,
1019
- privateKey,
1020
- }
1021
- : undefined;
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
+ };
1022
1847
  const vault = createVault({
1023
1848
  vault_id: bundle.vault?.vault_id,
1024
1849
  tenant_id: bundle.vault?.tenant_id,
1025
1850
  subject_id: bundle.vault?.subject_id,
1026
1851
  actor_id: args.actor_id ?? args.actorId ?? bundle.vault?.actor_id,
1027
1852
  policy_id: bundle.vault?.policy_id,
1028
- vault_key: vaultKey,
1029
- address_key: addressKey,
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',
1030
1859
  signingKeyPair: restoredSigningKeyPair,
1031
- signer: keyring?.signer,
1860
+ signer: keyring.signer,
1032
1861
  now: bundle.vault?.created_at,
1033
1862
  });
1034
1863
  vault.created_at = bundle.vault?.created_at ?? vault.created_at;
1035
1864
  vault.updated_at = bundle.vault?.updated_at ?? vault.updated_at;
1036
1865
  vault.sequence = Number.isInteger(bundle.vault?.sequence) ? bundle.vault.sequence : 0;
1037
- 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
+ : [];
1038
1871
  vault.receipts = Array.isArray(bundle.receipts) ? bundle.receipts.map((receipt) => ({
1039
- ...sanitizePublicValue(receipt),
1040
- signer: receipt.signer ? { ...sanitizePublicValue(receipt.signer) } : receipt.signer,
1041
- 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,
1042
1875
  })) : [];
1043
1876
  vault.memories = new Map();
1044
1877
  for (const record of bundle.memory_objects ?? []) {
1045
- 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 } });
1046
1880
  }
1047
1881
  const lifecycle = reconcileImportedLifecycle(bundle, vault.memories, vault.receipts);
1048
1882
  vault.activeAddresses = lifecycle.active;
1049
1883
  vault.tombstones = lifecycle.tombstones;
1050
- const roots = vault.__computeRoots();
1051
- vault.active_set_root = roots.active_set_root;
1052
- vault.receipt_log_root = roots.receipt_log_root;
1053
- appendEvent(vault, {
1054
- operation: 'import',
1055
- actor_id: args.actor_id ?? args.actorId,
1056
- policy_id: args.policy_id ?? args.policyId,
1057
- now: args.now,
1058
- metadata: {
1059
- imported_memory_count: vault.memories.size,
1060
- imported_active_count: vault.activeAddresses.size,
1061
- imported_tombstone_count: vault.tombstones.size,
1062
- },
1063
- });
1064
- return { vault, active_memory_addresses: [...vault.activeAddresses].sort(), tombstones: [...vault.tombstones.values()], warnings };
1065
-
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
+ };
1066
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';