enigma-memory 0.1.17 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/README.md +85 -27
  2. package/apps/cli/bin/enigma-desktop.mjs +140 -0
  3. package/apps/cli/bin/enigma-terminal.mjs +78 -0
  4. package/apps/cli/bin/enigma.mjs +4979 -3263
  5. package/apps/desktop/electron-main.cjs +217 -0
  6. package/apps/desktop/package.json +12 -0
  7. package/apps/desktop/src/app.js +264 -7
  8. package/apps/desktop/src/index.html +3514 -1373
  9. package/apps/desktop/src/launch-electron.mjs +51 -0
  10. package/apps/desktop/src/server.mjs +2914 -0
  11. package/apps/desktop/src/styles.css +2972 -260
  12. package/apps/desktop/src/zk-browser-prove.mjs +53 -0
  13. package/apps/desktop/src/zk-state.mjs +1789 -0
  14. package/apps/gateway/bin/enigma-gateway.mjs +102 -5
  15. package/apps/gateway/src/server.mjs +271 -8
  16. package/apps/ios/EnigmaCore/Package.swift +12 -0
  17. package/apps/ios/EnigmaCore/Sources/EnigmaCore/EnigmaAPIClient.swift +227 -0
  18. package/apps/ios/EnigmaCore/Sources/EnigmaCore/Models.swift +278 -0
  19. package/apps/ios/EnigmaCore/Sources/EnigmaCore/PKCE.swift +96 -0
  20. package/apps/ios/EnigmaCore/Sources/EnigmaCore/PrivacyMinimizer.swift +187 -0
  21. package/apps/ios/EnigmaCore/Sources/EnigmaCore/ToolModels.swift +129 -0
  22. package/apps/ios/EnigmaCore/Tests/EnigmaCoreTests/EnigmaCoreTests.swift +42 -0
  23. package/apps/ios/EnigmaIOS/Enigma/AppModel.swift +346 -0
  24. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AccentColor.colorset/Contents.json +12 -0
  25. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/Contents.json +11 -0
  26. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/EnigmaAppIcon.png +0 -0
  27. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/Contents.json +3 -0
  28. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/LaunchBackground.colorset/Contents.json +12 -0
  29. package/apps/ios/EnigmaIOS/Enigma/ChatView.swift +181 -0
  30. package/apps/ios/EnigmaIOS/Enigma/CouncilView.swift +78 -0
  31. package/apps/ios/EnigmaIOS/Enigma/CreateView.swift +152 -0
  32. package/apps/ios/EnigmaIOS/Enigma/EnigmaApp.swift +52 -0
  33. package/apps/ios/EnigmaIOS/Enigma/Info.plist +52 -0
  34. package/apps/ios/EnigmaIOS/Enigma/NaturalLanguagePrivacyTagger.swift +26 -0
  35. package/apps/ios/EnigmaIOS/Enigma/OAuthClient.swift +321 -0
  36. package/apps/ios/EnigmaIOS/Enigma/OnboardingView.swift +105 -0
  37. package/apps/ios/EnigmaIOS/Enigma/PrivateVaultView.swift +275 -0
  38. package/apps/ios/EnigmaIOS/Enigma/SecureStore.swift +76 -0
  39. package/apps/ios/EnigmaIOS/Enigma/SettingsView.swift +60 -0
  40. package/apps/ios/EnigmaIOS/Enigma/Theme.swift +80 -0
  41. package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/project.pbxproj +211 -0
  42. package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/xcshareddata/xcschemes/Enigma.xcscheme +23 -0
  43. package/apps/native-host/README.md +19 -8
  44. package/apps/native-host/bin/enigma-native-host.mjs +229 -13
  45. package/apps/relay/bin/enigma-relay.mjs +103 -5
  46. package/apps/relay/src/federation-runtime.mjs +618 -0
  47. package/apps/relay/src/server.mjs +310 -9
  48. package/apps/verifier/bin/enigma-verify.mjs +327 -11
  49. package/cortex-v3/circuits/build/intent_vk_bytes.json +35 -0
  50. package/cortex-v3/circuits/build/sale_vk_bytes.json +35 -0
  51. package/cortex-v3/circuits/build/vk_bytes.json +32 -0
  52. package/cortex-v3/proving-assets.json +64 -0
  53. package/cortex-v3/zk/BUILD-CONTRACT.md +87 -0
  54. package/cortex-v3/zk/action-transition-vk.json +119 -0
  55. package/cortex-v3/zk/alias-adversarial.test.mjs +220 -0
  56. package/cortex-v3/zk/groth16-verify-child.mjs +17 -0
  57. package/cortex-v3/zk/intent-witness.mjs +365 -0
  58. package/cortex-v3/zk/intent-witness.test.mjs +485 -0
  59. package/cortex-v3/zk/proving-assets.mjs +203 -0
  60. package/cortex-v3/zk/sale-witness.mjs +783 -0
  61. package/cortex-v3/zk/sale-witness.test.mjs +784 -0
  62. package/cortex-v3/zk/sealed-sale-release-vk.json +119 -0
  63. package/cortex-v3/zk/settlement-evidence.mjs +722 -0
  64. package/cortex-v3/zk/setup-intent.mjs +688 -0
  65. package/cortex-v3/zk/setup-sale.mjs +666 -0
  66. package/cortex-v3/zk/setup.mjs +594 -0
  67. package/cortex-v3/zk/witness.mjs +184 -0
  68. package/cortex-v3/zk/zk-codec.mjs +232 -0
  69. package/cortex-v3/zk/zk-codec.test.mjs +293 -0
  70. package/cortex-v3/zk/zk-settle.mjs +370 -0
  71. package/cortex-v3/zk/zk-tree.mjs +256 -0
  72. package/cortex-v3/zk/zk-tree.test.mjs +419 -0
  73. package/deploy/SIMULATION.md +14 -9
  74. package/deploy/docker-compose.local-production-simulation.yml +54 -12
  75. package/docs/benchmark-attestation-network.md +487 -487
  76. package/docs/benchmark-reproducibility.md +289 -289
  77. package/docs/blockchain-only-mechanisms.md +400 -400
  78. package/docs/browser-extension-install.md +8 -6
  79. package/docs/client-connectors.md +16 -12
  80. package/docs/demo-proof-network.md +275 -275
  81. package/docs/developer-ecosystem.md +15 -13
  82. package/docs/developer-proof-quickstart.md +325 -325
  83. package/docs/enigma-memory-ready-conformance.md +378 -376
  84. package/docs/install-anywhere.md +64 -30
  85. package/docs/installers-and-desktop.md +8 -7
  86. package/docs/memory-benchmarks.md +1 -1
  87. package/docs/memory-drive-health-model.md +690 -690
  88. package/docs/novelty-invention-candidates.md +161 -161
  89. package/docs/proof-network-build-notes.md +240 -240
  90. package/docs/proof-network-claim-boundaries.md +320 -318
  91. package/docs/proof-network.md +339 -339
  92. package/docs/sdk-api.md +324 -324
  93. package/docs/solana-proof-rail.md +453 -453
  94. package/examples/01-quickstart-agent/index.mjs +49 -0
  95. package/examples/01_agent_memory_quickstart.mjs +57 -0
  96. package/examples/02-multi-agent-swarm/index.mjs +57 -0
  97. package/examples/02_cross_model_passport.mjs +64 -0
  98. package/examples/03-langchain-memory/index.mjs +41 -0
  99. package/examples/03_poseidon_commitment_verification.mjs +71 -0
  100. package/examples/04-python-trading-agent/trader.py +49 -0
  101. package/examples/README.md +27 -0
  102. package/examples/ci/github-actions.yml +7 -2
  103. package/package.json +410 -278
  104. package/packages/adapters/PACKAGE_CONTRACT.md +1 -1
  105. package/packages/connectors/src/index.js +196 -4
  106. package/packages/connectors/swarm-router.mjs +168 -0
  107. package/packages/core/src/index.js +249 -2
  108. package/packages/core/src/version.mjs +7 -0
  109. package/packages/dev-tools/package.json +19 -0
  110. package/packages/dev-tools/src/index.js +4 -0
  111. package/packages/dev-tools/src/memory-benchmark-suite.js +112 -0
  112. package/packages/dev-tools/src/swarm-simulator.js +101 -0
  113. package/packages/dev-tools/src/vault-inspector.js +114 -0
  114. package/packages/dev-tools/src/vector-benchmark.js +100 -0
  115. package/packages/developer-platform/src/access-credentials.js +341 -0
  116. package/packages/developer-platform/src/http.js +132 -0
  117. package/packages/developer-platform/src/index.js +4 -0
  118. package/packages/developer-platform/src/usage-http.js +60 -0
  119. package/packages/developer-platform/src/usage.js +295 -0
  120. package/packages/enclave-runtime/attestation.mjs +159 -0
  121. package/packages/enclave-runtime/index.mjs +47 -0
  122. package/packages/enclave-runtime/session-manager.mjs +253 -0
  123. package/packages/enclave-runtime/zeroization-proof.mjs +227 -0
  124. package/packages/enigma-reflex/package.json +14 -0
  125. package/packages/enigma-reflex/src/index.js +204 -0
  126. package/packages/enigma-reflex/training/generate-dataset.mjs +40 -0
  127. package/packages/enigma-reflex/training/requirements.txt +8 -0
  128. package/packages/enigma-reflex/training/train.py +314 -0
  129. package/packages/enigma-weave/LICENSE +22 -0
  130. package/packages/enigma-weave/UPSTREAM.json +21 -0
  131. package/packages/enigma-weave/package.json +14 -0
  132. package/packages/enigma-weave/src/index.js +286 -0
  133. package/packages/hosted-cloud/src/index.js +80 -5
  134. package/packages/importers/src/index.js +432 -0
  135. package/packages/inference-runtime/src/browser.js +401 -0
  136. package/packages/inference-runtime/src/chat.js +265 -0
  137. package/packages/inference-runtime/src/code.js +407 -0
  138. package/packages/inference-runtime/src/contracts.js +162 -0
  139. package/packages/inference-runtime/src/http.js +232 -0
  140. package/packages/inference-runtime/src/image.js +186 -0
  141. package/packages/inference-runtime/src/index.js +10 -0
  142. package/packages/inference-runtime/src/model-router.js +320 -0
  143. package/packages/inference-runtime/src/platform.js +125 -0
  144. package/packages/inference-runtime/src/privacy.js +400 -0
  145. package/packages/inference-runtime/src/video.js +253 -0
  146. package/packages/mcp-server/README.md +22 -6
  147. package/packages/mcp-server/bin/enigma-mcp.mjs +2 -1
  148. package/packages/mcp-server/src/index.js +2498 -1185
  149. package/packages/mcp-server/src/oauth.js +561 -0
  150. package/packages/mcp-server/src/private-handoff.js +84 -0
  151. package/packages/mcp-server/src/remote-http.js +273 -0
  152. package/packages/mcp-server/src/remote-policy.js +72 -0
  153. package/packages/mcp-server/swarm-bridge.mjs +361 -0
  154. package/packages/mesh/index.d.ts +283 -0
  155. package/packages/mesh/package.json +23 -0
  156. package/packages/mesh/src/crypto.js +189 -0
  157. package/packages/mesh/src/federation-packets.js +353 -0
  158. package/packages/mesh/src/gossip.js +311 -0
  159. package/packages/mesh/src/index.js +6 -0
  160. package/packages/mesh/src/protocol.js +255 -0
  161. package/packages/mesh/src/router.js +279 -0
  162. package/packages/mesh/src/transport.js +306 -0
  163. package/packages/passport/src/index.js +436 -7
  164. package/packages/private-economy/src/credits-http.js +100 -0
  165. package/packages/private-economy/src/credits.js +447 -0
  166. package/packages/private-economy/src/index.js +5 -0
  167. package/packages/private-economy/src/payments-http.js +120 -0
  168. package/packages/private-economy/src/payments.js +509 -0
  169. package/packages/private-economy/src/x402.js +346 -0
  170. package/packages/proof-network/PACKAGE_CONTRACT.md +21 -0
  171. package/packages/rag/index.d.ts +182 -0
  172. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/THIRD_PARTY_LICENSES.txt +207 -0
  173. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/config.json +25 -0
  174. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/onnx/model_quantized.onnx +0 -0
  175. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/sha256-manifest.json +28 -0
  176. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer.json +30686 -0
  177. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer_config.json +15 -0
  178. package/packages/rag/package.json +27 -0
  179. package/packages/rag/src/blinded-search.js +109 -0
  180. package/packages/rag/src/bm25.js +169 -0
  181. package/packages/rag/src/embeddings.js +459 -0
  182. package/packages/rag/src/hybrid.js +76 -0
  183. package/packages/rag/src/index.js +38 -0
  184. package/packages/rag/src/reranker.js +61 -0
  185. package/packages/rag/src/research.js +107 -0
  186. package/packages/rag/src/vector-store.js +430 -0
  187. package/packages/rag/src/verify-model-artifacts.mjs +4 -0
  188. package/packages/sdk/index.d.ts +760 -0
  189. package/packages/sdk/package.json +33 -0
  190. package/packages/sdk/python/README.md +24 -0
  191. package/packages/sdk/python/enigma_sdk.py +250 -0
  192. package/packages/sdk/python/pyproject.toml +34 -0
  193. package/packages/sdk/python/requirements.txt +1 -0
  194. package/packages/sdk/python/setup.py +20 -0
  195. package/packages/sdk/src/federation/capability-grant.js +389 -0
  196. package/packages/sdk/src/federation/federation-router.js +360 -0
  197. package/packages/sdk/src/federation/ghostmesh-bridge.js +497 -0
  198. package/packages/sdk/src/federation/index.js +3 -0
  199. package/packages/sdk/src/index.js +1796 -0
  200. package/packages/sdk/src/intelligence/contradiction.js +337 -0
  201. package/packages/sdk/src/intelligence/decision-engine.js +155 -0
  202. package/packages/sdk/src/intelligence/index.js +4 -0
  203. package/packages/sdk/src/intelligence/ontology.js +122 -0
  204. package/packages/sdk/src/intelligence/temporal.js +123 -0
  205. package/packages/sdk/src/market-client.js +142 -0
  206. package/packages/sdk/src/mesh-client.js +110 -0
  207. package/packages/sdk/src/middleware/index.js +3 -0
  208. package/packages/sdk/src/middleware/langchain.js +159 -0
  209. package/packages/sdk/src/middleware/llamaindex.js +101 -0
  210. package/packages/sdk/src/middleware/vercel-ai.js +112 -0
  211. package/packages/sdk/src/rag-client.js +85 -0
  212. package/packages/sdk/src/swarm-orchestrator.js +260 -0
  213. package/packages/settlement/PACKAGE_CONTRACT.md +1 -1
  214. package/packages/snapcompact/THIRD_PARTY_LICENSES.txt +40 -0
  215. package/packages/snapcompact/assets/8x13-latin1.bdf +3837 -0
  216. package/packages/snapcompact/index.d.ts +284 -0
  217. package/packages/snapcompact/package.json +25 -0
  218. package/packages/snapcompact/src/index.js +716 -0
  219. package/packages/storage/PACKAGE_CONTRACT.md +1 -1
  220. package/packages/terminal-console/animations.mjs +240 -0
  221. package/packages/terminal-console/auto-anchor.mjs +220 -0
  222. package/packages/terminal-console/banner.mjs +91 -0
  223. package/packages/terminal-console/commands.mjs +459 -0
  224. package/packages/terminal-console/delegation.mjs +152 -0
  225. package/packages/terminal-console/index.mjs +5 -0
  226. package/packages/terminal-console/outbox.mjs +143 -0
  227. package/packages/terminal-console/phantom-bridge.mjs +637 -0
  228. package/packages/terminal-console/repl.mjs +136 -0
  229. package/packages/terminal-console/signer-store.mjs +130 -0
  230. package/packages/terminal-console/solana-rpc.mjs +214 -0
  231. package/packages/terminal-console/solana-transport.mjs +189 -0
  232. package/packages/terminal-tui/dashboard.mjs +214 -0
  233. package/packages/terminal-tui/index.mjs +28 -0
  234. package/packages/terminal-tui/merkle-tree-renderer.mjs +268 -0
  235. package/packages/terminal-tui/telemetry-hud.mjs +137 -0
  236. package/packages/vault/index.d.ts +449 -0
  237. package/packages/vault/package.json +27 -0
  238. package/packages/vault/src/e2ee.mjs +393 -0
  239. package/packages/vault/src/enclave.js +481 -0
  240. package/packages/vault/src/erasure.js +207 -0
  241. package/packages/vault/src/index.js +1150 -125
  242. package/packages/vault/src/persistence.js +307 -0
  243. package/packages/vault/src/poseidon.js +354 -0
  244. package/packages/vault/src/receipt.js +459 -0
  245. package/scripts/benchmark-optical-context.mjs +166 -0
  246. package/scripts/bootstrap-enigma.mjs +502 -0
  247. package/scripts/build-edge-backend-workers.mjs +20 -5
  248. package/scripts/build-goal-completion-audit.mjs +72 -25
  249. package/scripts/build-hosted-api-key-lifecycle.mjs +292 -274
  250. package/scripts/build-hosted-customer-lifecycle.mjs +493 -476
  251. package/scripts/build-hosted-probe-worker.mjs +19 -4
  252. package/scripts/build-installer-assets.mjs +409 -389
  253. package/scripts/build-operator-evidence-starter.mjs +59 -1
  254. package/scripts/build-production-backend-env-kit.mjs +2 -0
  255. package/scripts/build-production-unblocker.mjs +4 -1
  256. package/scripts/build-proof-network-packet.mjs +213 -213
  257. package/scripts/check.mjs +25 -4
  258. package/scripts/collect-hosted-backend-live-evidence.mjs +49 -12
  259. package/scripts/install-enigma-local.mjs +18 -5
  260. package/scripts/release-audit.mjs +74 -115
  261. package/scripts/release-provenance.mjs +12 -2
  262. package/scripts/run-backend-readiness-smoke.mjs +112 -10
  263. package/scripts/run-standard-memory-benchmarks.mjs +1354 -1352
  264. package/scripts/scan-secrets.mjs +178 -0
  265. package/scripts/simulate-production-env.mjs +71 -10
  266. package/scripts/validate-hosted-backend-live.mjs +112 -1
  267. package/specs/antibody-pack-v1.schema.json +95 -0
  268. package/specs/antigen-envelope-v1.schema.json +81 -0
  269. package/specs/boundary-manifest-v1.schema.json +35 -35
  270. package/specs/capsule-v1.schema.json +55 -55
  271. package/specs/claim-boundary-manifest-v1.schema.json +22 -22
  272. package/specs/claim-ledger-v1.schema.json +291 -0
  273. package/specs/context-passport-v1.schema.json +59 -0
  274. package/specs/deletion-tombstone-v1.schema.json +26 -26
  275. package/specs/evidence-packet-v1.schema.json +177 -0
  276. package/specs/hosted-backend-live-evidence-v1.schema.json +72 -3
  277. package/specs/immune-scan-report-v1.schema.json +112 -0
  278. package/specs/lifecycle-receipt-log-v1.schema.json +67 -0
  279. package/specs/memory-atom-v1.schema.json +59 -0
  280. package/specs/memory-event-v1.schema.json +42 -42
  281. package/specs/passport-v1.schema.json +50 -50
  282. package/specs/proof-of-non-use-v1.schema.json +65 -0
  283. package/specs/quarantine-record-v1.schema.json +126 -0
  284. package/specs/receipt-v1.schema.json +61 -61
  285. package/specs/state-checkpoint-v1.schema.json +37 -37
  286. package/specs/trust-bundle-v1.schema.json +56 -56
  287. package/specs/trust-card-v1.schema.json +119 -0
  288. package/docs/proof-network-launch-plan.md +0 -421
  289. package/packages/metering/PACKAGE_CONTRACT.md +0 -20
  290. package/scripts/build-ai-orchestration-plan.mjs +0 -248
