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
@@ -0,0 +1,132 @@
1
+ const MAX_BODY_BYTES = 64 * 1024;
2
+ const KEY_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
3
+
4
+ class ApiKeyHttpError extends Error {
5
+ constructor(status, code, message) {
6
+ super(message);
7
+ this.name = 'ApiKeyHttpError';
8
+ this.status = status;
9
+ this.code = code;
10
+ }
11
+ }
12
+
13
+ function writeJson(response, status, payload) {
14
+ const body = JSON.stringify(payload);
15
+ response.statusCode = status;
16
+ response.setHeader('Cache-Control', 'no-store');
17
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
18
+ response.setHeader('Content-Length', Buffer.byteLength(body));
19
+ response.setHeader('X-Content-Type-Options', 'nosniff');
20
+ response.end(body);
21
+ }
22
+
23
+ async function readJson(request) {
24
+ const contentType = String(request.headers['content-type'] || '').split(';', 1)[0].trim().toLowerCase();
25
+ if (contentType !== 'application/json') throw new ApiKeyHttpError(415, 'unsupported_media_type', 'Content-Type must be application/json');
26
+ const chunks = [];
27
+ let bytes = 0;
28
+ let tooLarge = false;
29
+ for await (const chunk of request) {
30
+ bytes += chunk.length;
31
+ if (bytes > MAX_BODY_BYTES) tooLarge = true;
32
+ else chunks.push(chunk);
33
+ }
34
+ if (tooLarge) throw new ApiKeyHttpError(413, 'payload_too_large', 'request body exceeds 64 KiB');
35
+ try {
36
+ const value = JSON.parse(Buffer.concat(chunks, bytes).toString('utf8') || '{}');
37
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error();
38
+ return value;
39
+ } catch {
40
+ throw new ApiKeyHttpError(400, 'invalid_json', 'request body must be a JSON object');
41
+ }
42
+ }
43
+
44
+ function rejectUnknown(input, allowed) {
45
+ const unknown = Object.keys(input).find((key) => !allowed.has(key));
46
+ if (unknown) throw new ApiKeyHttpError(400, 'invalid_request', `unknown field: ${unknown}`);
47
+ }
48
+
49
+ function issueInput(input, principal) {
50
+ rejectUnknown(input, new Set(['label', 'scopes', 'ttlSeconds']));
51
+ return {
52
+ ownerId: principal.ownerId || principal.subject,
53
+ tenantId: principal.tenantId || 'default',
54
+ actorId: principal.subject || principal.ownerId,
55
+ label: input.label,
56
+ scopes: input.scopes,
57
+ ttlSeconds: input.ttlSeconds,
58
+ };
59
+ }
60
+
61
+ function operationStatus(error) {
62
+ if (error instanceof ApiKeyHttpError) return error;
63
+ if (error?.code === 'not_found') return new ApiKeyHttpError(404, 'not_found', 'API key not found');
64
+ if (error instanceof TypeError) return new ApiKeyHttpError(400, 'invalid_request', error.message);
65
+ if (/not found/i.test(error?.message || '')) return new ApiKeyHttpError(404, 'not_found', 'API key not found');
66
+ if (/limit|active API key|revoked API key/i.test(error?.message || '')) return new ApiKeyHttpError(409, 'conflict', error.message);
67
+ return new ApiKeyHttpError(500, 'internal_error', 'API key operation failed');
68
+ }
69
+
70
+ export function createApiKeyHttpHandler(options = {}) {
71
+ const apiKeys = options.apiKeys;
72
+ if (!apiKeys || typeof apiKeys.issue !== 'function' || typeof apiKeys.authenticate !== 'function') throw new TypeError('apiKeys service is required');
73
+ if (typeof options.authorizeAdmin !== 'function') throw new TypeError('authorizeAdmin is required');
74
+ const basePath = String(options.basePath || '/v1/api-keys').replace(/\/+$/, '');
75
+
76
+ return async function apiKeyHttpHandler(request, response) {
77
+ const url = new URL(request.url || '/', 'http://localhost');
78
+ if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return false;
79
+ try {
80
+ let principal;
81
+ try { principal = await options.authorizeAdmin({ request, headers: request.headers }); } catch { principal = null; }
82
+ if (!principal?.subject && !principal?.ownerId) throw new ApiKeyHttpError(401, 'unauthorized', 'administrator authentication is required');
83
+ const identity = {
84
+ ownerId: principal.ownerId || principal.subject,
85
+ tenantId: principal.tenantId || 'default',
86
+ actorId: principal.subject || principal.ownerId,
87
+ };
88
+
89
+ if (request.method === 'GET' && url.pathname === `${basePath}/scopes`) {
90
+ writeJson(response, 200, { schema: 'enigma.api_key_scopes.v1', scopes: apiKeys.supportedScopes });
91
+ return true;
92
+ }
93
+ if (request.method === 'GET' && url.pathname === `${basePath}/audit`) {
94
+ const limitValue = url.searchParams.get('limit');
95
+ const limit = limitValue === null ? undefined : Number(limitValue);
96
+ writeJson(response, 200, { schema: 'enigma.api_key_audit.v1', events: await apiKeys.audit({ ...identity, limit }) });
97
+ return true;
98
+ }
99
+ if (request.method === 'GET' && url.pathname === basePath) {
100
+ writeJson(response, 200, { schema: 'enigma.api_key_list.v1', keys: await apiKeys.list(identity) });
101
+ return true;
102
+ }
103
+ if (request.method === 'POST' && url.pathname === basePath) {
104
+ const input = await readJson(request);
105
+ writeJson(response, 201, await apiKeys.issue(issueInput(input, identity)));
106
+ return true;
107
+ }
108
+
109
+ const suffix = url.pathname.slice(basePath.length + 1);
110
+ const [id, action, extra] = suffix.split('/');
111
+ if (!KEY_ID_PATTERN.test(id || '') || extra) throw new ApiKeyHttpError(404, 'not_found', 'API key endpoint not found');
112
+ if (request.method === 'POST' && action === 'rotate') {
113
+ const input = await readJson(request);
114
+ rejectUnknown(input, new Set(['label', 'scopes', 'ttlSeconds']));
115
+ writeJson(response, 201, await apiKeys.rotate(id, { ...issueInput(input, identity) }));
116
+ return true;
117
+ }
118
+ if (request.method === 'DELETE' && action === undefined) {
119
+ const input = await readJson(request);
120
+ rejectUnknown(input, new Set(['reason']));
121
+ writeJson(response, 200, await apiKeys.revoke(id, { ...identity, reason: input.reason }));
122
+ return true;
123
+ }
124
+ throw new ApiKeyHttpError(405, 'method_not_allowed', 'API key endpoint method is not allowed');
125
+ } catch (error) {
126
+ const normalized = operationStatus(error);
127
+ if (normalized.status === 401) response.setHeader('WWW-Authenticate', 'Bearer');
128
+ writeJson(response, normalized.status, { error: { code: normalized.code, message: normalized.message } });
129
+ return true;
130
+ }
131
+ };
132
+ }
@@ -0,0 +1,4 @@
1
+ export * from './access-credentials.js';
2
+ export * from './http.js';
3
+ export * from './usage.js';
4
+ export * from './usage-http.js';
@@ -0,0 +1,60 @@
1
+ function json(response, status, payload) {
2
+ const body = JSON.stringify(payload);
3
+ response.statusCode = status;
4
+ response.setHeader('Cache-Control', 'no-store');
5
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
6
+ response.setHeader('Content-Length', Buffer.byteLength(body));
7
+ response.setHeader('X-Content-Type-Options', 'nosniff');
8
+ response.end(body);
9
+ }
10
+
11
+ function queryInput(url, principal) {
12
+ const daysValue = url.searchParams.get('days');
13
+ const days = daysValue === null ? 30 : Number(daysValue);
14
+ if (!Number.isInteger(days) || days < 1 || days > 400) throw new TypeError('days must be an integer from 1 through 400');
15
+ const to = url.searchParams.get('to') || undefined;
16
+ const parsedTo = to === undefined ? Date.now() + 1 : Date.parse(to);
17
+ if (!Number.isFinite(parsedTo)) throw new TypeError('to must be an ISO timestamp');
18
+ const from = url.searchParams.get('from') || new Date(parsedTo - days * 24 * 60 * 60_000).toISOString();
19
+ if (!Number.isFinite(Date.parse(from))) throw new TypeError('from must be an ISO timestamp');
20
+ const limitValue = url.searchParams.get('limit');
21
+ const limit = limitValue === null ? undefined : Number(limitValue);
22
+ return { principal, from, to: new Date(parsedTo).toISOString(), limit };
23
+ }
24
+
25
+ export function createUsageHttpHandler(options = {}) {
26
+ const usage = options.usage;
27
+ if (!usage || typeof usage.aggregate !== 'function' || typeof usage.events !== 'function') throw new TypeError('usage service is required');
28
+ if (typeof options.authorize !== 'function') throw new TypeError('usage HTTP authorizer is required');
29
+ const basePath = String(options.basePath || '/v1/usage').replace(/\/+$/, '');
30
+
31
+ return async function usageHttpHandler(request, response) {
32
+ const url = new URL(request.url || '/', 'http://localhost');
33
+ if (url.pathname !== basePath && url.pathname !== `${basePath}/events`) return false;
34
+ try {
35
+ let principal;
36
+ try { principal = await options.authorize({ request, headers: request.headers, requiredScopes: ['usage:read'] }); } catch { principal = null; }
37
+ if (!principal) {
38
+ response.setHeader('WWW-Authenticate', 'Bearer scope="usage:read"');
39
+ json(response, 401, { error: { code: 'unauthorized', message: 'usage:read authorization is required' } });
40
+ return true;
41
+ }
42
+ if (request.method !== 'GET') {
43
+ response.setHeader('Allow', 'GET');
44
+ json(response, 405, { error: { code: 'method_not_allowed', message: 'usage endpoints are read-only' } });
45
+ return true;
46
+ }
47
+ const input = queryInput(url, principal);
48
+ if (url.pathname === `${basePath}/events`) {
49
+ json(response, 200, { schema: 'enigma.platform_usage_event_list.v1', events: await usage.events(input) });
50
+ } else {
51
+ json(response, 200, await usage.aggregate(input));
52
+ }
53
+ return true;
54
+ } catch (error) {
55
+ const status = error instanceof TypeError ? 400 : 500;
56
+ json(response, status, { error: { code: status === 400 ? 'invalid_request' : 'usage_error', message: error.message || 'usage query failed' } });
57
+ return true;
58
+ }
59
+ };
60
+ }
@@ -0,0 +1,295 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdir, open, readFile, rename } from 'node:fs/promises';
4
+ import { dirname, resolve } from 'node:path';
5
+
6
+ export const ENIGMA_USAGE_STORE_SCHEMA = 'enigma.platform_usage_store.v1';
7
+ export const ENIGMA_USAGE_EVENT_SCHEMA = 'enigma.platform_usage_event.v1';
8
+ export const ENIGMA_USAGE_AGGREGATE_SCHEMA = 'enigma.platform_usage_aggregate.v1';
9
+ const MODALITY_UNITS = Object.freeze({ text: 'tokens', image: 'images', video: 'videos', code: 'executions', browser: 'sessions' });
10
+ const USAGE_INPUT_FIELDS = new Set([
11
+ 'modality', 'operation', 'provider', 'model', 'inputTokens', 'inputUnits', 'outputTokens', 'outputUnits',
12
+ 'totalTokens', 'units', 'durationMs', 'promptRef', 'routeReceiptId', 'occurredAt',
13
+ ]);
14
+
15
+ function requiredString(value, name, maximum = 512) {
16
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${name} is required`);
17
+ const normalized = value.trim();
18
+ if (normalized.length > maximum) throw new TypeError(`${name} exceeds ${maximum} characters`);
19
+ return normalized;
20
+ }
21
+
22
+ function nonNegativeInteger(value, name, fallback = 0) {
23
+ const resolved = value === undefined ? fallback : value;
24
+ if (!Number.isSafeInteger(resolved) || resolved < 0) throw new TypeError(`${name} must be a non-negative integer`);
25
+ return resolved;
26
+ }
27
+
28
+ function boundedInteger(value, name, minimum, maximum, fallback) {
29
+ const resolved = value === undefined ? fallback : value;
30
+ if (!Number.isInteger(resolved) || resolved < minimum || resolved > maximum) throw new TypeError(`${name} must be from ${minimum} through ${maximum}`);
31
+ return resolved;
32
+ }
33
+
34
+ function timestamp(value, name) {
35
+ const milliseconds = value === undefined ? Date.now() : typeof value === 'number' ? value : Date.parse(value);
36
+ if (!Number.isFinite(milliseconds)) throw new TypeError(`${name} must be an ISO timestamp or epoch milliseconds`);
37
+ return milliseconds;
38
+ }
39
+
40
+ function emptyState() {
41
+ return { schema: ENIGMA_USAGE_STORE_SCHEMA, revision: 0, events: {} };
42
+ }
43
+
44
+ function normalizePrincipal(value = {}) {
45
+ return {
46
+ ownerId: requiredString(value.ownerId || value.subject, 'usage ownerId'),
47
+ tenantId: requiredString(value.tenantId || value.tenant_id || 'default', 'usage tenantId'),
48
+ keyId: value.keyId ? requiredString(value.keyId, 'usage keyId') : null,
49
+ };
50
+ }
51
+
52
+ function eventPublicView(event) {
53
+ return {
54
+ schema: event.schema,
55
+ id: event.id,
56
+ tenantId: event.tenantId,
57
+ ownerId: event.ownerId,
58
+ keyId: event.keyId,
59
+ modality: event.modality,
60
+ operation: event.operation,
61
+ provider: event.provider,
62
+ model: event.model,
63
+ unit: event.unit,
64
+ inputUnits: event.inputUnits,
65
+ outputUnits: event.outputUnits,
66
+ units: event.units,
67
+ estimatedCostMicrousd: event.estimatedCostMicrousd,
68
+ priced: event.estimatedCostMicrousd !== null,
69
+ occurredAt: event.occurredAt,
70
+ receiptRef: event.receiptRef,
71
+ };
72
+ }
73
+
74
+ function createStoreAdapter(load, save, { clock, retentionMs, maxEvents }) {
75
+ let queue = Promise.resolve();
76
+ const transaction = (operation) => {
77
+ const run = queue.then(async () => {
78
+ const state = await load();
79
+ const cutoff = clock() - retentionMs;
80
+ for (const [id, event] of Object.entries(state.events)) {
81
+ if (Date.parse(event.occurredAt) < cutoff) delete state.events[id];
82
+ }
83
+ const result = await operation(state);
84
+ const ordered = Object.values(state.events).sort((a, b) => a.occurredAt.localeCompare(b.occurredAt));
85
+ for (let index = 0; index < ordered.length - maxEvents; index += 1) delete state.events[ordered[index].id];
86
+ state.revision += 1;
87
+ await save(state);
88
+ return result;
89
+ });
90
+ queue = run.catch(() => undefined);
91
+ return run;
92
+ };
93
+ const view = async (operation) => {
94
+ await queue;
95
+ return operation(await load());
96
+ };
97
+ return Object.freeze({
98
+ async append(event) {
99
+ return transaction((state) => {
100
+ if (state.events[event.id]) return { inserted: false, event: structuredClone(state.events[event.id]) };
101
+ state.events[event.id] = structuredClone(event);
102
+ return { inserted: true, event: structuredClone(event) };
103
+ });
104
+ },
105
+ async query({ ownerId, tenantId, from, to, limit }) {
106
+ return transaction((state) => Object.values(state.events)
107
+ .filter((event) => event.ownerId === ownerId && event.tenantId === tenantId)
108
+ .filter((event) => {
109
+ const at = Date.parse(event.occurredAt);
110
+ return at >= from && at < to;
111
+ })
112
+ .sort((a, b) => b.occurredAt.localeCompare(a.occurredAt))
113
+ .slice(0, limit)
114
+ .map((event) => structuredClone(event)));
115
+ },
116
+ });
117
+ }
118
+
119
+ export function createEphemeralUsageStore(options = {}) {
120
+ const clock = options.clock || Date.now;
121
+ const retentionMs = boundedInteger(options.retentionDays, 'retentionDays', 1, 3660, 400) * 24 * 60 * 60_000;
122
+ const maxEvents = boundedInteger(options.maxEvents, 'maxEvents', 100, 10_000_000, 100_000);
123
+ let state = emptyState();
124
+ return createStoreAdapter(async () => structuredClone(state), async (next) => { state = structuredClone(next); }, { clock, retentionMs, maxEvents });
125
+ }
126
+
127
+ export function createFileUsageStore(options = {}) {
128
+ const path = resolve(String(options.path || '.enigma/usage.json'));
129
+ const clock = options.clock || Date.now;
130
+ const retentionMs = boundedInteger(options.retentionDays, 'retentionDays', 1, 3660, 400) * 24 * 60 * 60_000;
131
+ const maxEvents = boundedInteger(options.maxEvents, 'maxEvents', 100, 10_000_000, 100_000);
132
+ async function load() {
133
+ try {
134
+ const state = JSON.parse(await readFile(path, 'utf8'));
135
+ if (state?.schema !== ENIGMA_USAGE_STORE_SCHEMA) throw new Error('unsupported usage store schema');
136
+ return state;
137
+ } catch (error) {
138
+ if (error.code === 'ENOENT') return emptyState();
139
+ throw error;
140
+ }
141
+ }
142
+ async function save(state) {
143
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
144
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
145
+ const handle = await open(temporary, 'wx', 0o600);
146
+ try {
147
+ await handle.writeFile(JSON.stringify(state));
148
+ await handle.sync();
149
+ } finally {
150
+ await handle.close();
151
+ }
152
+ await rename(temporary, path);
153
+ }
154
+ return createStoreAdapter(load, save, { clock, retentionMs, maxEvents });
155
+ }
156
+
157
+ function pricingCostMicrousd(input, pricing) {
158
+ if (!pricing) return null;
159
+ if (input.modality === 'text') {
160
+ if (!Number.isFinite(pricing.inputPerMillionMicrousd) || !Number.isFinite(pricing.outputPerMillionMicrousd)) return null;
161
+ return Math.round((input.inputUnits / 1_000_000) * pricing.inputPerMillionMicrousd
162
+ + (input.outputUnits / 1_000_000) * pricing.outputPerMillionMicrousd);
163
+ }
164
+ if (!Number.isFinite(pricing.perUnitMicrousd)) return null;
165
+ return Math.round(input.units * pricing.perUnitMicrousd);
166
+ }
167
+
168
+ function aggregateBucket(target, event) {
169
+ target.eventCount += 1;
170
+ target.inputUnits += event.inputUnits;
171
+ target.outputUnits += event.outputUnits;
172
+ target.units += event.units;
173
+ if (event.estimatedCostMicrousd === null) target.unpricedEvents += 1;
174
+ else target.knownCostMicrousd += event.estimatedCostMicrousd;
175
+ }
176
+
177
+ function emptyAggregateBucket(fields = {}) {
178
+ return { ...fields, eventCount: 0, inputUnits: 0, outputUnits: 0, units: 0, knownCostMicrousd: 0, unpricedEvents: 0 };
179
+ }
180
+
181
+ export function aggregatePlatformUsage(events, identity, { from, to } = {}) {
182
+ const totals = emptyAggregateBucket({ currency: 'USD' });
183
+ const byDay = new Map();
184
+ const byModel = new Map();
185
+ const byModality = new Map();
186
+ for (const event of events) {
187
+ aggregateBucket(totals, event);
188
+ const day = event.occurredAt.slice(0, 10);
189
+ if (!byDay.has(day)) byDay.set(day, emptyAggregateBucket({ day }));
190
+ aggregateBucket(byDay.get(day), event);
191
+ const modelKey = `${event.provider}\0${event.model}`;
192
+ if (!byModel.has(modelKey)) byModel.set(modelKey, emptyAggregateBucket({ provider: event.provider, model: event.model }));
193
+ aggregateBucket(byModel.get(modelKey), event);
194
+ if (!byModality.has(event.modality)) byModality.set(event.modality, emptyAggregateBucket({ modality: event.modality, unit: event.unit }));
195
+ aggregateBucket(byModality.get(event.modality), event);
196
+ }
197
+ return {
198
+ schema: ENIGMA_USAGE_AGGREGATE_SCHEMA,
199
+ generatedAt: new Date().toISOString(),
200
+ tenantId: identity.tenantId,
201
+ ownerId: identity.ownerId,
202
+ period: { from: new Date(from).toISOString(), to: new Date(to).toISOString() },
203
+ totals,
204
+ byDay: [...byDay.values()].sort((a, b) => a.day.localeCompare(b.day)),
205
+ byModel: [...byModel.values()].sort((a, b) => (a.provider + a.model).localeCompare(b.provider + b.model)),
206
+ byModality: [...byModality.values()].sort((a, b) => a.modality.localeCompare(b.modality)),
207
+ costBoundary: totals.unpricedEvents
208
+ ? 'knownCostMicrousd excludes unpriced events and is not a complete invoice total'
209
+ : 'knownCostMicrousd is an estimate from configured operator pricing, not a provider invoice',
210
+ contentStored: false,
211
+ };
212
+ }
213
+
214
+ export function createUsageService(options = {}) {
215
+ const store = options.store;
216
+ if (!store || typeof store.append !== 'function' || typeof store.query !== 'function') throw new TypeError('usage store is required');
217
+ const clock = options.clock || Date.now;
218
+ const context = new AsyncLocalStorage();
219
+ const defaultPrincipal = options.defaultPrincipal ? normalizePrincipal(options.defaultPrincipal) : null;
220
+ const pricing = options.pricing || {};
221
+ const maxQueryEvents = boundedInteger(options.maxQueryEvents, 'maxQueryEvents', 100, 1_000_000, 100_000);
222
+
223
+ function currentPrincipal(explicit) {
224
+ if (explicit) return normalizePrincipal(explicit);
225
+ const active = context.getStore()?.principal;
226
+ return active ? normalizePrincipal(active) : defaultPrincipal;
227
+ }
228
+
229
+ return Object.freeze({
230
+ run(requestContext, operation) {
231
+ if (typeof operation !== 'function') throw new TypeError('usage.run requires an operation');
232
+ return context.run({ principal: requestContext?.principal || null }, operation);
233
+ },
234
+ setPrincipal(principal) {
235
+ const active = context.getStore();
236
+ if (!active) return false;
237
+ active.principal = principal;
238
+ return true;
239
+ },
240
+ async record(input = {}, explicitPrincipal = null) {
241
+ const principal = currentPrincipal(explicitPrincipal);
242
+ if (!principal) throw new Error('usage event has no authenticated principal');
243
+ const modality = requiredString(input.modality, 'usage modality', 32);
244
+ if (!Object.hasOwn(MODALITY_UNITS, modality)) throw new TypeError('unsupported usage modality');
245
+ const inputUnits = nonNegativeInteger(input.inputTokens ?? input.inputUnits, 'input units');
246
+ const outputUnits = nonNegativeInteger(input.outputTokens ?? input.outputUnits, 'output units');
247
+ const units = nonNegativeInteger(input.units, 'units', modality === 'text' ? inputUnits + outputUnits : 1);
248
+ const provider = requiredString(input.provider || 'unknown', 'usage provider', 160);
249
+ const model = requiredString(input.model || 'unknown', 'usage model', 160);
250
+ const occurredAt = new Date(input.occurredAt ? timestamp(input.occurredAt, 'occurredAt') : clock()).toISOString();
251
+ const pricingRule = pricing[`${provider}/${model}`] || pricing[model] || pricing[modality] || null;
252
+ const normalized = { modality, inputUnits, outputUnits, units };
253
+ const forbiddenField = Object.keys(input).find((key) => !USAGE_INPUT_FIELDS.has(key));
254
+ if (forbiddenField) throw new TypeError(`usage input field ${forbiddenField} is not allowed`);
255
+ const event = {
256
+ schema: ENIGMA_USAGE_EVENT_SCHEMA,
257
+ id: randomUUID(),
258
+ tenantId: principal.tenantId,
259
+ ownerId: principal.ownerId,
260
+ keyId: principal.keyId,
261
+ modality,
262
+ operation: requiredString(input.operation || `${modality}.generate`, 'usage operation', 160),
263
+ provider,
264
+ model,
265
+ unit: MODALITY_UNITS[modality],
266
+ inputUnits,
267
+ outputUnits,
268
+ units,
269
+ estimatedCostMicrousd: pricingCostMicrousd(normalized, pricingRule),
270
+ occurredAt,
271
+ receiptRef: input.routeReceiptId ? requiredString(input.routeReceiptId, 'receiptRef', 512) : null,
272
+ };
273
+ const stored = await store.append(event);
274
+ return eventPublicView(stored.event);
275
+ },
276
+ async events(input = {}) {
277
+ const identity = currentPrincipal(input.principal || input);
278
+ if (!identity) throw new Error('usage query has no authenticated principal');
279
+ const to = input.to === undefined ? clock() + 1 : timestamp(input.to, 'to');
280
+ const from = input.from === undefined ? to - 30 * 24 * 60 * 60_000 : timestamp(input.from, 'from');
281
+ if (from >= to) throw new TypeError('usage from must be before to');
282
+ const limit = boundedInteger(input.limit, 'limit', 1, maxQueryEvents, Math.min(1000, maxQueryEvents));
283
+ return (await store.query({ ...identity, from, to, limit })).map(eventPublicView);
284
+ },
285
+ async aggregate(input = {}) {
286
+ const identity = currentPrincipal(input.principal || input);
287
+ if (!identity) throw new Error('usage aggregate has no authenticated principal');
288
+ const to = input.to === undefined ? clock() + 1 : timestamp(input.to, 'to');
289
+ const from = input.from === undefined ? to - 30 * 24 * 60 * 60_000 : timestamp(input.from, 'from');
290
+ if (from >= to) throw new TypeError('usage from must be before to');
291
+ const events = await store.query({ ...identity, from, to, limit: maxQueryEvents });
292
+ return aggregatePlatformUsage(events, identity, { from, to });
293
+ },
294
+ });
295
+ }
@@ -0,0 +1,159 @@
1
+ import { createHash, timingSafeEqual } from 'node:crypto';
2
+
3
+ const PCR_ZERO = Buffer.alloc(32);
4
+ const HEX_256 = /^[a-f0-9]{64}$/;
5
+
6
+ export const ENCLAVE_RUNTIME_MEASUREMENT = 'enigma-memory:enclave-runtime:v1';
7
+ export const ZEROIZATION_POLICY_MEASUREMENT = 'best-effort-volatile-buffer:random-ff-zero:three-pass:v1';
8
+
9
+ export function canonicalJson(value) {
10
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
11
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
12
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
13
+ }
14
+
15
+ export function sha256Hex(value) {
16
+ const input = Buffer.isBuffer(value) || value instanceof Uint8Array
17
+ ? value
18
+ : Buffer.from(typeof value === 'string' ? value : canonicalJson(value));
19
+ return createHash('sha256').update(input).digest('hex');
20
+ }
21
+
22
+ export function extendPcr(currentDigest, measurement) {
23
+ const current = currentDigest == null
24
+ ? PCR_ZERO
25
+ : Buffer.from(String(currentDigest).replace(/^sha256:/, ''), 'hex');
26
+ if (current.length !== 32) throw new TypeError('PCR digest must be a 32-byte SHA-256 value.');
27
+ const measured = Buffer.from(String(measurement).replace(/^sha256:/, ''), 'hex');
28
+ if (measured.length !== 32) throw new TypeError('Measurement must be a 32-byte SHA-256 value.');
29
+ return sha256Hex(Buffer.concat([current, measured]));
30
+ }
31
+
32
+ function measurePcr(input) {
33
+ return extendPcr(null, sha256Hex(input));
34
+ }
35
+
36
+ export function createAttestation({
37
+ bundle,
38
+ bundleHash,
39
+ nonce,
40
+ now = new Date().toISOString(),
41
+ runtimeMeasurement = ENCLAVE_RUNTIME_MEASUREMENT,
42
+ policyMeasurement = ZEROIZATION_POLICY_MEASUREMENT,
43
+ platform = `${process.platform}:${process.arch}:node-${process.versions.node.split('.')[0]}`,
44
+ } = {}) {
45
+ const resolvedBundleHash = bundleHash
46
+ ? String(bundleHash).replace(/^sha256:/, '').toLowerCase()
47
+ : bundle === undefined
48
+ ? sha256Hex('no-bundle')
49
+ : sha256Hex(bundle);
50
+ if (!HEX_256.test(resolvedBundleHash)) throw new TypeError('bundleHash must be a SHA-256 digest.');
51
+
52
+ const pcrs = {
53
+ '0': measurePcr(runtimeMeasurement),
54
+ '1': measurePcr(policyMeasurement),
55
+ '2': measurePcr(platform),
56
+ '3': extendPcr(null, resolvedBundleHash),
57
+ };
58
+ const measurementHash = sha256Hex({ pcrs, runtimeMeasurement, policyMeasurement });
59
+
60
+ return {
61
+ schema: 'enigma.enclave.attestation.v1',
62
+ mode: 'simulated',
63
+ provider: 'enigma-local-pcr-simulator',
64
+ measured_at: now,
65
+ nonce: nonce ?? null,
66
+ runtime_measurement: runtimeMeasurement,
67
+ zeroization_policy_measurement: policyMeasurement,
68
+ platform_measurement: platform,
69
+ bundle_commitment: `sha256:${resolvedBundleHash}`,
70
+ pcr_bank: 'sha256',
71
+ pcrs,
72
+ measurement_hash: `sha256:${measurementHash}`,
73
+ claim_boundary: {
74
+ hardware_backed: false,
75
+ pcr_registers_simulated: true,
76
+ buffer_overwrite_best_effort: true,
77
+ hardware_ram_zeroization_proved: false,
78
+ runtime_copies_gc_caches_in_scope: false,
79
+ physical_media_sanitization_claimed: false,
80
+ },
81
+ };
82
+ }
83
+
84
+ export function verifyAttestation(attestation, { expectedBundleHash, expectedMeasurementHash } = {}) {
85
+ const errors = [];
86
+ if (!attestation || typeof attestation !== 'object') {
87
+ return { valid: false, errors: ['ATTESTATION_REQUIRED'] };
88
+ }
89
+ if (attestation.schema !== 'enigma.enclave.attestation.v1') errors.push('INVALID_SCHEMA');
90
+ if (attestation.mode !== 'simulated') errors.push('UNSUPPORTED_ATTESTATION_MODE');
91
+ if (attestation.pcr_bank !== 'sha256') errors.push('UNSUPPORTED_PCR_BANK');
92
+
93
+ const runtimeMeasurement = attestation.runtime_measurement;
94
+ const policyMeasurement = attestation.zeroization_policy_measurement;
95
+ const platformMeasurement = attestation.platform_measurement;
96
+ const bundleDigest = String(attestation.bundle_commitment ?? '').replace(/^sha256:/, '');
97
+ const declarationsValid = typeof runtimeMeasurement === 'string' && runtimeMeasurement.length > 0
98
+ && typeof policyMeasurement === 'string' && policyMeasurement.length > 0
99
+ && typeof platformMeasurement === 'string' && platformMeasurement.length > 0
100
+ && HEX_256.test(bundleDigest);
101
+ if (!declarationsValid) errors.push('INVALID_MEASUREMENT_DECLARATION');
102
+
103
+ const suppliedPcrs = attestation.pcrs;
104
+ const suppliedBankValid = suppliedPcrs
105
+ && ['0', '1', '2', '3'].every((index) => HEX_256.test(String(suppliedPcrs[index] ?? '')));
106
+ if (!suppliedBankValid) errors.push('INVALID_PCR_BANK');
107
+
108
+ let computedMeasurementHash;
109
+ if (declarationsValid) {
110
+ const expectedPcrs = {
111
+ '0': measurePcr(runtimeMeasurement),
112
+ '1': measurePcr(policyMeasurement),
113
+ '2': measurePcr(platformMeasurement),
114
+ '3': extendPcr(null, bundleDigest),
115
+ };
116
+ if (suppliedBankValid) {
117
+ for (const index of ['0', '1', '2', '3']) {
118
+ if (!safeStringEqual(expectedPcrs[index], suppliedPcrs[index])) errors.push(`PCR_${index}_MISMATCH`);
119
+ }
120
+ }
121
+ computedMeasurementHash = `sha256:${sha256Hex({
122
+ pcrs: expectedPcrs,
123
+ runtimeMeasurement,
124
+ policyMeasurement,
125
+ })}`;
126
+ if (!safeStringEqual(computedMeasurementHash, attestation.measurement_hash)) errors.push('MEASUREMENT_HASH_MISMATCH');
127
+ }
128
+
129
+ if (expectedBundleHash) {
130
+ const normalizedExpected = String(expectedBundleHash).replace(/^sha256:/, '').toLowerCase();
131
+ if (!HEX_256.test(normalizedExpected)
132
+ || !safeStringEqual(`sha256:${normalizedExpected}`, attestation.bundle_commitment)) {
133
+ errors.push('BUNDLE_COMMITMENT_MISMATCH');
134
+ }
135
+ }
136
+ if (expectedMeasurementHash
137
+ && !safeStringEqual(expectedMeasurementHash, computedMeasurementHash ?? attestation.measurement_hash)) {
138
+ errors.push('UNEXPECTED_MEASUREMENT');
139
+ }
140
+ const boundary = attestation.claim_boundary;
141
+ if (boundary?.hardware_backed !== false
142
+ || boundary?.pcr_registers_simulated !== true
143
+ || boundary?.hardware_ram_zeroization_proved !== false
144
+ || boundary?.runtime_copies_gc_caches_in_scope !== false) {
145
+ errors.push('INVALID_CLAIM_BOUNDARY');
146
+ }
147
+ return {
148
+ valid: errors.length === 0,
149
+ errors,
150
+ measurement_hash: computedMeasurementHash ?? attestation.measurement_hash,
151
+ bundle_commitment: attestation.bundle_commitment,
152
+ };
153
+ }
154
+
155
+ function safeStringEqual(left, right) {
156
+ const a = Buffer.from(String(left ?? ''));
157
+ const b = Buffer.from(String(right ?? ''));
158
+ return a.length === b.length && timingSafeEqual(a, b);
159
+ }