@@ -0,0 +1,400 @@
1
+ import { createHmac, randomBytes } from 'node:crypto';
2
+
3
+ export const ENIGMA_PRIVACY_RECEIPT_SCHEMA = 'enigma.smart_privacy_receipt.v1';
4
+ const MODES = new Set(['off', 'smart', 'full']);
5
+ const ACTIONS = new Set(['removed', 'generalized', 'dropped', 'masked', 'preserved', 'preserved_approximate', 'kept', 'kept_approximate']);
6
+ const referenceKey = randomBytes(32);
7
+ const DETERMINISTIC_DETECTORS = Object.freeze([
8
+ ['email', /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi],
9
+ ['evm_wallet', /\b0x[a-fA-F0-9]{40}\b/g],
10
+ ['solana_wallet', /\b[1-9A-HJ-NP-Za-km-z]{32,44}\b/g],
11
+ ['ipv4', /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, (value) => value.split('.').every((part) => Number(part) <= 255)],
12
+ ['phone', /(?:\+\d{1,3}[\s.-]?)?(?:\(\d{2,4}\)|\d{2,4})[\s.-]\d{3,4}[\s.-]\d{4}\b/g],
13
+ ['credit_card', /\b(?:\d[ -]*?){13,19}\b/g, luhn],
14
+ ['api_key', /\b(?:sk|pk|api|token|key)[_-](?:live|test)?[_-]?[A-Za-z0-9]{16,}\b/gi],
15
+ ['private_key', /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g],
16
+ ['gps', /(?<!\d)(?:-?(?:[1-8]?\d(?:\.\d+)?|90(?:\.0+)?))\s*[,/]\s*(?:-?(?:1[0-7]\d(?:\.\d+)?|\d?\d(?:\.\d+)?|180(?:\.0+)?))(?!\d)/g],
17
+ ]);
18
+
19
+ function luhn(value) {
20
+ const digits = value.replace(/\D/g, '');
21
+ if (digits.length < 13 || digits.length > 19 || /^(\d)\1+$/.test(digits)) return false;
22
+ let sum = 0;
23
+ let double = false;
24
+ for (let index = digits.length - 1; index >= 0; index -= 1) {
25
+ let digit = Number(digits[index]);
26
+ if (double) { digit *= 2; if (digit > 9) digit -= 9; }
27
+ sum += digit;
28
+ double = !double;
29
+ }
30
+ return sum % 10 === 0;
31
+ }
32
+
33
+ function reference(value) {
34
+ return createHmac('sha256', referenceKey).update(String(value)).digest('hex');
35
+ }
36
+
37
+ function normalizeMessages(value) {
38
+ if (!Array.isArray(value) || !value.length) throw new TypeError('messages must be a non-empty array');
39
+ return value.map((message, index) => {
40
+ if (!message || typeof message !== 'object') throw new TypeError(`messages[${index}] must be an object`);
41
+ const role = String(message.role || '');
42
+ if (!['system', 'user', 'assistant', 'tool'].includes(role)) throw new TypeError(`messages[${index}].role is invalid`);
43
+ if (typeof message.content !== 'string') throw new TypeError(`messages[${index}].content must be text for privacy minimization`);
44
+ return { ...message, role, content: message.content };
45
+ });
46
+ }
47
+
48
+ function deterministicSpans(text) {
49
+ const spans = [];
50
+ for (const [type, expression, valid] of DETERMINISTIC_DETECTORS) {
51
+ expression.lastIndex = 0;
52
+ let match;
53
+ while ((match = expression.exec(text))) {
54
+ if ((!valid || valid(match[0])) && match[0]) spans.push({ start: match.index, end: match.index + match[0].length, type, confidence: 1, action: 'masked', source: 'deterministic' });
55
+ if (expression.lastIndex === match.index) expression.lastIndex += 1;
56
+ }
57
+ }
58
+ return spans;
59
+ }
60
+
61
+ function normalizeSemanticSpans(value, length) {
62
+ if (!Array.isArray(value)) throw new Error('semantic privacy classifier must return a spans array');
63
+ return value.map((span, index) => {
64
+ if (!span || typeof span !== 'object') throw new TypeError(`privacy span ${index} is invalid`);
65
+ const start = Number(span.start);
66
+ const end = Number(span.end);
67
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start || end > length) {
68
+ throw new TypeError(`privacy span ${index} is out of bounds`);
69
+ }
70
+ const type = String(span.type || 'personal_detail').toLowerCase().replace(/[^a-z0-9_]/g, '_').slice(0, 40);
71
+ const confidence = Number.isFinite(span.confidence) ? Math.max(0, Math.min(1, span.confidence)) : 0.5;
72
+ const action = ACTIONS.has(span.action) ? span.action : 'masked';
73
+ return { start, end, type: type || 'personal_detail', confidence, action, source: 'semantic', generalized: span.generalized ? String(span.generalized).slice(0, 120) : null };
74
+ });
75
+ }
76
+
77
+ function mergeSpans(spans) {
78
+ const ordered = [...spans].sort((a, b) => a.start - b.start || b.end - a.end || b.confidence - a.confidence);
79
+ const result = [];
80
+ for (const span of ordered) {
81
+ const overlap = result.find((current) => span.start < current.end && span.end > current.start);
82
+ if (!overlap) result.push(span);
83
+ else if (span.source === 'deterministic' && overlap.source !== 'deterministic') Object.assign(overlap, span);
84
+ }
85
+ return result.sort((a, b) => a.start - b.start);
86
+ }
87
+
88
+ function privacySegmentPairs(original, minimized, replacements) {
89
+ if (!replacements.length) return { youWrote: [['kept', original]], modelSaw: [['kept', minimized]] };
90
+ const youWrote = [];
91
+ const modelSaw = [];
92
+ let cursor = 0;
93
+ for (const replacement of replacements) {
94
+ if (replacement.start > cursor) {
95
+ const kept = original.slice(cursor, replacement.start);
96
+ youWrote.push(['kept', kept]);
97
+ modelSaw.push(['kept', kept]);
98
+ }
99
+ youWrote.push([replacement.type, original.slice(replacement.start, replacement.end)]);
100
+ modelSaw.push([replacement.type, replacement.replacement]);
101
+ cursor = replacement.end;
102
+ }
103
+ if (cursor < original.length) {
104
+ youWrote.push(['kept', original.slice(cursor)]);
105
+ modelSaw.push(['kept', original.slice(cursor)]);
106
+ }
107
+ return { youWrote, modelSaw };
108
+ }
109
+ function privacyChangeProof(original, replacements) {
110
+ return replacements.map((replacement) => {
111
+ const raw = original.slice(replacement.start, replacement.end);
112
+ return {
113
+ type: replacement.type,
114
+ action: replacement.action,
115
+ originalRef: reference(raw),
116
+ originalLength: raw.length,
117
+ replacementRef: reference(replacement.replacement),
118
+ replacementLength: replacement.replacement.length,
119
+ };
120
+ });
121
+ }
122
+
123
+
124
+ export function createSemanticPrivacyEngine(options = {}) {
125
+ const classifier = options.classifier || null;
126
+ if (classifier && classifier.local !== true) {
127
+ throw new TypeError('Smart Privacy classifier must execute locally inside the Enigma trust boundary');
128
+ }
129
+ const minimumConfidence = Number.isFinite(options.minimumConfidence) ? options.minimumConfidence : 0.72;
130
+ const classifierTimeoutMs = Number.isInteger(options.classifierTimeoutMs) ? options.classifierTimeoutMs : 8_000;
131
+ const externalSensitiveTypes = new Set(options.externalSensitiveTypes || [
132
+ 'email', 'evm_wallet', 'solana_wallet', 'ipv4', 'phone', 'credit_card', 'api_key', 'private_key', 'gps',
133
+ 'person', 'name', 'location', 'address', 'organization', 'account', 'medical', 'legal_matter',
134
+ ]);
135
+
136
+ async function classify(text, context) {
137
+ if (!classifier) throw new Error('semantic classifier is not configured');
138
+ const controller = new AbortController();
139
+ const timer = setTimeout(() => controller.abort(new Error('semantic privacy classifier timed out')), classifierTimeoutMs);
140
+ try {
141
+ const result = await classifier.classify(text, { ...context, signal: controller.signal });
142
+ return normalizeSemanticSpans(result?.spans ?? result, text.length);
143
+ } finally {
144
+ clearTimeout(timer);
145
+ }
146
+ }
147
+
148
+ return Object.freeze({
149
+ async minimize(messagesInput, input = {}) {
150
+ const mode = String(input.mode || 'smart').toLowerCase();
151
+ if (!MODES.has(mode)) throw new TypeError('privacy mode must be off, smart, or full');
152
+ const messages = normalizeMessages(messagesInput);
153
+ if (mode === 'off') {
154
+ let disposed = false;
155
+ const privateState = Object.freeze({
156
+ restore(value) { if (disposed) throw new Error('privacy state is disposed'); return String(value ?? ''); },
157
+ restoreChunk(value) { if (disposed) throw new Error('privacy state is disposed'); return String(value ?? ''); },
158
+ preview() { if (disposed) throw new Error('privacy state is disposed'); return []; },
159
+ dispose() { disposed = true; },
160
+ get disposed() { return disposed; },
161
+ });
162
+ return {
163
+ mode,
164
+ messages,
165
+ privateState,
166
+ receipt: { schema: ENIGMA_PRIVACY_RECEIPT_SCHEMA, mode, status: 'ready', summary: [], proof: [], promptRef: reference(messages.map((m) => m.content).join('\n')), rawValuesRetained: false },
167
+ safeForExternal: true,
168
+ };
169
+ }
170
+ const tokenByValue = new Map();
171
+ const valueByToken = new Map();
172
+ const typeCounts = new Map();
173
+ const summaries = [];
174
+ const proofs = [];
175
+ const previews = [];
176
+ const minimizedMessages = [];
177
+
178
+ for (let messageIndex = 0; messageIndex < messages.length; messageIndex += 1) {
179
+ const message = messages[messageIndex];
180
+ const deterministic = deterministicSpans(message.content);
181
+ let semantic;
182
+ try {
183
+ semantic = await classify(message.content, { mode, role: message.role, messageIndex });
184
+ } catch (error) {
185
+ if (options.failOpen === true) semantic = [];
186
+ else {
187
+ const privacyError = new Error(`semantic privacy minimization failed closed: ${error.message}`);
188
+ privacyError.code = 'privacy_unavailable';
189
+ throw privacyError;
190
+ }
191
+ }
192
+ const candidates = mergeSpans([...deterministic, ...semantic.filter((span) => span.confidence >= minimumConfidence)]);
193
+ const selected = candidates.filter((span) => {
194
+ if (span.action === 'preserved' || span.action === 'kept') return false;
195
+ if (mode === 'full') return true;
196
+ return externalSensitiveTypes.has(span.type) || span.source === 'deterministic';
197
+ });
198
+ let output = '';
199
+ let cursor = 0;
200
+ const replacements = [];
201
+ for (const span of selected) {
202
+ const raw = message.content.slice(span.start, span.end);
203
+ output += message.content.slice(cursor, span.start);
204
+ let replacement;
205
+ let action = span.action;
206
+ if ((span.action === 'generalized' || span.action === 'kept_approximate' || span.action === 'preserved_approximate') && span.generalized) {
207
+ replacement = span.generalized;
208
+ action = 'generalized';
209
+ } else {
210
+ replacement = tokenByValue.get(raw);
211
+ if (!replacement) {
212
+ const count = (typeCounts.get(span.type) || 0) + 1;
213
+ typeCounts.set(span.type, count);
214
+ replacement = `⟦${span.type}_${count}⟧`;
215
+ tokenByValue.set(raw, replacement);
216
+ valueByToken.set(replacement, raw);
217
+ }
218
+ action = span.action === 'dropped' || span.action === 'removed' ? 'removed' : 'masked';
219
+ }
220
+ output += replacement;
221
+ replacements.push({ ...span, replacement, action });
222
+ summaries.push({ type: span.type, action, token: replacement, reason: span.source === 'deterministic' ? 'high-confidence identifier' : 'local semantic classification' });
223
+ cursor = span.end;
224
+ }
225
+ output += message.content.slice(cursor);
226
+ minimizedMessages.push({ ...message, content: output });
227
+ proofs.push({ messageIndex, changes: privacyChangeProof(message.content, replacements) });
228
+ previews.push({ messageIndex, ...privacySegmentPairs(message.content, output, replacements) });
229
+ }
230
+
231
+ let disposed = false;
232
+ let streamBuffer = '';
233
+ const privateState = Object.freeze({
234
+ restore(value) {
235
+ if (disposed) throw new Error('privacy state is disposed');
236
+ let restored = String(value ?? '');
237
+ for (const [token, original] of valueByToken) restored = restored.split(token).join(original);
238
+ return restored;
239
+ },
240
+ restoreChunk(value, input = {}) {
241
+ if (disposed) throw new Error('privacy state is disposed');
242
+ streamBuffer += String(value ?? '');
243
+ let boundary = streamBuffer.length;
244
+ if (input.final !== true) {
245
+ const open = streamBuffer.lastIndexOf('⟦');
246
+ const close = streamBuffer.lastIndexOf('⟧');
247
+ if (open > close) boundary = open;
248
+ }
249
+ const ready = streamBuffer.slice(0, boundary);
250
+ streamBuffer = streamBuffer.slice(boundary);
251
+ let restored = ready;
252
+ for (const [token, original] of valueByToken) restored = restored.split(token).join(original);
253
+ return restored;
254
+ },
255
+ preview() {
256
+ if (disposed) throw new Error('privacy state is disposed');
257
+ return structuredClone(previews);
258
+ },
259
+ dispose() {
260
+ tokenByValue.clear();
261
+ valueByToken.clear();
262
+ typeCounts.clear();
263
+ previews.length = 0;
264
+ streamBuffer = '';
265
+ disposed = true;
266
+ },
267
+ get disposed() { return disposed; },
268
+ });
269
+ const unresolved = minimizedMessages.some((message) => deterministicSpans(message.content).length > 0);
270
+ const receipt = {
271
+ schema: ENIGMA_PRIVACY_RECEIPT_SCHEMA,
272
+ mode,
273
+ status: 'ready',
274
+ classifier: classifier.id || 'semantic-local',
275
+ summary: summaries.slice(0, 64),
276
+ proof: proofs,
277
+ promptRef: reference(messages.map((message) => message.content).join('\n')),
278
+ rawValuesRetained: false,
279
+ };
280
+ return { mode, messages: minimizedMessages, privateState, receipt, safeForExternal: !unresolved };
281
+ },
282
+ });
283
+ }
284
+
285
+ export function createClassifierAdapter(options = {}) {
286
+ if (typeof options.classify !== 'function') throw new TypeError('classifier adapter requires classify()');
287
+ return Object.freeze({ id: String(options.id || 'local-semantic-classifier'), local: options.local !== false, classify: options.classify });
288
+ }
289
+
290
+ export function createLocalEmbeddingSemanticClassifier(options = {}) {
291
+ const id = String(options.id || 'enigma-local-minilm-privacy');
292
+ const similarityThreshold = Number.isFinite(options.similarityThreshold) ? options.similarityThreshold : 0.46;
293
+ let providerPromise = null;
294
+ let prototypePromise = null;
295
+ let disposed = false;
296
+ const prototypes = Object.freeze({
297
+ person: 'a private person real name identity individual full name',
298
+ location: 'a private home address city location residence where someone lives',
299
+ organization: 'a private employer company workplace school institution organization',
300
+ medical: 'a private medical diagnosis health condition treatment patient information',
301
+ legal_matter: 'a confidential legal case lawsuit docket investigation legal matter',
302
+ account: 'a private account identifier username customer number membership',
303
+ });
304
+
305
+ async function provider() {
306
+ if (disposed) throw new Error('local privacy classifier is disposed');
307
+ if (!providerPromise) {
308
+ providerPromise = import('../../rag/src/embeddings.js').then(({ createEmbeddingProvider }) => createEmbeddingProvider({ modelType: 'neural' }));
309
+ }
310
+ return providerPromise;
311
+ }
312
+
313
+ async function embeddedPrototypes() {
314
+ if (!prototypePromise) {
315
+ prototypePromise = (async () => {
316
+ const embedding = await provider();
317
+ const entries = await Promise.all(Object.entries(prototypes).map(async ([type, text]) => [type, await embedding.embed(text)]));
318
+ return entries;
319
+ })();
320
+ }
321
+ return prototypePromise;
322
+ }
323
+
324
+ function directCandidates(text) {
325
+ const patterns = [
326
+ ['person', /\b(?:[Mm]y name is|[Cc]all me|[Ii] am)\s+([A-Z][\p{L}'-]+(?:\s+[A-Z][\p{L}'-]+){0,3})/gu],
327
+ ['location', /\b(?:i live in|my address is|located in|based in)\s+([^\n,.;]{2,100})/giu],
328
+ ['organization', /\b(?:i work at|employed by|my employer is|i study at)\s+([^\n,.;]{2,100})/giu],
329
+ ['medical', /\b(?:diagnosed with|my diagnosis is|my medical condition is)\s+([^\n,.;]{2,140})/giu],
330
+ ['legal_matter', /\b(?:my case|our lawsuit|docket number|legal matter)\s+(?:is\s+)?([^\n,.;]{2,140})/giu],
331
+ ['account', /\b(?:my username is|account number|customer id|member id)\s*(?:is|:)?\s*([^\s,.;]{2,100})/giu],
332
+ ];
333
+ const spans = [];
334
+ for (const [type, expression] of patterns) {
335
+ expression.lastIndex = 0;
336
+ let match;
337
+ while ((match = expression.exec(text))) {
338
+ const value = match[1];
339
+ const start = match.index + match[0].lastIndexOf(value);
340
+ spans.push({ start, end: start + value.length, type, confidence: 0.99, action: type === 'location' ? 'generalized' : 'masked', generalized: type === 'location' ? 'a private location' : null });
341
+ }
342
+ }
343
+ return spans;
344
+ }
345
+
346
+ function phraseCandidates(text) {
347
+ const common = new Set(['The', 'This', 'That', 'When', 'Where', 'What', 'Please', 'Enigma', 'I']);
348
+ const spans = [];
349
+ const expression = /\b[A-Z][\p{L}'-]{1,}(?:\s+[A-Z][\p{L}'-]{1,}){0,3}\b/gu;
350
+ let match;
351
+ while ((match = expression.exec(text))) {
352
+ if (!common.has(match[0]) && match[0].length <= 100) spans.push({ start: match.index, end: match.index + match[0].length, text: match[0] });
353
+ }
354
+ return spans.slice(0, 24);
355
+ }
356
+
357
+ return Object.freeze({
358
+ id,
359
+ local: true,
360
+ async classify(text) {
361
+ if (typeof text !== 'string') throw new TypeError('privacy classifier text must be a string');
362
+ const direct = directCandidates(text);
363
+ const covered = (candidate) => direct.some((span) => candidate.start < span.end && candidate.end > span.start);
364
+ const phrases = phraseCandidates(text).filter((candidate) => !covered(candidate));
365
+ if (!phrases.length) return { spans: direct };
366
+ const [{ cosineSimilarity }, embedding, prototypeEntries] = await Promise.all([
367
+ import('../../rag/src/embeddings.js'),
368
+ provider(),
369
+ embeddedPrototypes(),
370
+ ]);
371
+ for (const phrase of phrases) {
372
+ const contextStart = Math.max(0, phrase.start - 80);
373
+ const contextEnd = Math.min(text.length, phrase.end + 80);
374
+ const vector = await embedding.embed(`${phrase.text}. Context: ${text.slice(contextStart, contextEnd)}`);
375
+ let best = { type: null, score: -1 };
376
+ for (const [type, prototype] of prototypeEntries) {
377
+ const score = cosineSimilarity(vector, prototype);
378
+ if (score > best.score) best = { type, score };
379
+ }
380
+ if (best.score >= similarityThreshold) {
381
+ direct.push({
382
+ start: phrase.start,
383
+ end: phrase.end,
384
+ type: best.type,
385
+ confidence: Math.min(0.95, best.score),
386
+ action: best.type === 'location' ? 'generalized' : 'masked',
387
+ generalized: best.type === 'location' ? 'a private location' : null,
388
+ });
389
+ }
390
+ }
391
+ return { spans: direct };
392
+ },
393
+ async dispose() {
394
+ disposed = true;
395
+ if (providerPromise) await (await providerPromise).dispose();
396
+ providerPromise = null;
397
+ prototypePromise = null;
398
+ },
399
+ });
400
+ }
@@ -0,0 +1,253 @@
1
+ import { createHmac, randomBytes, randomUUID } from 'node:crypto';
2
+
3
+ export const ENIGMA_VIDEO_JOB_SCHEMA = 'enigma.video_job.v1';
4
+ export const ENIGMA_VIDEO_RESULT_SCHEMA = 'enigma.video_result.v1';
5
+
6
+ const TERMINAL = new Set(['succeeded', 'failed', 'cancelled', 'expired']);
7
+ const STATUS = new Set(['queued', 'running', ...TERMINAL]);
8
+ const runtimeReferenceKey = randomBytes(32);
9
+
10
+ function requiredString(value, field, maxLength = 16_000) {
11
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${field} must be a non-empty string`);
12
+ const normalized = value.trim();
13
+ if (normalized.length > maxLength) throw new TypeError(`${field} exceeds ${maxLength} characters`);
14
+ return normalized;
15
+ }
16
+
17
+ function integer(value, field, minimum, maximum, fallback) {
18
+ const candidate = value === undefined ? fallback : value;
19
+ if (!Number.isInteger(candidate) || candidate < minimum || candidate > maximum) {
20
+ throw new TypeError(`${field} must be an integer from ${minimum} to ${maximum}`);
21
+ }
22
+ return candidate;
23
+ }
24
+
25
+ function reference(value) {
26
+ return createHmac('sha256', runtimeReferenceKey).update(value).digest('hex');
27
+ }
28
+
29
+ function normalizeSourceImage(value) {
30
+ if (value === undefined || value === null || value === '') return null;
31
+ const source = requiredString(value, 'sourceImage', 16 * 1024 * 1024);
32
+ if (source.startsWith('data:image/')) {
33
+ if (!/^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(source)) {
34
+ throw new TypeError('sourceImage data URL must be base64 PNG, JPEG, or WebP');
35
+ }
36
+ return source;
37
+ }
38
+ const url = new URL(source);
39
+ if (url.protocol !== 'https:') throw new TypeError('sourceImage URL must use HTTPS');
40
+ return url.toString();
41
+ }
42
+
43
+ export function normalizeVideoRequest(input = {}) {
44
+ const sourceImage = normalizeSourceImage(input.sourceImage ?? input.source_image);
45
+ return {
46
+ model: requiredString(input.model, 'model', 160),
47
+ prompt: requiredString(input.prompt, 'prompt'),
48
+ kind: sourceImage ? 'image-to-video' : 'text-to-video',
49
+ sourceImage,
50
+ duration: integer(input.duration, 'duration', 1, 30, 5),
51
+ resolution: requiredString(input.resolution || '720p', 'resolution', 32),
52
+ aspectRatio: requiredString(input.aspectRatio ?? input.aspect_ratio ?? '16:9', 'aspectRatio', 16),
53
+ audio: input.audio === true,
54
+ };
55
+ }
56
+
57
+ function normalizeProviderStatus(value) {
58
+ const aliases = { pending: 'queued', processing: 'running', complete: 'succeeded', completed: 'succeeded', error: 'failed' };
59
+ const status = aliases[value] || value;
60
+ if (!STATUS.has(status)) throw new Error(`video provider returned unsupported status: ${value}`);
61
+ return status;
62
+ }
63
+
64
+ function normalizeVideoAsset(value) {
65
+ if (!value || typeof value !== 'object') throw new Error('video provider returned no asset');
66
+ if (typeof value.url === 'string') {
67
+ const url = new URL(value.url);
68
+ if (url.protocol !== 'https:' && url.protocol !== 'data:') throw new Error('video asset URL must use HTTPS or data:');
69
+ return { url: url.toString(), mimeType: value.mimeType || 'video/mp4' };
70
+ }
71
+ if (typeof value.base64 === 'string') {
72
+ if (value.base64.length > 192 * 1024 * 1024) throw new Error('video asset exceeds the 144 MiB decoded limit');
73
+ return { base64: value.base64, mimeType: value.mimeType || 'video/mp4' };
74
+ }
75
+ throw new Error('video asset requires url or base64');
76
+ }
77
+
78
+ export function createHttpVideoProvider(options = {}) {
79
+ const id = requiredString(options.id || 'http-video', 'provider id', 160);
80
+ const baseUrl = new URL(options.baseUrl);
81
+ const apiKey = requiredString(options.apiKey, 'apiKey', 4_096);
82
+ const models = [...new Set((options.models || []).map((model) => requiredString(model, 'model', 160)))];
83
+ if (!models.length) throw new TypeError('HTTP video provider requires models');
84
+ const queuePath = options.queuePath || 'videos/queue';
85
+ const retrievePath = options.retrievePath || 'videos/retrieve';
86
+ return Object.freeze({
87
+ id,
88
+ models: Object.freeze(models),
89
+ async queue(request, context = {}) {
90
+ const response = await fetch(new URL(queuePath, baseUrl), {
91
+ method: 'POST',
92
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', ...(options.headers || {}) },
93
+ body: JSON.stringify({
94
+ model: request.model,
95
+ prompt: request.prompt,
96
+ duration: request.duration,
97
+ resolution: request.resolution,
98
+ aspect_ratio: request.aspectRatio,
99
+ audio: request.audio,
100
+ ...(request.sourceImage ? { source_image: request.sourceImage } : {}),
101
+ }),
102
+ signal: context.signal,
103
+ });
104
+ const payload = await response.json();
105
+ if (!response.ok) throw new Error(`video provider ${id} queue failed with HTTP ${response.status}`);
106
+ const providerJobId = payload.id ?? payload.job_id ?? payload.jobId;
107
+ return { providerJobId: requiredString(providerJobId, 'providerJobId', 512), status: normalizeProviderStatus(payload.status || 'queued') };
108
+ },
109
+ async retrieve(providerJobId, context = {}) {
110
+ const response = await fetch(new URL(retrievePath, baseUrl), {
111
+ method: 'POST',
112
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', ...(options.headers || {}) },
113
+ body: JSON.stringify({ id: providerJobId }),
114
+ signal: context.signal,
115
+ });
116
+ const contentType = response.headers.get('content-type') || '';
117
+ if (response.ok && contentType.startsWith('video/')) {
118
+ return { status: 'succeeded', asset: { base64: Buffer.from(await response.arrayBuffer()).toString('base64'), mimeType: contentType.split(';')[0] } };
119
+ }
120
+ const payload = await response.json();
121
+ if (!response.ok) throw new Error(`video provider ${id} retrieve failed with HTTP ${response.status}`);
122
+ const asset = payload.asset || (payload.url ? { url: payload.url, mimeType: payload.mime_type } : null);
123
+ return {
124
+ status: normalizeProviderStatus(payload.status || (asset ? 'succeeded' : 'running')),
125
+ progress: Number.isFinite(payload.progress) ? Math.max(0, Math.min(100, payload.progress)) : null,
126
+ asset,
127
+ error: payload.error || null,
128
+ providerRequestId: response.headers.get('x-request-id') || null,
129
+ };
130
+ },
131
+ async cancel(providerJobId, context = {}) {
132
+ if (!options.cancelPath) return false;
133
+ const response = await fetch(new URL(options.cancelPath.replace(':id', encodeURIComponent(providerJobId)), baseUrl), {
134
+ method: 'POST',
135
+ headers: { Authorization: `Bearer ${apiKey}`, ...(options.headers || {}) },
136
+ signal: context.signal,
137
+ });
138
+ return response.ok;
139
+ },
140
+ });
141
+ }
142
+
143
+ export function createVideoBackend(options = {}) {
144
+ const providers = new Map();
145
+ const modelRoutes = new Map();
146
+ for (const candidate of options.providers || []) {
147
+ if (!candidate || typeof candidate.queue !== 'function' || typeof candidate.retrieve !== 'function') {
148
+ throw new TypeError('video provider requires queue() and retrieve()');
149
+ }
150
+ const id = requiredString(candidate.id, 'provider id', 160);
151
+ if (providers.has(id)) throw new TypeError(`duplicate video provider: ${id}`);
152
+ const models = [...new Set((candidate.models || []).map((model) => requiredString(model, 'model', 160)))];
153
+ if (!models.length) throw new TypeError(`video provider ${id} requires models`);
154
+ const provider = Object.freeze({ ...candidate, id, models: Object.freeze(models) });
155
+ providers.set(id, provider);
156
+ for (const model of models) {
157
+ if (modelRoutes.has(model)) throw new TypeError(`video model has multiple providers: ${model}`);
158
+ modelRoutes.set(model, provider);
159
+ }
160
+ }
161
+ const ttlMs = integer(options.ttlMs, 'ttlMs', 60_000, 24 * 60 * 60_000, 60 * 60_000);
162
+ const timeoutMs = integer(options.timeoutMs, 'timeoutMs', 1_000, 15 * 60_000, 120_000);
163
+ const jobs = new Map();
164
+
165
+ function publicJob(job) {
166
+ return {
167
+ schema: ENIGMA_VIDEO_JOB_SCHEMA,
168
+ id: job.id,
169
+ model: job.model,
170
+ provider: job.provider.id,
171
+ kind: job.kind,
172
+ status: job.status,
173
+ progress: job.progress,
174
+ createdAt: job.createdAt,
175
+ expiresAt: job.expiresAt,
176
+ promptRef: job.promptRef,
177
+ error: job.error,
178
+ };
179
+ }
180
+
181
+ async function withTimeout(operation) {
182
+ const controller = new AbortController();
183
+ const timer = setTimeout(() => controller.abort(new Error('video provider timed out')), timeoutMs);
184
+ try { return await operation(controller.signal); } finally { clearTimeout(timer); }
185
+ }
186
+
187
+ return Object.freeze({
188
+ listModels() {
189
+ return [...modelRoutes].map(([model, provider]) => ({ model, provider: provider.id, modality: 'video' }));
190
+ },
191
+ async queue(input = {}) {
192
+ const request = normalizeVideoRequest(input);
193
+ const provider = modelRoutes.get(request.model);
194
+ if (!provider) throw new Error(`no video provider configured for model ${request.model}`);
195
+ const queued = await withTimeout((signal) => provider.queue(request, { signal }));
196
+ const now = Date.now();
197
+ const job = {
198
+ id: randomUUID(),
199
+ provider,
200
+ providerJobId: requiredString(queued.providerJobId, 'providerJobId', 512),
201
+ model: request.model,
202
+ kind: request.kind,
203
+ status: normalizeProviderStatus(queued.status || 'queued'),
204
+ progress: 0,
205
+ promptRef: reference(request.prompt),
206
+ createdAt: now,
207
+ expiresAt: now + ttlMs,
208
+ error: null,
209
+ usageRecorded: false,
210
+ };
211
+ jobs.set(job.id, job);
212
+ return publicJob(job);
213
+ },
214
+ async retrieve(id) {
215
+ const job = jobs.get(requiredString(id, 'job id', 512));
216
+ if (!job) throw new Error('video job not found');
217
+ if (Date.now() >= job.expiresAt && !TERMINAL.has(job.status)) job.status = 'expired';
218
+ if (job.status === 'expired') return publicJob(job);
219
+ if (!TERMINAL.has(job.status)) {
220
+ const result = await withTimeout((signal) => job.provider.retrieve(job.providerJobId, { signal }));
221
+ job.status = normalizeProviderStatus(result.status);
222
+ job.progress = result.progress ?? (job.status === 'succeeded' ? 100 : job.progress);
223
+ job.error = result.error ? String(result.error).slice(0, 1_000) : null;
224
+ if (job.status === 'succeeded') {
225
+ job.asset = normalizeVideoAsset(result.asset);
226
+ job.providerRequestId = result.providerRequestId || null;
227
+ if (!job.usageRecorded) {
228
+ job.usageRecorded = true;
229
+ await options.onUsage?.({ modality: 'video', provider: job.provider.id, model: job.model, units: 1, promptRef: job.promptRef });
230
+ }
231
+ }
232
+ }
233
+ return job.status === 'succeeded'
234
+ ? { schema: ENIGMA_VIDEO_RESULT_SCHEMA, job: publicJob(job), asset: job.asset, providerRequestId: job.providerRequestId, retention: 'caller-controlled' }
235
+ : publicJob(job);
236
+ },
237
+ async cancel(id) {
238
+ const job = jobs.get(requiredString(id, 'job id', 512));
239
+ if (!job) throw new Error('video job not found');
240
+ if (TERMINAL.has(job.status)) return publicJob(job);
241
+ await job.provider.cancel?.(job.providerJobId, {});
242
+ job.status = 'cancelled';
243
+ return publicJob(job);
244
+ },
245
+ sweep(now = Date.now()) {
246
+ let removed = 0;
247
+ for (const [id, job] of jobs) {
248
+ if (now >= job.expiresAt) { jobs.delete(id); removed += 1; }
249
+ }
250
+ return removed;
251
+ },
252
+ });
253
+ }