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.
- package/README.md +76 -24
- package/apps/cli/bin/enigma-desktop.mjs +140 -0
- package/apps/cli/bin/enigma-terminal.mjs +78 -0
- package/apps/cli/bin/enigma.mjs +1923 -285
- package/apps/desktop/electron-main.cjs +217 -0
- package/apps/desktop/package.json +12 -0
- package/apps/desktop/src/app.js +264 -7
- package/apps/desktop/src/index.html +3514 -1373
- package/apps/desktop/src/launch-electron.mjs +51 -0
- package/apps/desktop/src/server.mjs +2914 -0
- package/apps/desktop/src/styles.css +2972 -260
- package/apps/desktop/src/zk-browser-prove.mjs +53 -0
- package/apps/desktop/src/zk-state.mjs +1789 -0
- package/apps/gateway/bin/enigma-gateway.mjs +102 -5
- package/apps/gateway/src/server.mjs +271 -8
- package/apps/ios/EnigmaCore/Package.swift +12 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/EnigmaAPIClient.swift +227 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/Models.swift +278 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/PKCE.swift +96 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/PrivacyMinimizer.swift +187 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/ToolModels.swift +129 -0
- package/apps/ios/EnigmaCore/Tests/EnigmaCoreTests/EnigmaCoreTests.swift +42 -0
- package/apps/ios/EnigmaIOS/Enigma/AppModel.swift +346 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AccentColor.colorset/Contents.json +12 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/Contents.json +11 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/EnigmaAppIcon.png +0 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/Contents.json +3 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/LaunchBackground.colorset/Contents.json +12 -0
- package/apps/ios/EnigmaIOS/Enigma/ChatView.swift +181 -0
- package/apps/ios/EnigmaIOS/Enigma/CouncilView.swift +78 -0
- package/apps/ios/EnigmaIOS/Enigma/CreateView.swift +152 -0
- package/apps/ios/EnigmaIOS/Enigma/EnigmaApp.swift +52 -0
- package/apps/ios/EnigmaIOS/Enigma/Info.plist +52 -0
- package/apps/ios/EnigmaIOS/Enigma/NaturalLanguagePrivacyTagger.swift +26 -0
- package/apps/ios/EnigmaIOS/Enigma/OAuthClient.swift +321 -0
- package/apps/ios/EnigmaIOS/Enigma/OnboardingView.swift +105 -0
- package/apps/ios/EnigmaIOS/Enigma/PrivateVaultView.swift +275 -0
- package/apps/ios/EnigmaIOS/Enigma/SecureStore.swift +76 -0
- package/apps/ios/EnigmaIOS/Enigma/SettingsView.swift +60 -0
- package/apps/ios/EnigmaIOS/Enigma/Theme.swift +80 -0
- package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/project.pbxproj +211 -0
- package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/xcshareddata/xcschemes/Enigma.xcscheme +23 -0
- package/apps/native-host/README.md +19 -8
- package/apps/native-host/bin/enigma-native-host.mjs +229 -13
- package/apps/relay/bin/enigma-relay.mjs +103 -5
- package/apps/relay/src/federation-runtime.mjs +618 -0
- package/apps/relay/src/server.mjs +310 -9
- package/apps/verifier/bin/enigma-verify.mjs +327 -11
- package/cortex-v3/circuits/build/intent_vk_bytes.json +35 -0
- package/cortex-v3/circuits/build/sale_vk_bytes.json +35 -0
- package/cortex-v3/circuits/build/vk_bytes.json +32 -0
- package/cortex-v3/proving-assets.json +64 -0
- package/cortex-v3/zk/BUILD-CONTRACT.md +87 -0
- package/cortex-v3/zk/action-transition-vk.json +119 -0
- package/cortex-v3/zk/alias-adversarial.test.mjs +220 -0
- package/cortex-v3/zk/groth16-verify-child.mjs +17 -0
- package/cortex-v3/zk/intent-witness.mjs +365 -0
- package/cortex-v3/zk/intent-witness.test.mjs +485 -0
- package/cortex-v3/zk/proving-assets.mjs +203 -0
- package/cortex-v3/zk/sale-witness.mjs +783 -0
- package/cortex-v3/zk/sale-witness.test.mjs +784 -0
- package/cortex-v3/zk/sealed-sale-release-vk.json +119 -0
- package/cortex-v3/zk/settlement-evidence.mjs +722 -0
- package/cortex-v3/zk/setup-intent.mjs +688 -0
- package/cortex-v3/zk/setup-sale.mjs +666 -0
- package/cortex-v3/zk/setup.mjs +594 -0
- package/cortex-v3/zk/witness.mjs +184 -0
- package/cortex-v3/zk/zk-codec.mjs +232 -0
- package/cortex-v3/zk/zk-codec.test.mjs +293 -0
- package/cortex-v3/zk/zk-settle.mjs +370 -0
- package/cortex-v3/zk/zk-tree.mjs +256 -0
- package/cortex-v3/zk/zk-tree.test.mjs +419 -0
- package/deploy/docker-compose.local-production-simulation.yml +36 -0
- package/docs/browser-extension-install.md +8 -6
- package/docs/client-connectors.md +15 -11
- package/docs/developer-ecosystem.md +15 -13
- package/docs/enigma-memory-ready-conformance.md +11 -9
- package/docs/install-anywhere.md +61 -28
- package/docs/installers-and-desktop.md +8 -7
- package/docs/novelty-invention-candidates.md +161 -161
- package/docs/proof-network-claim-boundaries.md +320 -318
- package/examples/01-quickstart-agent/index.mjs +49 -0
- package/examples/01_agent_memory_quickstart.mjs +57 -0
- package/examples/02-multi-agent-swarm/index.mjs +57 -0
- package/examples/02_cross_model_passport.mjs +64 -0
- package/examples/03-langchain-memory/index.mjs +41 -0
- package/examples/03_poseidon_commitment_verification.mjs +71 -0
- package/examples/04-python-trading-agent/trader.py +49 -0
- package/examples/README.md +27 -0
- package/examples/ci/github-actions.yml +7 -2
- package/package.json +142 -11
- package/packages/adapters/PACKAGE_CONTRACT.md +1 -1
- package/packages/connectors/src/index.js +196 -4
- package/packages/connectors/swarm-router.mjs +168 -0
- package/packages/core/src/index.js +248 -1
- package/packages/core/src/version.mjs +7 -0
- package/packages/dev-tools/package.json +19 -0
- package/packages/dev-tools/src/index.js +4 -0
- package/packages/dev-tools/src/memory-benchmark-suite.js +112 -0
- package/packages/dev-tools/src/swarm-simulator.js +101 -0
- package/packages/dev-tools/src/vault-inspector.js +114 -0
- package/packages/dev-tools/src/vector-benchmark.js +100 -0
- package/packages/developer-platform/src/access-credentials.js +341 -0
- package/packages/developer-platform/src/http.js +132 -0
- package/packages/developer-platform/src/index.js +4 -0
- package/packages/developer-platform/src/usage-http.js +60 -0
- package/packages/developer-platform/src/usage.js +295 -0
- package/packages/enclave-runtime/attestation.mjs +159 -0
- package/packages/enclave-runtime/index.mjs +47 -0
- package/packages/enclave-runtime/session-manager.mjs +253 -0
- package/packages/enclave-runtime/zeroization-proof.mjs +227 -0
- package/packages/enigma-reflex/package.json +14 -0
- package/packages/enigma-reflex/src/index.js +204 -0
- package/packages/enigma-reflex/training/generate-dataset.mjs +40 -0
- package/packages/enigma-reflex/training/requirements.txt +8 -0
- package/packages/enigma-reflex/training/train.py +314 -0
- package/packages/enigma-weave/LICENSE +22 -0
- package/packages/enigma-weave/UPSTREAM.json +21 -0
- package/packages/enigma-weave/package.json +14 -0
- package/packages/enigma-weave/src/index.js +286 -0
- package/packages/hosted-cloud/src/index.js +80 -5
- package/packages/importers/src/index.js +432 -0
- package/packages/inference-runtime/src/browser.js +401 -0
- package/packages/inference-runtime/src/chat.js +265 -0
- package/packages/inference-runtime/src/code.js +407 -0
- package/packages/inference-runtime/src/contracts.js +162 -0
- package/packages/inference-runtime/src/http.js +232 -0
- package/packages/inference-runtime/src/image.js +186 -0
- package/packages/inference-runtime/src/index.js +10 -0
- package/packages/inference-runtime/src/model-router.js +320 -0
- package/packages/inference-runtime/src/platform.js +125 -0
- package/packages/inference-runtime/src/privacy.js +400 -0
- package/packages/inference-runtime/src/video.js +253 -0
- package/packages/mcp-server/README.md +22 -6
- package/packages/mcp-server/bin/enigma-mcp.mjs +2 -1
- package/packages/mcp-server/src/index.js +1418 -105
- package/packages/mcp-server/src/oauth.js +561 -0
- package/packages/mcp-server/src/private-handoff.js +84 -0
- package/packages/mcp-server/src/remote-http.js +273 -0
- package/packages/mcp-server/src/remote-policy.js +72 -0
- package/packages/mcp-server/swarm-bridge.mjs +361 -0
- package/packages/mesh/index.d.ts +283 -0
- package/packages/mesh/package.json +23 -0
- package/packages/mesh/src/crypto.js +189 -0
- package/packages/mesh/src/federation-packets.js +353 -0
- package/packages/mesh/src/gossip.js +311 -0
- package/packages/mesh/src/index.js +6 -0
- package/packages/mesh/src/protocol.js +255 -0
- package/packages/mesh/src/router.js +279 -0
- package/packages/mesh/src/transport.js +306 -0
- package/packages/passport/src/index.js +426 -1
- package/packages/private-economy/src/credits-http.js +100 -0
- package/packages/private-economy/src/credits.js +447 -0
- package/packages/private-economy/src/index.js +5 -0
- package/packages/private-economy/src/payments-http.js +120 -0
- package/packages/private-economy/src/payments.js +509 -0
- package/packages/private-economy/src/x402.js +346 -0
- package/packages/proof-network/PACKAGE_CONTRACT.md +21 -0
- package/packages/rag/index.d.ts +182 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/THIRD_PARTY_LICENSES.txt +207 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/config.json +25 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/onnx/model_quantized.onnx +0 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/sha256-manifest.json +28 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer.json +30686 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer_config.json +15 -0
- package/packages/rag/package.json +27 -0
- package/packages/rag/src/blinded-search.js +109 -0
- package/packages/rag/src/bm25.js +169 -0
- package/packages/rag/src/embeddings.js +459 -0
- package/packages/rag/src/hybrid.js +76 -0
- package/packages/rag/src/index.js +38 -0
- package/packages/rag/src/reranker.js +61 -0
- package/packages/rag/src/research.js +107 -0
- package/packages/rag/src/vector-store.js +430 -0
- package/packages/rag/src/verify-model-artifacts.mjs +4 -0
- package/packages/sdk/index.d.ts +760 -0
- package/packages/sdk/package.json +33 -0
- package/packages/sdk/python/README.md +24 -0
- package/packages/sdk/python/enigma_sdk.py +250 -0
- package/packages/sdk/python/pyproject.toml +34 -0
- package/packages/sdk/python/requirements.txt +1 -0
- package/packages/sdk/python/setup.py +20 -0
- package/packages/sdk/src/federation/capability-grant.js +389 -0
- package/packages/sdk/src/federation/federation-router.js +360 -0
- package/packages/sdk/src/federation/ghostmesh-bridge.js +497 -0
- package/packages/sdk/src/federation/index.js +3 -0
- package/packages/sdk/src/index.js +1796 -0
- package/packages/sdk/src/intelligence/contradiction.js +337 -0
- package/packages/sdk/src/intelligence/decision-engine.js +155 -0
- package/packages/sdk/src/intelligence/index.js +4 -0
- package/packages/sdk/src/intelligence/ontology.js +122 -0
- package/packages/sdk/src/intelligence/temporal.js +123 -0
- package/packages/sdk/src/market-client.js +142 -0
- package/packages/sdk/src/mesh-client.js +110 -0
- package/packages/sdk/src/middleware/index.js +3 -0
- package/packages/sdk/src/middleware/langchain.js +159 -0
- package/packages/sdk/src/middleware/llamaindex.js +101 -0
- package/packages/sdk/src/middleware/vercel-ai.js +112 -0
- package/packages/sdk/src/rag-client.js +85 -0
- package/packages/sdk/src/swarm-orchestrator.js +260 -0
- package/packages/settlement/PACKAGE_CONTRACT.md +1 -1
- package/packages/snapcompact/THIRD_PARTY_LICENSES.txt +40 -0
- package/packages/snapcompact/assets/8x13-latin1.bdf +3837 -0
- package/packages/snapcompact/index.d.ts +284 -0
- package/packages/snapcompact/package.json +25 -0
- package/packages/snapcompact/src/index.js +716 -0
- package/packages/storage/PACKAGE_CONTRACT.md +1 -1
- package/packages/terminal-console/animations.mjs +240 -0
- package/packages/terminal-console/auto-anchor.mjs +220 -0
- package/packages/terminal-console/banner.mjs +91 -0
- package/packages/terminal-console/commands.mjs +459 -0
- package/packages/terminal-console/delegation.mjs +152 -0
- package/packages/terminal-console/index.mjs +5 -0
- package/packages/terminal-console/outbox.mjs +143 -0
- package/packages/terminal-console/phantom-bridge.mjs +637 -0
- package/packages/terminal-console/repl.mjs +136 -0
- package/packages/terminal-console/signer-store.mjs +130 -0
- package/packages/terminal-console/solana-rpc.mjs +214 -0
- package/packages/terminal-console/solana-transport.mjs +189 -0
- package/packages/terminal-tui/dashboard.mjs +214 -0
- package/packages/terminal-tui/index.mjs +28 -0
- package/packages/terminal-tui/merkle-tree-renderer.mjs +268 -0
- package/packages/terminal-tui/telemetry-hud.mjs +137 -0
- package/packages/vault/index.d.ts +449 -0
- package/packages/vault/package.json +27 -0
- package/packages/vault/src/e2ee.mjs +393 -0
- package/packages/vault/src/enclave.js +481 -0
- package/packages/vault/src/erasure.js +207 -0
- package/packages/vault/src/index.js +1018 -155
- package/packages/vault/src/persistence.js +307 -0
- package/packages/vault/src/poseidon.js +354 -0
- package/packages/vault/src/receipt.js +459 -0
- package/scripts/benchmark-optical-context.mjs +166 -0
- package/scripts/bootstrap-enigma.mjs +502 -0
- package/scripts/build-edge-backend-workers.mjs +20 -5
- package/scripts/build-goal-completion-audit.mjs +72 -25
- package/scripts/build-hosted-api-key-lifecycle.mjs +26 -8
- package/scripts/build-hosted-customer-lifecycle.mjs +20 -3
- package/scripts/build-hosted-probe-worker.mjs +19 -4
- package/scripts/build-installer-assets.mjs +41 -21
- package/scripts/build-operator-evidence-starter.mjs +59 -1
- package/scripts/build-production-backend-env-kit.mjs +2 -0
- package/scripts/build-production-unblocker.mjs +3 -0
- package/scripts/check.mjs +17 -3
- package/scripts/collect-hosted-backend-live-evidence.mjs +49 -12
- package/scripts/install-enigma-local.mjs +18 -5
- package/scripts/release-audit.mjs +65 -109
- package/scripts/release-provenance.mjs +6 -0
- package/scripts/run-backend-readiness-smoke.mjs +112 -10
- package/scripts/scan-secrets.mjs +1 -0
- package/scripts/simulate-production-env.mjs +7 -2
- package/scripts/validate-hosted-backend-live.mjs +112 -1
- package/specs/antibody-pack-v1.schema.json +95 -0
- package/specs/antigen-envelope-v1.schema.json +81 -0
- package/specs/boundary-manifest-v1.schema.json +35 -35
- package/specs/capsule-v1.schema.json +55 -55
- package/specs/claim-boundary-manifest-v1.schema.json +22 -22
- package/specs/claim-ledger-v1.schema.json +291 -0
- package/specs/context-passport-v1.schema.json +59 -0
- package/specs/deletion-tombstone-v1.schema.json +26 -26
- package/specs/evidence-packet-v1.schema.json +177 -0
- package/specs/hosted-backend-live-evidence-v1.schema.json +72 -3
- package/specs/immune-scan-report-v1.schema.json +112 -0
- package/specs/lifecycle-receipt-log-v1.schema.json +67 -0
- package/specs/memory-atom-v1.schema.json +59 -0
- package/specs/memory-event-v1.schema.json +42 -42
- package/specs/passport-v1.schema.json +50 -50
- package/specs/proof-of-non-use-v1.schema.json +65 -0
- package/specs/quarantine-record-v1.schema.json +126 -0
- package/specs/receipt-v1.schema.json +61 -61
- package/specs/state-checkpoint-v1.schema.json +37 -37
- package/specs/trust-bundle-v1.schema.json +56 -56
- package/specs/trust-card-v1.schema.json +119 -0
- package/docs/proof-network-launch-plan.md +0 -421
- package/packages/metering/PACKAGE_CONTRACT.md +0 -20
- package/scripts/build-ai-orchestration-plan.mjs +0 -248
|
@@ -0,0 +1,2914 @@
|
|
|
1
|
+
// Enigma Sovereign Terminal Desktop Server Module
|
|
2
|
+
// Reusable server library for enigma-desktop CLI bin, scripts, and programmatic hosts.
|
|
3
|
+
// Implements strict host/origin validation (exact 127.0.0.1 or localhost + bound port) to defeat DNS rebinding.
|
|
4
|
+
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { ENIGMA_VERSION } from '../../../packages/core/src/version.mjs';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import crypto from 'node:crypto';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { TerminalSession } from '../../../packages/terminal-console/commands.mjs';
|
|
15
|
+
import { PhantomBrowserBridge, OFFICIAL_TOKEN_CA, SOLANA_MAINNET_RPC } from '../../../packages/terminal-console/phantom-bridge.mjs';
|
|
16
|
+
import { fetchSolanaStreamMetrics } from '../../../packages/terminal-console/solana-rpc.mjs';
|
|
17
|
+
import { AutoAnchorEngine } from '../../../packages/terminal-console/auto-anchor.mjs';
|
|
18
|
+
import { createSolanaProofMemoRef } from '../../../packages/terminal-console/solana-transport.mjs';
|
|
19
|
+
import { createProofNetworkAnchorBatch } from '../../../packages/proof-network/src/index.js';
|
|
20
|
+
import { normalizeSolanaLocalArtifact } from '../../cli/bin/enigma.mjs';
|
|
21
|
+
import { createZkRuntime, defaultZkStatePath, ZK_HONESTY_NOTE } from './zk-state.mjs';
|
|
22
|
+
import { EncryptedVectorStore } from '../../../packages/rag/src/index.js';
|
|
23
|
+
import { computeHidingCommitment, deriveKeysFromPassphrase, PBKDF2_KDF_SPEC } from '../../../packages/vault/src/index.js';
|
|
24
|
+
import { Enigma } from '../../../packages/sdk/src/index.js';
|
|
25
|
+
import { GhostMeshFederationBridge } from '../../../packages/sdk/src/federation/index.js';
|
|
26
|
+
import {
|
|
27
|
+
WebSocketTransport as FederationWebSocketTransport,
|
|
28
|
+
deriveDestination,
|
|
29
|
+
generateMeshIdentity,
|
|
30
|
+
} from '../../../packages/mesh/src/index.js';
|
|
31
|
+
import { createEnvironmentPlatformRuntime } from '../../../packages/inference-runtime/src/platform.js';
|
|
32
|
+
import { createInferenceHttpHandler } from '../../../packages/inference-runtime/src/http.js';
|
|
33
|
+
import { apiKeyFromAuthorization, createApiKeyService, createEphemeralApiKeyStore, createFileApiKeyStore } from '../../../packages/developer-platform/src/access-credentials.js';
|
|
34
|
+
import { createApiKeyHttpHandler } from '../../../packages/developer-platform/src/http.js';
|
|
35
|
+
import { createEphemeralUsageStore, createFileUsageStore, createUsageService } from '../../../packages/developer-platform/src/usage.js';
|
|
36
|
+
import { createUsageHttpHandler } from '../../../packages/developer-platform/src/usage-http.js';
|
|
37
|
+
import { createEphemeralCreditsStore, createFileCreditsStore, createPrivateCreditsService } from '../../../packages/private-economy/src/credits.js';
|
|
38
|
+
import { createPrivateCreditsHttpHandler } from '../../../packages/private-economy/src/credits-http.js';
|
|
39
|
+
import { createCreditPaymentService, createEphemeralPaymentStore, createFilePaymentStore, createSolanaPaymentVerifier, createStripeSubscriptionProvider } from '../../../packages/private-economy/src/payments.js';
|
|
40
|
+
import { createPaymentHttpHandler } from '../../../packages/private-economy/src/payments-http.js';
|
|
41
|
+
import { createX402FacilitatorClient, createX402ResourceHandler } from '../../../packages/private-economy/src/x402.js';
|
|
42
|
+
const DESKTOP_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
|
|
44
|
+
export function getDefaultBundlePath() {
|
|
45
|
+
if (process.env.ENIGMA_BUNDLE_PATH) {
|
|
46
|
+
return path.resolve(process.env.ENIGMA_BUNDLE_PATH);
|
|
47
|
+
}
|
|
48
|
+
return path.resolve(process.cwd(), '.enigma', 'bundle.json');
|
|
49
|
+
}
|
|
50
|
+
const MAX_BODY_BYTES = 64 * 1024; // 64 KB limit
|
|
51
|
+
|
|
52
|
+
const MIME_TYPES = {
|
|
53
|
+
'.html': 'text/html; charset=utf-8',
|
|
54
|
+
'.css': 'text/css; charset=utf-8',
|
|
55
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
56
|
+
'.json': 'application/json',
|
|
57
|
+
'.png': 'image/png',
|
|
58
|
+
'.svg': 'image/svg+xml',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
class MemoryCaptureStream {
|
|
62
|
+
constructor() {
|
|
63
|
+
this.buffer = '';
|
|
64
|
+
}
|
|
65
|
+
write(chunk) {
|
|
66
|
+
this.buffer += String(chunk);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
clear() {
|
|
70
|
+
this.buffer = '';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class DesktopOperationalError extends Error {
|
|
75
|
+
constructor(code, message, statusCode = 400) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.name = 'DesktopOperationalError';
|
|
78
|
+
this.code = code;
|
|
79
|
+
this.statusCode = statusCode;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function adapterEntries(options = {}) {
|
|
84
|
+
const configured = options.modelAdapters
|
|
85
|
+
?? options.providerAdapters
|
|
86
|
+
?? options.agentAdapters
|
|
87
|
+
?? options.adapters
|
|
88
|
+
?? options.connectors
|
|
89
|
+
?? [];
|
|
90
|
+
if (configured instanceof Map) return [...configured.entries()];
|
|
91
|
+
if (Array.isArray(configured)) return configured.map((adapter) => [adapter?.id, adapter]);
|
|
92
|
+
if (configured && typeof configured === 'object') return Object.entries(configured);
|
|
93
|
+
throw new TypeError('Desktop model adapters must be an array, object, or Map');
|
|
94
|
+
}
|
|
95
|
+
const DESKTOP_CONTEXT_CARRIER_NAMES = new Set(['text', 'bitmap-png']);
|
|
96
|
+
const DESKTOP_CONTEXT_CARRIER_REQUEST_MODES = new Set(['text', 'bitmap-png', 'auto']);
|
|
97
|
+
|
|
98
|
+
function normalizeCarrierNames(value) {
|
|
99
|
+
let names;
|
|
100
|
+
if (Array.isArray(value) || value instanceof Set) {
|
|
101
|
+
names = [...value];
|
|
102
|
+
} else if (typeof value === 'string') {
|
|
103
|
+
names = [value];
|
|
104
|
+
} else if (value && typeof value === 'object') {
|
|
105
|
+
const declared = value.supported ?? value.carriers ?? value.modes;
|
|
106
|
+
if (Array.isArray(declared) || declared instanceof Set) {
|
|
107
|
+
names = [...declared];
|
|
108
|
+
} else if (typeof declared === 'string') {
|
|
109
|
+
names = [declared];
|
|
110
|
+
} else {
|
|
111
|
+
names = [...DESKTOP_CONTEXT_CARRIER_NAMES].filter((name) => value[name] === true);
|
|
112
|
+
if (value.vision === true) names.push('bitmap-png');
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
names = [];
|
|
116
|
+
}
|
|
117
|
+
const normalized = [...new Set(['text', ...names.map((name) => String(name).trim())])];
|
|
118
|
+
const unsupported = normalized.find((name) => !DESKTOP_CONTEXT_CARRIER_NAMES.has(name));
|
|
119
|
+
if (unsupported) {
|
|
120
|
+
throw new TypeError(`Unsupported desktop context carrier capability: ${unsupported}`);
|
|
121
|
+
}
|
|
122
|
+
return Object.freeze(normalized);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeContextCarriersByModel(adapter, models) {
|
|
126
|
+
const configured = adapter.contextCarriersByModel;
|
|
127
|
+
const entries = configured instanceof Map
|
|
128
|
+
? [...configured.entries()]
|
|
129
|
+
: (configured && typeof configured === 'object' ? Object.entries(configured) : []);
|
|
130
|
+
const declared = new Map(entries.map(([model, names]) => [String(model).trim(), names]));
|
|
131
|
+
for (const model of declared.keys()) {
|
|
132
|
+
if (!models.includes(model)) {
|
|
133
|
+
throw new TypeError(`Desktop context carrier capability references unconfigured model: ${model}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const normalized = {};
|
|
137
|
+
for (const model of models) {
|
|
138
|
+
normalized[model] = normalizeCarrierNames(declared.get(model));
|
|
139
|
+
}
|
|
140
|
+
return Object.freeze(normalized);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveDesktopContextCarrierRequest(payload, adapter, model) {
|
|
144
|
+
const requested = payload.contextCarrier;
|
|
145
|
+
if (!requested || typeof requested !== 'object' || Array.isArray(requested)) return null;
|
|
146
|
+
const mode = requested.mode === undefined ? 'text' : String(requested.mode);
|
|
147
|
+
if (!DESKTOP_CONTEXT_CARRIER_REQUEST_MODES.has(mode)) {
|
|
148
|
+
throw new DesktopOperationalError(
|
|
149
|
+
'CONTEXT_CARRIER_MODE_INVALID',
|
|
150
|
+
'contextCarrier.mode must be "text", "bitmap-png", or "auto"',
|
|
151
|
+
400
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const supported = adapter.contextCarriersByModel[model] || Object.freeze(['text']);
|
|
155
|
+
return {
|
|
156
|
+
mode,
|
|
157
|
+
provider: adapter.provider,
|
|
158
|
+
model,
|
|
159
|
+
vision: supported.includes('bitmap-png'),
|
|
160
|
+
costs: requested.costs ?? requested.costPolicy ?? requested.costAssumptions,
|
|
161
|
+
expectedReuse: requested.expectedReuse,
|
|
162
|
+
tokenEstimates: requested.tokenEstimates,
|
|
163
|
+
limits: requested.limits ?? requested.renderLimits,
|
|
164
|
+
tabWidth: requested.tabWidth,
|
|
165
|
+
collapseRepeatedLines: requested.collapseRepeatedLines,
|
|
166
|
+
repetition: requested.repetition,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function sanitizeDesktopAdapterResult(value, key = '', seen = new WeakSet()) {
|
|
171
|
+
if (typeof value === 'string') {
|
|
172
|
+
return value.startsWith('data:image/png;base64,') ? '' : value;
|
|
173
|
+
}
|
|
174
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
175
|
+
return { omittedBinaryBytes: value.byteLength };
|
|
176
|
+
}
|
|
177
|
+
if (!value || typeof value !== 'object') return value;
|
|
178
|
+
if (seen.has(value)) return '[circular]';
|
|
179
|
+
seen.add(value);
|
|
180
|
+
if (
|
|
181
|
+
key === 'contextCarrier'
|
|
182
|
+
|| (
|
|
183
|
+
typeof value.selected === 'string'
|
|
184
|
+
&& typeof value.textFallback === 'string'
|
|
185
|
+
&& Array.isArray(value.blocks)
|
|
186
|
+
&& value.descriptor
|
|
187
|
+
&& value.decision
|
|
188
|
+
)
|
|
189
|
+
) {
|
|
190
|
+
return {
|
|
191
|
+
descriptor: value.descriptor,
|
|
192
|
+
decision: value.decision,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (Array.isArray(value)) {
|
|
196
|
+
return value.map((entry) => sanitizeDesktopAdapterResult(entry, '', seen));
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
value?.source?.type === 'base64'
|
|
200
|
+
|| value?.inlineData?.mimeType === 'image/png'
|
|
201
|
+
|| value?.type === 'input_image'
|
|
202
|
+
) {
|
|
203
|
+
return { omittedCarrierImage: true };
|
|
204
|
+
}
|
|
205
|
+
const sanitized = {};
|
|
206
|
+
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
207
|
+
sanitized[entryKey] = sanitizeDesktopAdapterResult(entryValue, entryKey, seen);
|
|
208
|
+
}
|
|
209
|
+
return sanitized;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const DESKTOP_PRIVACY_MODES = new Set(['off', 'anchor-guard']);
|
|
213
|
+
const DESKTOP_ANCHOR_GUARD_PATTERNS = Object.freeze([
|
|
214
|
+
{
|
|
215
|
+
category: 'email',
|
|
216
|
+
expression: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
category: 'evm_wallet',
|
|
220
|
+
expression: /\b0x[a-fA-F0-9]{40}\b/g,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
category: 'ipv4',
|
|
224
|
+
expression: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
|
|
225
|
+
valid: (value) => value.split('.').every((part) => Number(part) <= 255),
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
category: 'phone',
|
|
229
|
+
expression: /(?:\+\d{1,3}[\s.-]?)?(?:\(\d{3}\)|\d{3})[\s.-]\d{3}[\s.-]\d{4}\b/g,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
category: 'base58_wallet',
|
|
233
|
+
expression: /\b[1-9A-HJ-NP-Za-km-z]{32,44}\b/g,
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
function resolveDesktopPrivacyMode(payload) {
|
|
238
|
+
const requested = payload.privacy;
|
|
239
|
+
if (requested === undefined || requested === null) return 'off';
|
|
240
|
+
const mode = typeof requested === 'string' ? requested : requested?.mode;
|
|
241
|
+
if (!DESKTOP_PRIVACY_MODES.has(mode)) {
|
|
242
|
+
throw new DesktopOperationalError(
|
|
243
|
+
'PRIVACY_MODE_INVALID',
|
|
244
|
+
'privacy.mode must be "off" or "anchor-guard"',
|
|
245
|
+
400
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return mode;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function createDesktopAnchorGuard(mode) {
|
|
252
|
+
const replacements = [];
|
|
253
|
+
const tokensByValue = new Map();
|
|
254
|
+
|
|
255
|
+
function tokenFor(value, category) {
|
|
256
|
+
const prior = tokensByValue.get(value);
|
|
257
|
+
if (prior) return prior;
|
|
258
|
+
const token = `{{ENIGMA_${category.toUpperCase()}_${replacements.length + 1}}}`;
|
|
259
|
+
tokensByValue.set(value, token);
|
|
260
|
+
replacements.push({ category, token, value });
|
|
261
|
+
return token;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function redactText(value) {
|
|
265
|
+
if (mode === 'off' || typeof value !== 'string' || value.length === 0) return value;
|
|
266
|
+
let protectedText = value;
|
|
267
|
+
for (const pattern of DESKTOP_ANCHOR_GUARD_PATTERNS) {
|
|
268
|
+
protectedText = protectedText.replace(pattern.expression, (match) => {
|
|
269
|
+
if (pattern.valid && !pattern.valid(match)) return match;
|
|
270
|
+
return tokenFor(match, pattern.category);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return protectedText;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function transformStrings(value, transform, seen = new WeakMap()) {
|
|
277
|
+
if (typeof value === 'string') return transform(value);
|
|
278
|
+
if (!value || typeof value !== 'object' || Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
if (seen.has(value)) return seen.get(value);
|
|
282
|
+
if (Array.isArray(value)) {
|
|
283
|
+
const transformed = [];
|
|
284
|
+
seen.set(value, transformed);
|
|
285
|
+
for (const entry of value) transformed.push(transformStrings(entry, transform, seen));
|
|
286
|
+
return transformed;
|
|
287
|
+
}
|
|
288
|
+
const transformed = {};
|
|
289
|
+
seen.set(value, transformed);
|
|
290
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
291
|
+
transformed[key] = transformStrings(entry, transform, seen);
|
|
292
|
+
}
|
|
293
|
+
return transformed;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function redactValue(value) {
|
|
297
|
+
return transformStrings(value, redactText);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function restoreText(value) {
|
|
301
|
+
if (typeof value !== 'string' || replacements.length === 0) return value;
|
|
302
|
+
let restored = value;
|
|
303
|
+
for (const replacement of replacements) {
|
|
304
|
+
restored = restored.replaceAll(replacement.token, replacement.value);
|
|
305
|
+
}
|
|
306
|
+
return restored;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function restoreValue(value) {
|
|
310
|
+
return transformStrings(value, restoreText);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function report(invocation) {
|
|
314
|
+
if (mode === 'off') return null;
|
|
315
|
+
const categoryCounts = {};
|
|
316
|
+
for (const replacement of replacements) {
|
|
317
|
+
categoryCounts[replacement.category] = (categoryCounts[replacement.category] || 0) + 1;
|
|
318
|
+
}
|
|
319
|
+
const requestValue = String(invocation.prompt || '');
|
|
320
|
+
const retrievedValue = String(invocation.context || '');
|
|
321
|
+
const protectedInput = JSON.stringify({
|
|
322
|
+
schema: 'enigma.desktop.protected_adapter_input.v1',
|
|
323
|
+
requestValue,
|
|
324
|
+
retrievedValue,
|
|
325
|
+
});
|
|
326
|
+
const { commitment: protectedInputRef } = computeHidingCommitment(protectedInput);
|
|
327
|
+
return {
|
|
328
|
+
schema: 'enigma.desktop.anchor_guard_report.v1',
|
|
329
|
+
mode,
|
|
330
|
+
redactionCount: replacements.length,
|
|
331
|
+
categories: Object.fromEntries(Object.entries(categoryCounts).sort(([a], [b]) => a.localeCompare(b))),
|
|
332
|
+
protectedInputRef,
|
|
333
|
+
inputShape: {
|
|
334
|
+
requestChars: requestValue.length,
|
|
335
|
+
retrievedChars: retrievedValue.length,
|
|
336
|
+
},
|
|
337
|
+
limitations: [
|
|
338
|
+
'Pattern-based guard covers email, EVM wallet, IPv4, formatted phone, and base58 wallet anchors.',
|
|
339
|
+
'It does not identify arbitrary names, places, or sensitive prose.',
|
|
340
|
+
'Configured adapters control final transport; this unsigned operational report is not cryptographic proof.',
|
|
341
|
+
],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return { redactValue, restoreValue, report };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
export function createDesktopModelAdapterRegistry(options = {}) {
|
|
349
|
+
const registry = new Map();
|
|
350
|
+
for (const [configuredId, candidate] of adapterEntries(options)) {
|
|
351
|
+
const adapter = typeof candidate === 'function'
|
|
352
|
+
? { id: configuredId, invoke: candidate }
|
|
353
|
+
: candidate;
|
|
354
|
+
if (!adapter || typeof adapter !== 'object' || typeof adapter.invoke !== 'function') continue;
|
|
355
|
+
const id = String(adapter.id ?? configuredId ?? '').trim();
|
|
356
|
+
const provider = String(adapter.provider ?? adapter.providerId ?? '').trim();
|
|
357
|
+
const models = Array.isArray(adapter.models)
|
|
358
|
+
? adapter.models.map((model) => String(model).trim()).filter(Boolean)
|
|
359
|
+
: [String(adapter.model ?? '').trim()].filter(Boolean);
|
|
360
|
+
if (!id || !provider || models.length === 0) {
|
|
361
|
+
throw new TypeError('Each desktop model adapter requires id, provider, at least one model, and invoke()');
|
|
362
|
+
}
|
|
363
|
+
if (registry.has(id)) throw new TypeError(`Duplicate desktop model adapter id: ${id}`);
|
|
364
|
+
const uniqueModels = Object.freeze([...new Set(models)]);
|
|
365
|
+
registry.set(id, Object.freeze({
|
|
366
|
+
id,
|
|
367
|
+
provider,
|
|
368
|
+
models: uniqueModels,
|
|
369
|
+
contextCarriersByModel: normalizeContextCarriersByModel(adapter, uniqueModels),
|
|
370
|
+
invoke: adapter.invoke.bind(adapter),
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
return registry;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function resolveDesktopModelAdapter(registry, selection = {}) {
|
|
377
|
+
const provider = String(selection.provider ?? '').trim();
|
|
378
|
+
const model = String(selection.model ?? '').trim();
|
|
379
|
+
const adapterId = String(selection.adapterId ?? selection.adapter_id ?? '').trim();
|
|
380
|
+
const configured = [...registry.values()];
|
|
381
|
+
if (configured.length === 0) {
|
|
382
|
+
throw new DesktopOperationalError(
|
|
383
|
+
'MODEL_ADAPTER_NOT_CONFIGURED',
|
|
384
|
+
'No desktop model adapter is configured. Configure an authenticated provider connector in createDesktopServer({ modelAdapters }) and restart the desktop host.',
|
|
385
|
+
503
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (!provider || !model) {
|
|
389
|
+
throw new DesktopOperationalError(
|
|
390
|
+
'MODEL_SELECTION_REQUIRED',
|
|
391
|
+
'provider and model are required; select a configured desktop model adapter',
|
|
392
|
+
400
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const adapter = configured.find((entry) =>
|
|
396
|
+
(!adapterId || entry.id === adapterId)
|
|
397
|
+
&& entry.provider === provider
|
|
398
|
+
&& entry.models.includes(model)
|
|
399
|
+
);
|
|
400
|
+
if (adapter) return adapter;
|
|
401
|
+
throw new DesktopOperationalError(
|
|
402
|
+
'MODEL_ADAPTER_NOT_CONFIGURED',
|
|
403
|
+
`No configured desktop model adapter matches provider "${provider}", model "${model}"${adapterId ? `, adapter "${adapterId}"` : ''}.`,
|
|
404
|
+
422
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function createDesktopServer(options = {}) {
|
|
409
|
+
let boundPort = options.port !== undefined ? options.port : 8765;
|
|
410
|
+
const host = '127.0.0.1';
|
|
411
|
+
let ephemeralTempDir = null;
|
|
412
|
+
let bundlePath;
|
|
413
|
+
if (options.inMemoryOnly === true) {
|
|
414
|
+
ephemeralTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'enigma-ephemeral-sandbox-'));
|
|
415
|
+
bundlePath = options.bundlePath ? path.resolve(options.bundlePath) : path.join(ephemeralTempDir, 'bundle.json');
|
|
416
|
+
} else {
|
|
417
|
+
bundlePath = options.bundlePath ? path.resolve(options.bundlePath) : getDefaultBundlePath();
|
|
418
|
+
}
|
|
419
|
+
const authToken = options.authToken || crypto.randomBytes(32).toString('hex');
|
|
420
|
+
const oauthBearerAuthorizer = options.authorizeBearer || options.oauth?.authorizeBearer || null;
|
|
421
|
+
if (oauthBearerAuthorizer !== null && typeof oauthBearerAuthorizer !== 'function') {
|
|
422
|
+
throw new TypeError('authorizeBearer must be a function');
|
|
423
|
+
}
|
|
424
|
+
const apiKeyStore = options.apiKeyStore || (options.inMemoryOnly === true
|
|
425
|
+
? createEphemeralApiKeyStore()
|
|
426
|
+
: createFileApiKeyStore({ path: options.apiKeyStorePath || path.join(path.dirname(bundlePath), 'api-keys.json') }));
|
|
427
|
+
const apiKeys = options.apiKeys || createApiKeyService({
|
|
428
|
+
store: apiKeyStore,
|
|
429
|
+
environment: options.apiKeyEnvironment || 'live',
|
|
430
|
+
});
|
|
431
|
+
const usageStore = options.usageStore || (options.inMemoryOnly === true
|
|
432
|
+
? createEphemeralUsageStore()
|
|
433
|
+
: createFileUsageStore({ path: options.usageStorePath || path.join(path.dirname(bundlePath), 'usage.json') }));
|
|
434
|
+
const usage = options.usage || createUsageService({
|
|
435
|
+
store: usageStore,
|
|
436
|
+
pricing: options.usagePricing || {},
|
|
437
|
+
defaultPrincipal: { ownerId: 'local-owner', tenantId: 'local' },
|
|
438
|
+
});
|
|
439
|
+
const creditsStore = options.creditsStore || (options.inMemoryOnly === true
|
|
440
|
+
? createEphemeralCreditsStore()
|
|
441
|
+
: createFileCreditsStore({ path: options.creditsStorePath || path.join(path.dirname(bundlePath), 'credits.json') }));
|
|
442
|
+
const credits = options.credits || createPrivateCreditsService({ store: creditsStore });
|
|
443
|
+
const recoveryAttempts = [];
|
|
444
|
+
const configuredPaymentPackages = options.paymentPackages || JSON.parse(process.env.ENIGMA_CREDIT_PACKAGES_JSON || '[]');
|
|
445
|
+
const usdcDestinationTokenAccount = options.usdcDestinationTokenAccount || process.env.ENIGMA_USDC_TREASURY_TOKEN_ACCOUNT || null;
|
|
446
|
+
const tokenBurnMint = options.tokenBurnMint || process.env.ENIGMA_CREDIT_BURN_MINT || null;
|
|
447
|
+
const verifySolanaPayment = options.verifySolanaPayment || ((usdcDestinationTokenAccount || tokenBurnMint)
|
|
448
|
+
? createSolanaPaymentVerifier({ rpcUrl: options.solanaPaymentRpcUrl || process.env.ENIGMA_SOLANA_PAYMENT_RPC || SOLANA_MAINNET_RPC })
|
|
449
|
+
: null);
|
|
450
|
+
const stripeApiKey = process.env.ENIGMA_STRIPE_SECRET_KEY || null;
|
|
451
|
+
const stripeWebhookSecret = process.env.ENIGMA_STRIPE_WEBHOOK_SECRET || null;
|
|
452
|
+
if (!options.stripe && Boolean(stripeApiKey) !== Boolean(stripeWebhookSecret)) {
|
|
453
|
+
throw new Error('Stripe subscription rail requires both ENIGMA_STRIPE_SECRET_KEY and ENIGMA_STRIPE_WEBHOOK_SECRET');
|
|
454
|
+
}
|
|
455
|
+
const stripe = options.stripe || (stripeApiKey && stripeWebhookSecret
|
|
456
|
+
? createStripeSubscriptionProvider({
|
|
457
|
+
apiKey: stripeApiKey,
|
|
458
|
+
webhookSecret: stripeWebhookSecret,
|
|
459
|
+
baseUrl: process.env.ENIGMA_STRIPE_API_BASE || undefined,
|
|
460
|
+
})
|
|
461
|
+
: null);
|
|
462
|
+
const paymentStore = options.paymentStore || (options.inMemoryOnly === true
|
|
463
|
+
? createEphemeralPaymentStore()
|
|
464
|
+
: createFilePaymentStore({ path: options.paymentStorePath || path.join(path.dirname(bundlePath), 'payments.json') }));
|
|
465
|
+
const payments = options.payments || createCreditPaymentService({
|
|
466
|
+
store: paymentStore,
|
|
467
|
+
credits,
|
|
468
|
+
packages: configuredPaymentPackages,
|
|
469
|
+
usdcDestinationTokenAccount,
|
|
470
|
+
tokenBurnMint,
|
|
471
|
+
verifySolanaPayment,
|
|
472
|
+
stripe,
|
|
473
|
+
});
|
|
474
|
+
function localSessionPrincipal(req) {
|
|
475
|
+
const candidate = String(req.headers['x-enigma-auth-token'] || '');
|
|
476
|
+
const actual = Buffer.from(candidate);
|
|
477
|
+
const expected = Buffer.from(authToken);
|
|
478
|
+
if (actual.length !== expected.length || !crypto.timingSafeEqual(actual, expected)) return null;
|
|
479
|
+
return { subject: 'local-owner', ownerId: 'local-owner', tenantId: 'local', scopes: apiKeys.supportedScopes, authentication: 'desktop_session' };
|
|
480
|
+
}
|
|
481
|
+
function requiredApiScopes(url) {
|
|
482
|
+
if (url.pathname.startsWith('/v1/images/')) return ['images:generate'];
|
|
483
|
+
if (url.pathname.startsWith('/v1/videos/')) return ['video:generate'];
|
|
484
|
+
if (url.pathname.startsWith('/v1/code/')) return ['code:execute'];
|
|
485
|
+
if (url.pathname.startsWith('/v1/browser/')) return ['browser:control'];
|
|
486
|
+
return ['models:invoke'];
|
|
487
|
+
}
|
|
488
|
+
async function authenticateApiRequest(req, requiredScopes) {
|
|
489
|
+
const local = localSessionPrincipal(req);
|
|
490
|
+
if (local) {
|
|
491
|
+
usage.setPrincipal(local);
|
|
492
|
+
req.enigmaPrincipal = local;
|
|
493
|
+
return local;
|
|
494
|
+
}
|
|
495
|
+
const rawApiKey = apiKeyFromAuthorization(req.headers.authorization);
|
|
496
|
+
let principal = rawApiKey ? await apiKeys.authenticate(rawApiKey, { requiredScopes }) : null;
|
|
497
|
+
if (!principal && oauthBearerAuthorizer) {
|
|
498
|
+
principal = await oauthBearerAuthorizer({
|
|
499
|
+
authorization: req.headers.authorization,
|
|
500
|
+
requiredScopes,
|
|
501
|
+
request: req,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (principal) {
|
|
505
|
+
usage.setPrincipal(principal);
|
|
506
|
+
req.enigmaPrincipal = principal;
|
|
507
|
+
}
|
|
508
|
+
return principal;
|
|
509
|
+
}
|
|
510
|
+
const apiKeyHttp = createApiKeyHttpHandler({
|
|
511
|
+
apiKeys,
|
|
512
|
+
authorizeAdmin: async ({ request }) => localSessionPrincipal(request),
|
|
513
|
+
});
|
|
514
|
+
const usageHttp = createUsageHttpHandler({
|
|
515
|
+
usage,
|
|
516
|
+
authorize: async ({ request, requiredScopes }) => authenticateApiRequest(request, requiredScopes),
|
|
517
|
+
});
|
|
518
|
+
const creditsHttp = createPrivateCreditsHttpHandler({
|
|
519
|
+
credits,
|
|
520
|
+
authorizeCreation: async ({ request }) => Boolean(await authenticateApiRequest(request, ['credits:create'])),
|
|
521
|
+
admitRecovery: async ({ request }) => {
|
|
522
|
+
if (!await authenticateApiRequest(request, ['credits:create'])) return false;
|
|
523
|
+
const cutoff = Date.now() - 60_000;
|
|
524
|
+
while (recoveryAttempts.length && recoveryAttempts[0] < cutoff) recoveryAttempts.shift();
|
|
525
|
+
if (recoveryAttempts.length >= 5) return false;
|
|
526
|
+
recoveryAttempts.push(Date.now());
|
|
527
|
+
return true;
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
const paymentHttp = createPaymentHttpHandler({
|
|
531
|
+
payments,
|
|
532
|
+
stripe,
|
|
533
|
+
successUrl: () => `http://${host}:${boundPort}/?payment=success`,
|
|
534
|
+
cancelUrl: () => `http://${host}:${boundPort}/?payment=cancelled`,
|
|
535
|
+
});
|
|
536
|
+
const instanceId = crypto.randomUUID();
|
|
537
|
+
|
|
538
|
+
const phantomBridge = new PhantomBrowserBridge({
|
|
539
|
+
port: 0,
|
|
540
|
+
rpcUrl: SOLANA_MAINNET_RPC,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
const autoAnchorEngine = new AutoAnchorEngine({
|
|
544
|
+
rootDir: path.dirname(bundlePath),
|
|
545
|
+
safeStorage: options.safeStorage,
|
|
546
|
+
cluster: 'mainnet-beta',
|
|
547
|
+
});
|
|
548
|
+
const stdoutCapture = new MemoryCaptureStream();
|
|
549
|
+
const stderrCapture = new MemoryCaptureStream();
|
|
550
|
+
|
|
551
|
+
const vectorStoreDir = path.dirname(bundlePath);
|
|
552
|
+
const shouldPersistVectorStore = options.inMemoryOnly !== true && options.persistVectorStore !== false;
|
|
553
|
+
const shouldPersistSdkState = options.inMemoryOnly !== true && options.persistSdkState !== false;
|
|
554
|
+
const requiresPersistentKey = shouldPersistVectorStore || shouldPersistSdkState;
|
|
555
|
+
const vectorStorePath = options.vectorStorePath
|
|
556
|
+
? path.resolve(options.vectorStorePath)
|
|
557
|
+
: path.join(vectorStoreDir, 'vector_store.enc.json');
|
|
558
|
+
const vectorStoreKeyPath = path.join(vectorStoreDir, 'vector_store.key.enc');
|
|
559
|
+
|
|
560
|
+
function getOrDeriveVectorMasterKey() {
|
|
561
|
+
if (options.vaultKey) {
|
|
562
|
+
return Buffer.isBuffer(options.vaultKey) ? options.vaultKey : Buffer.from(options.vaultKey, 'hex');
|
|
563
|
+
}
|
|
564
|
+
if (options.customKey || (options.zk && options.zk.customKey)) {
|
|
565
|
+
const k = String(options.customKey || options.zk.customKey);
|
|
566
|
+
return crypto.createHash('sha256').update(`enigma.custom_key.vector.${k}`).digest();
|
|
567
|
+
}
|
|
568
|
+
const passphraseInput = options.passphrase || process.env.ENIGMA_PASSPHRASE;
|
|
569
|
+
if (passphraseInput) {
|
|
570
|
+
const saltPath = path.join(vectorStoreDir, 'vector_store.kdf.json');
|
|
571
|
+
let salt;
|
|
572
|
+
if (fs.existsSync(saltPath)) {
|
|
573
|
+
try {
|
|
574
|
+
const kdfData = JSON.parse(fs.readFileSync(saltPath, 'utf8'));
|
|
575
|
+
salt = Buffer.from(kdfData.salt, 'base64');
|
|
576
|
+
} catch (e) {
|
|
577
|
+
salt = crypto.randomBytes(16);
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
salt = crypto.randomBytes(16);
|
|
581
|
+
fs.mkdirSync(vectorStoreDir, { recursive: true });
|
|
582
|
+
fs.writeFileSync(saltPath, JSON.stringify({ algorithm: 'pbkdf2', salt: salt.toString('base64'), iterations: PBKDF2_KDF_SPEC.iterations }), 'utf8');
|
|
583
|
+
}
|
|
584
|
+
return deriveKeysFromPassphrase(passphraseInput, salt, PBKDF2_KDF_SPEC).vaultKey;
|
|
585
|
+
}
|
|
586
|
+
if (options.safeStorage && typeof options.safeStorage.encryptString === 'function' && typeof options.safeStorage.decryptString === 'function') {
|
|
587
|
+
if (fs.existsSync(vectorStoreKeyPath)) {
|
|
588
|
+
const encData = JSON.parse(fs.readFileSync(vectorStoreKeyPath, 'utf8'));
|
|
589
|
+
if (encData.scheme !== 'enigma.safestorage.v1' || !encData.encryptedHex) {
|
|
590
|
+
throw new Error('Corrupted safeStorage vector key custody file');
|
|
591
|
+
}
|
|
592
|
+
const plainHex = options.safeStorage.decryptString(Buffer.from(encData.encryptedHex, 'hex'));
|
|
593
|
+
return Buffer.from(plainHex, 'hex');
|
|
594
|
+
} else {
|
|
595
|
+
const freshKey = crypto.randomBytes(32);
|
|
596
|
+
const encrypted = options.safeStorage.encryptString(freshKey.toString('hex'));
|
|
597
|
+
fs.mkdirSync(vectorStoreDir, { recursive: true });
|
|
598
|
+
fs.writeFileSync(vectorStoreKeyPath, JSON.stringify({
|
|
599
|
+
scheme: 'enigma.safestorage.v1',
|
|
600
|
+
encryptedHex: Buffer.from(encrypted).toString('hex'),
|
|
601
|
+
createdAt: Date.now(),
|
|
602
|
+
}, null, 2), 'utf8');
|
|
603
|
+
return freshKey;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (options.allowHostBoundDemo === true || process.env.ENIGMA_ALLOW_HOSTBOUND === '1') {
|
|
607
|
+
const hostPayload = `${crypto.createHash('sha256').update(bundlePath).digest('hex')}:demo_host_key`;
|
|
608
|
+
return crypto.scryptSync(hostPayload, 'enigma.hostbound.vector.salt', 32);
|
|
609
|
+
}
|
|
610
|
+
if (!requiresPersistentKey) {
|
|
611
|
+
return crypto.randomBytes(32);
|
|
612
|
+
}
|
|
613
|
+
throw new Error(
|
|
614
|
+
'Vector store key custody not configured: provide safeStorage (Electron), a vaultKey, a passphrase (or ENIGMA_PASSPHRASE), or set allowHostBoundDemo:true for local CLI demos.'
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
let masterVectorKey = null;
|
|
619
|
+
if (requiresPersistentKey) {
|
|
620
|
+
masterVectorKey = getOrDeriveVectorMasterKey();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const ownsVectorStore = !options.vectorStore || options.ownVectorStore === true;
|
|
624
|
+
const ownsEmbeddingProvider = !options.embeddingProvider || options.ownEmbeddingProvider === true;
|
|
625
|
+
let vectorStore = options.vectorStore || new EncryptedVectorStore({
|
|
626
|
+
quantize: true,
|
|
627
|
+
...(options.embeddingProvider ? { embeddingProvider: options.embeddingProvider } : {}),
|
|
628
|
+
});
|
|
629
|
+
if (shouldPersistVectorStore && fs.existsSync(vectorStorePath)) {
|
|
630
|
+
try {
|
|
631
|
+
const encryptedBundle = JSON.parse(fs.readFileSync(vectorStorePath, 'utf8'));
|
|
632
|
+
vectorStore = EncryptedVectorStore.decryptFromVault(encryptedBundle, masterVectorKey, {
|
|
633
|
+
...(options.embeddingProvider || options.vectorStore?.embedder
|
|
634
|
+
? { embeddingProvider: options.embeddingProvider || options.vectorStore.embedder }
|
|
635
|
+
: {}),
|
|
636
|
+
});
|
|
637
|
+
} catch (err) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
`Failed to decrypt desktop vector store at "${vectorStorePath}": ${err.message}. `
|
|
640
|
+
+ 'The existing encrypted file has been preserved intact. '
|
|
641
|
+
+ 'Provide the original passphrase via ENIGMA_PASSPHRASE or use --passphrase-file to unlock.'
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function persistVectorStore() {
|
|
647
|
+
if (!shouldPersistVectorStore || !vectorStorePath) return;
|
|
648
|
+
const bundle = vectorStore.encryptForVault(masterVectorKey);
|
|
649
|
+
fs.mkdirSync(path.dirname(vectorStorePath), { recursive: true });
|
|
650
|
+
const tmp = `${vectorStorePath}.tmp.${crypto.randomBytes(3).toString('hex')}`;
|
|
651
|
+
fs.writeFileSync(tmp, JSON.stringify(bundle, null, 2), 'utf8');
|
|
652
|
+
fs.renameSync(tmp, vectorStorePath);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const modelAdapters = createDesktopModelAdapterRegistry(options);
|
|
656
|
+
const platformRuntime = options.platformRuntime && typeof options.platformRuntime === 'object'
|
|
657
|
+
? options.platformRuntime
|
|
658
|
+
: createEnvironmentPlatformRuntime({ usage });
|
|
659
|
+
const platformHttp = createInferenceHttpHandler({
|
|
660
|
+
...platformRuntime,
|
|
661
|
+
authorize: async (req) => authenticateApiRequest(req, requiredApiScopes(new URL(req.url, `http://${host}:${boundPort}`))),
|
|
662
|
+
});
|
|
663
|
+
const x402Accepts = options.x402Accepts || (process.env.ENIGMA_X402_ACCEPTS_JSON ? JSON.parse(process.env.ENIGMA_X402_ACCEPTS_JSON) : null);
|
|
664
|
+
const x402ResourceBaseUrl = options.x402ResourceBaseUrl || process.env.ENIGMA_X402_RESOURCE_BASE_URL || null;
|
|
665
|
+
const x402Facilitator = options.x402Facilitator || (process.env.ENIGMA_X402_FACILITATOR_URL
|
|
666
|
+
? createX402FacilitatorClient({
|
|
667
|
+
baseUrl: process.env.ENIGMA_X402_FACILITATOR_URL,
|
|
668
|
+
apiKey: process.env.ENIGMA_X402_FACILITATOR_API_KEY || null,
|
|
669
|
+
})
|
|
670
|
+
: null);
|
|
671
|
+
const x402ConfiguredParts = [x402Accepts, x402ResourceBaseUrl, x402Facilitator].filter(Boolean).length;
|
|
672
|
+
if (x402ConfiguredParts !== 0 && x402ConfiguredParts !== 3) {
|
|
673
|
+
throw new Error('x402 chat requires accepts, resource base URL, and facilitator configuration together');
|
|
674
|
+
}
|
|
675
|
+
const x402Http = x402ConfiguredParts === 3
|
|
676
|
+
? createX402ResourceHandler({
|
|
677
|
+
facilitator: x402Facilitator,
|
|
678
|
+
routes: [{
|
|
679
|
+
method: 'POST',
|
|
680
|
+
path: '/v1/x402/chat/completions',
|
|
681
|
+
resource: {
|
|
682
|
+
url: new URL('/v1/x402/chat/completions', x402ResourceBaseUrl).toString(),
|
|
683
|
+
description: 'Enigma private-routing chat completion',
|
|
684
|
+
mimeType: 'application/json',
|
|
685
|
+
serviceName: 'Enigma',
|
|
686
|
+
tags: ['ai', 'privacy'],
|
|
687
|
+
},
|
|
688
|
+
accepts: x402Accepts,
|
|
689
|
+
execute: async ({ rawBody }) => {
|
|
690
|
+
if (!platformRuntime.chat) throw new Error('chat runtime is not configured');
|
|
691
|
+
let payload;
|
|
692
|
+
try { payload = JSON.parse(rawBody.toString('utf8') || '{}'); }
|
|
693
|
+
catch { throw new TypeError('x402 chat body must be valid JSON'); }
|
|
694
|
+
return { body: await platformRuntime.chat.complete(payload) };
|
|
695
|
+
},
|
|
696
|
+
}],
|
|
697
|
+
})
|
|
698
|
+
: async () => false;
|
|
699
|
+
const sdkOptions = options.sdk && typeof options.sdk === 'object' ? options.sdk : {};
|
|
700
|
+
const federationOptions = options.federation && typeof options.federation === 'object'
|
|
701
|
+
? options.federation
|
|
702
|
+
: {};
|
|
703
|
+
const sdkScope = String(sdkOptions.scope || 'enigma.desktop').trim();
|
|
704
|
+
const sdkStoragePath = shouldPersistSdkState
|
|
705
|
+
? path.resolve(sdkOptions.storagePath || path.join(vectorStoreDir, 'desktop_agent_state.enc.json'))
|
|
706
|
+
: null;
|
|
707
|
+
const operationalStatePath = shouldPersistSdkState
|
|
708
|
+
? path.resolve(sdkOptions.operationalStatePath || path.join(vectorStoreDir, 'desktop_sdk_runtime.enc.json'))
|
|
709
|
+
: null;
|
|
710
|
+
const sdkVaultKey = masterVectorKey
|
|
711
|
+
? Buffer.from(crypto.hkdfSync(
|
|
712
|
+
'sha256',
|
|
713
|
+
masterVectorKey,
|
|
714
|
+
Buffer.from('enigma.desktop.sdk.salt.v1', 'utf8'),
|
|
715
|
+
Buffer.from('enigma.desktop.sdk.v1', 'utf8'),
|
|
716
|
+
32
|
|
717
|
+
))
|
|
718
|
+
: crypto.randomBytes(32);
|
|
719
|
+
const operationalKey = Buffer.from(crypto.hkdfSync(
|
|
720
|
+
'sha256',
|
|
721
|
+
sdkVaultKey,
|
|
722
|
+
Buffer.from('enigma.desktop.operational.salt.v1', 'utf8'),
|
|
723
|
+
Buffer.from('enigma.desktop.operational.v1', 'utf8'),
|
|
724
|
+
32
|
|
725
|
+
));
|
|
726
|
+
const operationalAad = Buffer.from('enigma.desktop.sdk_runtime.v1', 'utf8');
|
|
727
|
+
let operationalState = null;
|
|
728
|
+
let sdkPromise = null;
|
|
729
|
+
let federationBridgePromise = null;
|
|
730
|
+
let federationBridge = null;
|
|
731
|
+
let federationTransport = null;
|
|
732
|
+
let agentTurnQueue = Promise.resolve();
|
|
733
|
+
const federationEvents = [];
|
|
734
|
+
|
|
735
|
+
function serializeIdentity(identity) {
|
|
736
|
+
return {
|
|
737
|
+
scope: identity.scope,
|
|
738
|
+
publicKey: identity.publicKey.toString('base64'),
|
|
739
|
+
secretKey: identity.secretKey.toString('base64'),
|
|
740
|
+
encryptionPublicKey: identity.encryptionPublicKey.toString('base64'),
|
|
741
|
+
encryptionSecretKey: identity.encryptionSecretKey.toString('base64'),
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function deserializeIdentity(value) {
|
|
746
|
+
if (!value || typeof value !== 'object') throw new Error('Desktop SDK identity state is missing');
|
|
747
|
+
const identity = {
|
|
748
|
+
scope: String(value.scope || ''),
|
|
749
|
+
publicKey: Buffer.from(value.publicKey || '', 'base64'),
|
|
750
|
+
secretKey: Buffer.from(value.secretKey || '', 'base64'),
|
|
751
|
+
encryptionPublicKey: Buffer.from(value.encryptionPublicKey || '', 'base64'),
|
|
752
|
+
encryptionSecretKey: Buffer.from(value.encryptionSecretKey || '', 'base64'),
|
|
753
|
+
};
|
|
754
|
+
for (const [name, key] of Object.entries(identity)) {
|
|
755
|
+
if (name !== 'scope' && key.length !== 32) {
|
|
756
|
+
throw new Error(`Desktop SDK identity ${name} must be exactly 32 bytes`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (identity.scope !== sdkScope) {
|
|
760
|
+
throw new Error(`Desktop SDK identity scope "${identity.scope}" does not match "${sdkScope}"`);
|
|
761
|
+
}
|
|
762
|
+
identity.destination = deriveDestination(
|
|
763
|
+
identity.publicKey,
|
|
764
|
+
identity.encryptionPublicKey,
|
|
765
|
+
identity.scope
|
|
766
|
+
);
|
|
767
|
+
identity.destinationHex = identity.destination.toString('hex');
|
|
768
|
+
return identity;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function readOperationalState() {
|
|
772
|
+
if (operationalState) return operationalState;
|
|
773
|
+
if (!operationalStatePath || !fs.existsSync(operationalStatePath)) {
|
|
774
|
+
operationalState = {
|
|
775
|
+
schema: 'enigma.desktop.sdk_runtime.v1',
|
|
776
|
+
identity: serializeIdentity(generateMeshIdentity(sdkScope)),
|
|
777
|
+
federation: null,
|
|
778
|
+
};
|
|
779
|
+
return operationalState;
|
|
780
|
+
}
|
|
781
|
+
try {
|
|
782
|
+
const envelope = JSON.parse(fs.readFileSync(operationalStatePath, 'utf8'));
|
|
783
|
+
if (envelope?.schema !== 'enigma.desktop.encrypted_sdk_runtime.v1') {
|
|
784
|
+
throw new Error('unsupported encrypted runtime schema');
|
|
785
|
+
}
|
|
786
|
+
const decipher = crypto.createDecipheriv(
|
|
787
|
+
'aes-256-gcm',
|
|
788
|
+
operationalKey,
|
|
789
|
+
Buffer.from(envelope.iv, 'base64')
|
|
790
|
+
);
|
|
791
|
+
decipher.setAAD(operationalAad);
|
|
792
|
+
decipher.setAuthTag(Buffer.from(envelope.authTag, 'base64'));
|
|
793
|
+
const plaintext = Buffer.concat([
|
|
794
|
+
decipher.update(Buffer.from(envelope.ciphertext, 'base64')),
|
|
795
|
+
decipher.final(),
|
|
796
|
+
]);
|
|
797
|
+
const value = JSON.parse(plaintext.toString('utf8'));
|
|
798
|
+
if (value?.schema !== 'enigma.desktop.sdk_runtime.v1') {
|
|
799
|
+
throw new Error('invalid decrypted runtime schema');
|
|
800
|
+
}
|
|
801
|
+
deserializeIdentity(value.identity);
|
|
802
|
+
operationalState = value;
|
|
803
|
+
return operationalState;
|
|
804
|
+
} catch (error) {
|
|
805
|
+
throw new Error(
|
|
806
|
+
`Failed to decrypt desktop SDK runtime at "${operationalStatePath}": ${error.message}. `
|
|
807
|
+
+ 'The encrypted file has been preserved; restore the original desktop key custody.'
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function persistOperationalState() {
|
|
813
|
+
if (!operationalStatePath) return;
|
|
814
|
+
const state = readOperationalState();
|
|
815
|
+
if (federationBridge) state.federation = federationBridge.exportState();
|
|
816
|
+
const iv = crypto.randomBytes(12);
|
|
817
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', operationalKey, iv);
|
|
818
|
+
cipher.setAAD(operationalAad);
|
|
819
|
+
const ciphertext = Buffer.concat([
|
|
820
|
+
cipher.update(Buffer.from(JSON.stringify(state), 'utf8')),
|
|
821
|
+
cipher.final(),
|
|
822
|
+
]);
|
|
823
|
+
const envelope = {
|
|
824
|
+
schema: 'enigma.desktop.encrypted_sdk_runtime.v1',
|
|
825
|
+
iv: iv.toString('base64'),
|
|
826
|
+
authTag: cipher.getAuthTag().toString('base64'),
|
|
827
|
+
ciphertext: ciphertext.toString('base64'),
|
|
828
|
+
};
|
|
829
|
+
fs.mkdirSync(path.dirname(operationalStatePath), { recursive: true });
|
|
830
|
+
const temporaryPath = `${operationalStatePath}.tmp.${crypto.randomBytes(4).toString('hex')}`;
|
|
831
|
+
fs.writeFileSync(temporaryPath, JSON.stringify(envelope, null, 2), 'utf8');
|
|
832
|
+
fs.renameSync(temporaryPath, operationalStatePath);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function getSdk() {
|
|
836
|
+
if (!sdkPromise) {
|
|
837
|
+
const pending = (async () => {
|
|
838
|
+
const identity = deserializeIdentity(readOperationalState().identity);
|
|
839
|
+
const connectOptions = {
|
|
840
|
+
agentId: sdkOptions.agentId || 'enigma-desktop',
|
|
841
|
+
scope: sdkScope,
|
|
842
|
+
storagePath: sdkStoragePath,
|
|
843
|
+
vaultKey: sdkVaultKey,
|
|
844
|
+
identity,
|
|
845
|
+
...(sdkOptions.ragClient ? { ragClient: sdkOptions.ragClient } : {}),
|
|
846
|
+
...(sdkOptions.ownRagClient === true ? { ownRagClient: true } : {}),
|
|
847
|
+
};
|
|
848
|
+
const instance = sdkOptions.enigmaFactory
|
|
849
|
+
? await sdkOptions.enigmaFactory(connectOptions)
|
|
850
|
+
: await Enigma.connect(connectOptions);
|
|
851
|
+
persistOperationalState();
|
|
852
|
+
return instance;
|
|
853
|
+
})();
|
|
854
|
+
sdkPromise = pending;
|
|
855
|
+
pending.catch(() => {
|
|
856
|
+
if (sdkPromise === pending) sdkPromise = null;
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
return sdkPromise;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function configuredFederationPeers() {
|
|
863
|
+
const configured = federationOptions.registeredPeers ?? federationOptions.peerDescriptors ?? [];
|
|
864
|
+
if (configured instanceof Map) return [...configured.entries()];
|
|
865
|
+
if (Array.isArray(configured)) {
|
|
866
|
+
return configured.map((peer) => [peer?.peerId ?? peer?.peer_id, peer]);
|
|
867
|
+
}
|
|
868
|
+
if (configured && typeof configured === 'object') return Object.entries(configured);
|
|
869
|
+
throw new TypeError('federation registeredPeers must be an array, object, or Map');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function recordFederationEvent(kind, event) {
|
|
873
|
+
federationEvents.push({
|
|
874
|
+
kind,
|
|
875
|
+
at: new Date().toISOString(),
|
|
876
|
+
packetId: event?.packet_id ?? event?.packetId ?? null,
|
|
877
|
+
peerId: event?.fromPeerId ?? null,
|
|
878
|
+
});
|
|
879
|
+
if (federationEvents.length > 25) federationEvents.splice(0, federationEvents.length - 25);
|
|
880
|
+
persistOperationalState();
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async function getFederationBridge() {
|
|
884
|
+
if (!federationBridgePromise) {
|
|
885
|
+
const pending = (async () => {
|
|
886
|
+
const enigma = await getSdk();
|
|
887
|
+
federationTransport = federationOptions.transport || new FederationWebSocketTransport({
|
|
888
|
+
id: federationOptions.transportId || 'desktop-federation-ws',
|
|
889
|
+
host: '127.0.0.1',
|
|
890
|
+
port: federationOptions.port ?? 0,
|
|
891
|
+
peers: federationOptions.peerAddresses || [],
|
|
892
|
+
});
|
|
893
|
+
const bridge = new GhostMeshFederationBridge({
|
|
894
|
+
enigma,
|
|
895
|
+
transport: federationTransport,
|
|
896
|
+
scope: sdkScope,
|
|
897
|
+
manageTransport: federationOptions.manageTransport !== false,
|
|
898
|
+
requestTimeoutMs: federationOptions.requestTimeoutMs,
|
|
899
|
+
state: readOperationalState().federation || undefined,
|
|
900
|
+
});
|
|
901
|
+
for (const [peerId, peer] of configuredFederationPeers()) {
|
|
902
|
+
bridge.registerPeer(peerId, peer);
|
|
903
|
+
}
|
|
904
|
+
bridge.on('grant', (event) => recordFederationEvent('grant', event));
|
|
905
|
+
bridge.on('query', (event) => recordFederationEvent('query', event));
|
|
906
|
+
bridge.on('response', (event) => recordFederationEvent('response', event));
|
|
907
|
+
bridge.on('revocation', (event) => recordFederationEvent('revocation', event));
|
|
908
|
+
bridge.on('rejected', (event) => recordFederationEvent('rejected', event));
|
|
909
|
+
federationBridge = bridge;
|
|
910
|
+
return bridge;
|
|
911
|
+
})();
|
|
912
|
+
federationBridgePromise = pending;
|
|
913
|
+
pending.catch(() => {
|
|
914
|
+
if (federationBridgePromise === pending) federationBridgePromise = null;
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
return federationBridgePromise;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function federationStatus(bridge) {
|
|
921
|
+
const endpoint = bridge.localEndpoint;
|
|
922
|
+
const transport = typeof federationTransport?.getStats === 'function'
|
|
923
|
+
? federationTransport.getStats()
|
|
924
|
+
: {
|
|
925
|
+
id: federationTransport?.id || null,
|
|
926
|
+
running: Boolean(federationTransport?.running),
|
|
927
|
+
};
|
|
928
|
+
return {
|
|
929
|
+
ok: true,
|
|
930
|
+
schema: 'enigma.desktop.federation_status.v1',
|
|
931
|
+
started: bridge.started,
|
|
932
|
+
scope: bridge.scope,
|
|
933
|
+
endpoint: {
|
|
934
|
+
destinationHex: endpoint.destinationHex
|
|
935
|
+
|| deriveDestination(endpoint.publicKey, endpoint.encryptionPublicKey, bridge.scope).toString('hex'),
|
|
936
|
+
publicKeyHex: Buffer.from(endpoint.publicKey).toString('hex'),
|
|
937
|
+
encryptionPublicKeyHex: Buffer.from(endpoint.encryptionPublicKey).toString('hex'),
|
|
938
|
+
},
|
|
939
|
+
transport,
|
|
940
|
+
registeredPeers: [...bridge.peers.values()].map((peer) => ({
|
|
941
|
+
peerId: peer.peerId,
|
|
942
|
+
scope: peer.scope,
|
|
943
|
+
destinationHex: peer.destination.toString('hex'),
|
|
944
|
+
publicKeyHex: peer.publicKey.toString('hex'),
|
|
945
|
+
encryptionPublicKeyHex: peer.encryptionPublicKey.toString('hex'),
|
|
946
|
+
})),
|
|
947
|
+
activeGrantIds: [...bridge.knownGrants.keys()],
|
|
948
|
+
pendingQueries: bridge.pendingQueries.size,
|
|
949
|
+
recentEvents: federationEvents.slice().reverse(),
|
|
950
|
+
persisted: Boolean(operationalStatePath),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function enqueueAgentTurn(action) {
|
|
955
|
+
const pending = agentTurnQueue.then(action);
|
|
956
|
+
agentTurnQueue = pending.catch(() => {});
|
|
957
|
+
return pending;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const session = new TerminalSession({
|
|
961
|
+
bundlePath,
|
|
962
|
+
io: {
|
|
963
|
+
stdin: process.stdin,
|
|
964
|
+
stdout: stdoutCapture,
|
|
965
|
+
stderr: stderrCapture,
|
|
966
|
+
},
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
session.phantomBridge = phantomBridge;
|
|
970
|
+
phantomBridge.onWalletConnected = (wallet) => {
|
|
971
|
+
session.connectedWallet = wallet;
|
|
972
|
+
};
|
|
973
|
+
session.autoAnchorEngine = autoAnchorEngine;
|
|
974
|
+
|
|
975
|
+
let executeQueue = Promise.resolve();
|
|
976
|
+
let zkRuntimePromise = null;
|
|
977
|
+
let zkActionQueue = Promise.resolve();
|
|
978
|
+
|
|
979
|
+
function getZkRuntime() {
|
|
980
|
+
if (!zkRuntimePromise) {
|
|
981
|
+
const zkOptions = options.zk && typeof options.zk === 'object' ? options.zk : {};
|
|
982
|
+
const pending = createZkRuntime({
|
|
983
|
+
...zkOptions,
|
|
984
|
+
safeStorage: zkOptions.safeStorage ?? options.safeStorage,
|
|
985
|
+
statePath: zkOptions.statePath || defaultZkStatePath(bundlePath),
|
|
986
|
+
});
|
|
987
|
+
zkRuntimePromise = pending;
|
|
988
|
+
pending.catch(() => {
|
|
989
|
+
if (zkRuntimePromise === pending) zkRuntimePromise = null;
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
return zkRuntimePromise;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function enqueueZkAction(action) {
|
|
996
|
+
const pending = zkActionQueue.then(action);
|
|
997
|
+
zkActionQueue = pending.catch(() => {});
|
|
998
|
+
return pending;
|
|
999
|
+
}
|
|
1000
|
+
// --- GhostMesh P2P Mesh Subsystem -----------------------------------------
|
|
1001
|
+
let meshNode = null;
|
|
1002
|
+
const meshInbox = [];
|
|
1003
|
+
const meshSaleOffers = [];
|
|
1004
|
+
const meshZkProofs = [];
|
|
1005
|
+
|
|
1006
|
+
async function getMeshNode() {
|
|
1007
|
+
if (!meshNode) {
|
|
1008
|
+
const meshOpts = options.mesh && typeof options.mesh === 'object' ? options.mesh : {};
|
|
1009
|
+
const { MeshGossipNode, WebSocketTransport } = await import('../../../packages/mesh/src/index.js');
|
|
1010
|
+
meshNode = new MeshGossipNode({
|
|
1011
|
+
scope: meshOpts.scope || 'enigma.memory',
|
|
1012
|
+
nodeId: meshOpts.nodeId,
|
|
1013
|
+
});
|
|
1014
|
+
if (meshOpts.port !== undefined && meshOpts.port !== null) {
|
|
1015
|
+
const wsTransport = new WebSocketTransport({
|
|
1016
|
+
port: meshOpts.port,
|
|
1017
|
+
peers: meshOpts.peers || [],
|
|
1018
|
+
});
|
|
1019
|
+
meshNode.addTransport(wsTransport);
|
|
1020
|
+
}
|
|
1021
|
+
meshNode.on('capsule', (capsule) => {
|
|
1022
|
+
meshInbox.push({ ...capsule, receivedAt: new Date().toISOString() });
|
|
1023
|
+
});
|
|
1024
|
+
meshNode.on('sale:offer', (offer) => {
|
|
1025
|
+
meshSaleOffers.push({ ...offer, receivedAt: new Date().toISOString() });
|
|
1026
|
+
});
|
|
1027
|
+
meshNode.on('zk:proof', (proof) => {
|
|
1028
|
+
meshZkProofs.push({ ...proof, receivedAt: new Date().toISOString() });
|
|
1029
|
+
});
|
|
1030
|
+
await meshNode.start();
|
|
1031
|
+
}
|
|
1032
|
+
return meshNode;
|
|
1033
|
+
}
|
|
1034
|
+
// --- Custody-local proving + opaque relayer sessions -----------------------
|
|
1035
|
+
// Private proving inputs never enter the renderer. The custody runtime
|
|
1036
|
+
// proves locally, stores the public proof beside a single-use opaque handle,
|
|
1037
|
+
// and zeroizes private pending values after settlement, expiry, or eviction.
|
|
1038
|
+
const FR_MODULUS = BigInt('0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001');
|
|
1039
|
+
const PREPARE_TTL_MS = 10 * 60 * 1000;
|
|
1040
|
+
const PREPARE_MAX_SESSIONS = 32;
|
|
1041
|
+
const pendingPrepares = new Map();
|
|
1042
|
+
const relayInFlight = new Set();
|
|
1043
|
+
const scrubPrepareValue = (value) => {
|
|
1044
|
+
if (!value || typeof value !== 'object') return;
|
|
1045
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
1046
|
+
value.fill(0);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
for (const key of Object.keys(value)) {
|
|
1050
|
+
const entry = value[key];
|
|
1051
|
+
if (entry && typeof entry === 'object') scrubPrepareValue(entry);
|
|
1052
|
+
else if (typeof entry === 'string') value[key] = '';
|
|
1053
|
+
else if (typeof entry === 'bigint') value[key] = 0n;
|
|
1054
|
+
else if (typeof entry === 'number') value[key] = 0;
|
|
1055
|
+
else value[key] = null;
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
const clearPendingPrepare = (entry) => {
|
|
1059
|
+
if (!entry) return;
|
|
1060
|
+
scrubPrepareValue(entry.pending);
|
|
1061
|
+
entry.pending = null;
|
|
1062
|
+
entry.proofBytes = null;
|
|
1063
|
+
entry.publicInputs = null;
|
|
1064
|
+
};
|
|
1065
|
+
// Witness material the relayer must NEVER receive; presence is a hard 400.
|
|
1066
|
+
const RELAY_FORBIDDEN_FIELDS = [
|
|
1067
|
+
'ownerKey', 'rho', 'payloadCommit', 'witness', 'witnessInput',
|
|
1068
|
+
'noteOpenings', 'recipientSecretKeyB64', 'secretKey', 'plaintext',
|
|
1069
|
+
];
|
|
1070
|
+
const PROVER_ASSET_ROUTE = {
|
|
1071
|
+
'action_transition.wasm': { key: 'wasm', contentType: 'application/wasm' },
|
|
1072
|
+
'action_transition.zkey': { key: 'zkey', contentType: 'application/octet-stream' },
|
|
1073
|
+
'snarkjs.min.js': { key: 'snarkjs', contentType: 'text/javascript; charset=utf-8' },
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
async function proverAssetFiles(runtime) {
|
|
1077
|
+
const zkOptions = options.zk && typeof options.zk === 'object' ? options.zk : {};
|
|
1078
|
+
// wasm/zkey: via the checksum-pinned resolver when available (packaged
|
|
1079
|
+
// installs lack circuits/build); injected-deps tests keep the fixture paths.
|
|
1080
|
+
const assets = typeof runtime.getProvingAssetPaths === 'function'
|
|
1081
|
+
? await runtime.getProvingAssetPaths()
|
|
1082
|
+
: runtime.artifactPaths;
|
|
1083
|
+
let snarkjs = null;
|
|
1084
|
+
if (zkOptions.snarkjsBundlePath) {
|
|
1085
|
+
snarkjs = path.resolve(zkOptions.snarkjsBundlePath);
|
|
1086
|
+
} else {
|
|
1087
|
+
// snarkjs is a declared runtime dependency: resolve its package entry
|
|
1088
|
+
// and take the sibling IIFE bundle (build/snarkjs.min.js).
|
|
1089
|
+
try {
|
|
1090
|
+
const req = createRequire(import.meta.url);
|
|
1091
|
+
const main = req.resolve('snarkjs');
|
|
1092
|
+
const candidate = path.join(path.dirname(main), 'snarkjs.min.js');
|
|
1093
|
+
if (fs.existsSync(candidate)) snarkjs = candidate;
|
|
1094
|
+
} catch { /* fall through to legacy walk */ }
|
|
1095
|
+
if (!snarkjs) {
|
|
1096
|
+
let dir = path.dirname(assets.zkey);
|
|
1097
|
+
for (let i = 0; i < 4 && !snarkjs; i += 1) {
|
|
1098
|
+
dir = path.dirname(dir);
|
|
1099
|
+
const candidate = path.join(dir, 'node_modules', 'snarkjs', 'build', 'snarkjs.min.js');
|
|
1100
|
+
if (fs.existsSync(candidate)) snarkjs = candidate;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return { wasm: assets.wasm, zkey: assets.zkey, snarkjs };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function validateRelayShape(proof, publicInputs, expectedInputCount) {
|
|
1108
|
+
if (!proof || typeof proof !== 'object' || Array.isArray(proof)) {
|
|
1109
|
+
return 'proof must be an object { a, b, c } of hex strings (64/128/64 bytes)';
|
|
1110
|
+
}
|
|
1111
|
+
const expectedHexLength = { a: 128, b: 256, c: 128 };
|
|
1112
|
+
for (const [key, hexLength] of Object.entries(expectedHexLength)) {
|
|
1113
|
+
const value = proof[key];
|
|
1114
|
+
if (typeof value !== 'string' || !/^[0-9a-fA-F]+$/.test(value) || value.length !== hexLength) {
|
|
1115
|
+
return `proof.${key} must be a ${hexLength}-character hex string (${hexLength / 2} bytes)`;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (!Array.isArray(publicInputs) || publicInputs.length !== expectedInputCount) {
|
|
1119
|
+
return `publicInputs must be an array of ${expectedInputCount} × 64-hex field elements`;
|
|
1120
|
+
}
|
|
1121
|
+
for (let i = 0; i < publicInputs.length; i += 1) {
|
|
1122
|
+
const value = publicInputs[i];
|
|
1123
|
+
if (typeof value !== 'string' || !/^[0-9a-fA-F]{64}$/.test(value)) {
|
|
1124
|
+
return `publicInputs[${i}] must be a 64-character hex string`;
|
|
1125
|
+
}
|
|
1126
|
+
if (BigInt(`0x${value}`) >= FR_MODULUS) {
|
|
1127
|
+
return `publicInputs[${i}] is not a canonical BN254 scalar field element`;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
// --- Sealed Sale (ZKCP) marketplace helpers --------------------------------
|
|
1135
|
+
// SOVEREIGNTY INVARIANT: no user secret (ownerKey/rho/DEK/buyerSecret/
|
|
1136
|
+
// plaintext) may cross these endpoints. Proving happens in this process
|
|
1137
|
+
// against the checksum-pinned sealed_sale_release artifacts; the wire
|
|
1138
|
+
// carries only { proof bytes, 6 public inputs, pubkeys, terms } and public
|
|
1139
|
+
// commitments/ciphertext. The envelope triple leaves custody only in claim.
|
|
1140
|
+
const SALE_FORBIDDEN_FIELDS = [
|
|
1141
|
+
'ownerKey', 'rho', 'dekLo', 'dekHi', 'buyerSecret', 'secretKey',
|
|
1142
|
+
'recipientSecretKeyB64', 'noteOpenings', 'noteContext', 'witness',
|
|
1143
|
+
'witnessInput', 'plaintext', 'esk', 'payloadCommit',
|
|
1144
|
+
'epk', 'encDek', 'nonce', 'nonceHex',
|
|
1145
|
+
];
|
|
1146
|
+
const saleForbiddenField = (payload) => {
|
|
1147
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return 'request body must be a JSON object';
|
|
1148
|
+
for (const field of SALE_FORBIDDEN_FIELDS) {
|
|
1149
|
+
if (field in payload) {
|
|
1150
|
+
return `sale endpoints accept only proof/public-input/commitment material — secret field "${field}" must never be sent`;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return null;
|
|
1154
|
+
};
|
|
1155
|
+
const parseHex32 = (value, name) => {
|
|
1156
|
+
if (typeof value !== 'string' || !/^[0-9a-fA-F]{64}$/.test(value)) {
|
|
1157
|
+
return { error: `${name} must be a 64-character hex string (32 bytes)` };
|
|
1158
|
+
}
|
|
1159
|
+
return { bytes: Buffer.from(value, 'hex') };
|
|
1160
|
+
};
|
|
1161
|
+
const parseUint64 = (value, name) => {
|
|
1162
|
+
if (typeof value !== 'string' && typeof value !== 'number') return { error: `${name} must be an integer (string or number)` };
|
|
1163
|
+
if (!/^(0|[1-9][0-9]*)$/.test(String(value))) return { error: `${name} must be a non-negative integer` };
|
|
1164
|
+
const parsed = BigInt(value);
|
|
1165
|
+
if (parsed >= (1n << 64n)) return { error: `${name} is out of u64 range` };
|
|
1166
|
+
return { value: parsed };
|
|
1167
|
+
};
|
|
1168
|
+
const parseCiphertextChunks = (value) => {
|
|
1169
|
+
if (!Array.isArray(value) || value.length !== 32) {
|
|
1170
|
+
return { error: 'ciphertextChunks must be an array of 32 decimal-string field elements' };
|
|
1171
|
+
}
|
|
1172
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1173
|
+
const chunk = value[i];
|
|
1174
|
+
if (typeof chunk !== 'string' || !/^(0|[1-9][0-9]*)$/.test(chunk)) {
|
|
1175
|
+
return { error: `ciphertextChunks[${i}] must be a decimal string` };
|
|
1176
|
+
}
|
|
1177
|
+
if (BigInt(chunk) >= FR_MODULUS) {
|
|
1178
|
+
return { error: `ciphertextChunks[${i}] is not a canonical BN254 scalar field element` };
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return { chunks: value };
|
|
1182
|
+
};
|
|
1183
|
+
const parsePredecessorReceiptRefs = (value) => {
|
|
1184
|
+
if (value === undefined) return { refs: [] };
|
|
1185
|
+
if (!Array.isArray(value) || value.length > 16) {
|
|
1186
|
+
return { error: 'predecessorReceiptRefs must be an array of at most 16 ZK settlement receipt refs' };
|
|
1187
|
+
}
|
|
1188
|
+
if (value.some((ref) => typeof ref !== 'string' || !/^zksettle_[a-f0-9]{64}$/.test(ref))) {
|
|
1189
|
+
return { error: 'predecessorReceiptRefs contains an invalid ZK settlement receipt ref' };
|
|
1190
|
+
}
|
|
1191
|
+
return { refs: [...new Set(value)] };
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
const SALE_ERROR_STATUS = {
|
|
1195
|
+
ZK_UNAVAILABLE: 503,
|
|
1196
|
+
BAD_REQUEST: 400,
|
|
1197
|
+
VK_GATE_REJECTED: 409,
|
|
1198
|
+
PROOF_STATE_MISMATCH: 409,
|
|
1199
|
+
NOTE_OPENING_MISSING: 409,
|
|
1200
|
+
NOTE_NOT_SALEABLE: 409,
|
|
1201
|
+
BUYER_KEY_MISSING: 409,
|
|
1202
|
+
SALE_ENVELOPE_MISSING: 409,
|
|
1203
|
+
SALE_ENVELOPE_MISMATCH: 409,
|
|
1204
|
+
SALE_NOT_CLAIMED: 409,
|
|
1205
|
+
SALE_ALREADY_CLAIMED: 409,
|
|
1206
|
+
SALE_TERMS_MISMATCH: 409,
|
|
1207
|
+
SALE_TERMS_NOT_BUYER_BOUND: 409,
|
|
1208
|
+
SALE_CIPHER_MISMATCH: 409,
|
|
1209
|
+
SALE_RECEIPT_MISSING: 404,
|
|
1210
|
+
SALE_OPEN_REJECTED: 422,
|
|
1211
|
+
};
|
|
1212
|
+
const saleErrorStatus = (err) => SALE_ERROR_STATUS[err.code]
|
|
1213
|
+
?? (err.name === 'ProvingAssetError' ? 501 : 500);
|
|
1214
|
+
const receiptJson = (receipt) => ({
|
|
1215
|
+
saleTermsHash: Buffer.from(receipt.saleTermsHash).toString('hex'),
|
|
1216
|
+
keyCommit: Buffer.from(receipt.keyCommit).toString('hex'),
|
|
1217
|
+
cipherCommit: Buffer.from(receipt.cipherCommit).toString('hex'),
|
|
1218
|
+
envelopeCommit: Buffer.from(receipt.envelopeCommit).toString('hex'),
|
|
1219
|
+
payloadCommitPos: Buffer.from(receipt.payloadCommitPos).toString('hex'),
|
|
1220
|
+
epk: Buffer.from(receipt.epk).toString('hex'),
|
|
1221
|
+
encDek: Buffer.from(receipt.encDek).toString('hex'),
|
|
1222
|
+
nonce: Buffer.from(receipt.nonce).toString('hex'),
|
|
1223
|
+
slot: receipt.slot,
|
|
1224
|
+
claimed: !(receipt.epk.every((b) => b === 0) && receipt.encDek.every((b) => b === 0) && receipt.nonce.every((b) => b === 0)),
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
function writeJson(res, statusCode, value) {
|
|
1228
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1229
|
+
res.end(JSON.stringify(value));
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function enqueueCommandExecution(commandLine) {
|
|
1233
|
+
const executePromise = executeQueue.then(async () => {
|
|
1234
|
+
stdoutCapture.clear();
|
|
1235
|
+
stderrCapture.clear();
|
|
1236
|
+
const exitCode = await session.executeCommand(commandLine);
|
|
1237
|
+
const output = stdoutCapture.buffer + (stderrCapture.buffer ? '\n' + stderrCapture.buffer : '');
|
|
1238
|
+
return {
|
|
1239
|
+
ok: exitCode === 0 || exitCode === 'EXIT',
|
|
1240
|
+
exitCode: exitCode === 'EXIT' ? 0 : (exitCode || 0),
|
|
1241
|
+
output: output || '(no output)',
|
|
1242
|
+
connectedWallet: session.connectedWallet || null,
|
|
1243
|
+
};
|
|
1244
|
+
});
|
|
1245
|
+
executeQueue = executePromise.catch(() => {});
|
|
1246
|
+
return executePromise;
|
|
1247
|
+
}
|
|
1248
|
+
function validateSecurityHeaders(req, res, { requireAuth = false } = {}) {
|
|
1249
|
+
const rawHost = req.headers['host'];
|
|
1250
|
+
if (!rawHost) {
|
|
1251
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1252
|
+
res.end(JSON.stringify({ ok: false, error: 'host header required' }));
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
let parsedHost;
|
|
1257
|
+
try {
|
|
1258
|
+
parsedHost = new URL(`http://${rawHost}`);
|
|
1259
|
+
} catch {
|
|
1260
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1261
|
+
res.end(JSON.stringify({ ok: false, error: 'malformed host header' }));
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const hostName = parsedHost.hostname.toLowerCase();
|
|
1266
|
+
const hostPort = Number(parsedHost.port || 80);
|
|
1267
|
+
const validHostName = hostName === '127.0.0.1' || hostName === 'localhost';
|
|
1268
|
+
const validHostPort = boundPort === 0 || hostPort === boundPort;
|
|
1269
|
+
|
|
1270
|
+
if (!validHostName || !validHostPort) {
|
|
1271
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1272
|
+
res.end(JSON.stringify({ ok: false, error: 'host header rejected: must be exact 127.0.0.1 or localhost on bound port' }));
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const rawOrigin = req.headers['origin'];
|
|
1277
|
+
if (rawOrigin) {
|
|
1278
|
+
let parsedOrigin;
|
|
1279
|
+
try {
|
|
1280
|
+
parsedOrigin = new URL(rawOrigin);
|
|
1281
|
+
} catch {
|
|
1282
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1283
|
+
res.end(JSON.stringify({ ok: false, error: 'malformed origin header' }));
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
const originName = parsedOrigin.hostname.toLowerCase();
|
|
1288
|
+
const originPort = Number(parsedOrigin.port || (parsedOrigin.protocol === 'https:' ? 443 : 80));
|
|
1289
|
+
const validOriginName = originName === '127.0.0.1' || originName === 'localhost';
|
|
1290
|
+
const validOriginPort = boundPort === 0 || originPort === boundPort;
|
|
1291
|
+
|
|
1292
|
+
if (!validOriginName || !validOriginPort) {
|
|
1293
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1294
|
+
res.end(JSON.stringify({ ok: false, error: 'cross-origin request rejected' }));
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (requireAuth) {
|
|
1300
|
+
const token = req.headers['x-enigma-auth-token'];
|
|
1301
|
+
if (!token || token !== authToken) {
|
|
1302
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1303
|
+
res.end(JSON.stringify({ ok: false, error: 'unauthorized or invalid session token' }));
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function readBody(req, res, callback) {
|
|
1312
|
+
let body = '';
|
|
1313
|
+
let bytes = 0;
|
|
1314
|
+
let rejected = false;
|
|
1315
|
+
|
|
1316
|
+
req.on('data', (chunk) => {
|
|
1317
|
+
if (rejected) return;
|
|
1318
|
+
bytes += chunk.length;
|
|
1319
|
+
if (bytes > MAX_BODY_BYTES) {
|
|
1320
|
+
rejected = true;
|
|
1321
|
+
req.pause();
|
|
1322
|
+
res.writeHead(413, {
|
|
1323
|
+
'Content-Type': 'application/json',
|
|
1324
|
+
'Connection': 'close',
|
|
1325
|
+
});
|
|
1326
|
+
res.end(JSON.stringify({ ok: false, error: 'payload too large' }));
|
|
1327
|
+
req.resume();
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
body += chunk;
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
req.on('end', () => {
|
|
1334
|
+
if (!rejected) {
|
|
1335
|
+
callback(body);
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function writeOperationalError(res, error, fallbackCode, fallbackStatus = 500) {
|
|
1341
|
+
const statusCode = error instanceof DesktopOperationalError
|
|
1342
|
+
? error.statusCode
|
|
1343
|
+
: fallbackStatus;
|
|
1344
|
+
writeJson(res, statusCode, {
|
|
1345
|
+
ok: false,
|
|
1346
|
+
error: {
|
|
1347
|
+
code: error instanceof DesktopOperationalError ? error.code : fallbackCode,
|
|
1348
|
+
message: error?.message || 'Desktop operational request failed',
|
|
1349
|
+
},
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const server = http.createServer(async (req, res) => {
|
|
1354
|
+
// Apply host validation before serving any request (including static files/token)
|
|
1355
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: false })) return;
|
|
1356
|
+
|
|
1357
|
+
const url = new URL(req.url, `http://${host}:${boundPort}`);
|
|
1358
|
+
|
|
1359
|
+
const origin = req.headers['origin'];
|
|
1360
|
+
if (origin) {
|
|
1361
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
1362
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
1363
|
+
res.setHeader('Access-Control-Expose-Headers', 'PAYMENT-REQUIRED, PAYMENT-RESPONSE');
|
|
1364
|
+
res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, X-Enigma-Auth-Token, PAYMENT-SIGNATURE');
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (req.method === 'OPTIONS') {
|
|
1368
|
+
res.writeHead(204);
|
|
1369
|
+
res.end();
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if (await apiKeyHttp(req, res)) return;
|
|
1373
|
+
if (await creditsHttp(req, res)) return;
|
|
1374
|
+
if (await paymentHttp(req, res)) return;
|
|
1375
|
+
if (await x402Http(req, res, url)) return;
|
|
1376
|
+
if (await usageHttp(req, res)) return;
|
|
1377
|
+
if (await usage.run({ principal: null }, () => platformHttp.handle(req, res, url))) return;
|
|
1378
|
+
if (url.pathname === '/api/health' && req.method === 'GET') {
|
|
1379
|
+
writeJson(res, 200, {
|
|
1380
|
+
ok: true,
|
|
1381
|
+
schema: 'enigma.desktop.health.v1',
|
|
1382
|
+
service: 'enigma-desktop',
|
|
1383
|
+
version: ENIGMA_VERSION,
|
|
1384
|
+
instanceId,
|
|
1385
|
+
authRequired: true,
|
|
1386
|
+
});
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
if (url.pathname === '/api/agent/adapters' && req.method === 'GET') {
|
|
1392
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1393
|
+
writeJson(res, 200, {
|
|
1394
|
+
ok: true,
|
|
1395
|
+
schema: 'enigma.desktop.model_adapters.v1',
|
|
1396
|
+
configured: modelAdapters.size > 0,
|
|
1397
|
+
adapters: [...modelAdapters.values()].map(({
|
|
1398
|
+
id,
|
|
1399
|
+
provider,
|
|
1400
|
+
models,
|
|
1401
|
+
contextCarriersByModel,
|
|
1402
|
+
}) => ({
|
|
1403
|
+
id,
|
|
1404
|
+
provider,
|
|
1405
|
+
models,
|
|
1406
|
+
contextCarriersByModel,
|
|
1407
|
+
})),
|
|
1408
|
+
});
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
if (url.pathname === '/api/agent/turn' && req.method === 'POST') {
|
|
1413
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1414
|
+
readBody(req, res, async (body) => {
|
|
1415
|
+
let payload;
|
|
1416
|
+
try {
|
|
1417
|
+
payload = JSON.parse(body || '{}');
|
|
1418
|
+
} catch {
|
|
1419
|
+
writeOperationalError(
|
|
1420
|
+
res,
|
|
1421
|
+
new DesktopOperationalError('INVALID_JSON', 'request body must be valid JSON', 400),
|
|
1422
|
+
'AGENT_TURN_FAILED'
|
|
1423
|
+
);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
try {
|
|
1427
|
+
const prompt = typeof payload.prompt === 'string' ? payload.prompt.trim() : '';
|
|
1428
|
+
if (!prompt) {
|
|
1429
|
+
throw new DesktopOperationalError(
|
|
1430
|
+
'AGENT_PROMPT_REQUIRED',
|
|
1431
|
+
'prompt must be a non-empty string',
|
|
1432
|
+
400
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
const adapter = resolveDesktopModelAdapter(modelAdapters, payload);
|
|
1436
|
+
const selectedModel = String(payload.model).trim();
|
|
1437
|
+
const contextCarrier = resolveDesktopContextCarrierRequest(payload, adapter, selectedModel);
|
|
1438
|
+
const privacyMode = resolveDesktopPrivacyMode(payload);
|
|
1439
|
+
if (privacyMode === 'anchor-guard' && contextCarrier && contextCarrier.mode !== 'text') {
|
|
1440
|
+
throw new DesktopOperationalError(
|
|
1441
|
+
'PRIVACY_CARRIER_UNSUPPORTED',
|
|
1442
|
+
'Anchor Guard requires a text context carrier so protected anchors cannot be embedded in an image',
|
|
1443
|
+
422
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
const anchorGuard = createDesktopAnchorGuard(privacyMode);
|
|
1447
|
+
let privacyReport = null;
|
|
1448
|
+
const enigma = await getSdk();
|
|
1449
|
+
const turn = await enqueueAgentTurn(() => enigma.runAgentTurn({
|
|
1450
|
+
prompt,
|
|
1451
|
+
retrieval: payload.retrieval && typeof payload.retrieval === 'object'
|
|
1452
|
+
? payload.retrieval
|
|
1453
|
+
: {},
|
|
1454
|
+
rememberResponse: payload.rememberResponse !== false,
|
|
1455
|
+
...(contextCarrier ? { contextCarrier } : {}),
|
|
1456
|
+
metadata: {
|
|
1457
|
+
...(payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : {}),
|
|
1458
|
+
desktopModel: {
|
|
1459
|
+
adapterId: adapter.id,
|
|
1460
|
+
provider: adapter.provider,
|
|
1461
|
+
model: selectedModel,
|
|
1462
|
+
},
|
|
1463
|
+
},
|
|
1464
|
+
invoke: async (invocation) => {
|
|
1465
|
+
try {
|
|
1466
|
+
const protectedInvocation = privacyMode === 'off'
|
|
1467
|
+
? invocation
|
|
1468
|
+
: anchorGuard.redactValue(invocation);
|
|
1469
|
+
privacyReport = anchorGuard.report(protectedInvocation);
|
|
1470
|
+
const adapterResult = await adapter.invoke({
|
|
1471
|
+
...protectedInvocation,
|
|
1472
|
+
adapterId: adapter.id,
|
|
1473
|
+
provider: adapter.provider,
|
|
1474
|
+
model: selectedModel,
|
|
1475
|
+
});
|
|
1476
|
+
const sanitizedResult = sanitizeDesktopAdapterResult(adapterResult);
|
|
1477
|
+
return privacyMode === 'off'
|
|
1478
|
+
? sanitizedResult
|
|
1479
|
+
: anchorGuard.restoreValue(sanitizedResult);
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
throw new DesktopOperationalError(
|
|
1482
|
+
'MODEL_ADAPTER_INVOCATION_FAILED',
|
|
1483
|
+
`Configured model adapter "${adapter.id}" failed: ${error.message}`,
|
|
1484
|
+
502
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
},
|
|
1488
|
+
}));
|
|
1489
|
+
const contextRefs = turn.context.items.map((item) => ({
|
|
1490
|
+
memoryId: item.id,
|
|
1491
|
+
commitment: item.commitment || null,
|
|
1492
|
+
receiptRef: item.metadata?.agentTurn?.receiptRef
|
|
1493
|
+
|| item.metadata?.updateReceiptRef
|
|
1494
|
+
|| null,
|
|
1495
|
+
}));
|
|
1496
|
+
writeJson(res, 200, {
|
|
1497
|
+
ok: true,
|
|
1498
|
+
schema: 'enigma.desktop.agent_turn.v1',
|
|
1499
|
+
adapter: {
|
|
1500
|
+
id: adapter.id,
|
|
1501
|
+
provider: adapter.provider,
|
|
1502
|
+
model: selectedModel,
|
|
1503
|
+
},
|
|
1504
|
+
result: turn.result,
|
|
1505
|
+
context: {
|
|
1506
|
+
...(turn.contextCarrier ? {} : { contextText: turn.context.contextText }),
|
|
1507
|
+
refs: contextRefs,
|
|
1508
|
+
estimatedTokens: turn.context.estimatedTokens,
|
|
1509
|
+
},
|
|
1510
|
+
memory: turn.memory
|
|
1511
|
+
? { id: turn.memory.id, scope: turn.memory.scope, type: turn.memory.type }
|
|
1512
|
+
: null,
|
|
1513
|
+
receiptRefs: turn.receipts.refs,
|
|
1514
|
+
...(turn.contextCarrier ? { contextCarrier: turn.contextCarrier } : {}),
|
|
1515
|
+
...(privacyReport ? { privacyReport } : {}),
|
|
1516
|
+
});
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
writeOperationalError(res, error, 'AGENT_TURN_FAILED');
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
if (url.pathname === '/api/mesh/federation/status' && req.method === 'GET') {
|
|
1525
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1526
|
+
try {
|
|
1527
|
+
const bridge = await getFederationBridge();
|
|
1528
|
+
writeJson(res, 200, federationStatus(bridge));
|
|
1529
|
+
} catch (error) {
|
|
1530
|
+
writeOperationalError(res, error, 'FEDERATION_BRIDGE_FAILED');
|
|
1531
|
+
}
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
if (url.pathname === '/api/mesh/federation/start' && req.method === 'POST') {
|
|
1536
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1537
|
+
try {
|
|
1538
|
+
const bridge = await getFederationBridge();
|
|
1539
|
+
await bridge.start();
|
|
1540
|
+
persistOperationalState();
|
|
1541
|
+
writeJson(res, 200, federationStatus(bridge));
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
writeOperationalError(res, error, 'FEDERATION_BRIDGE_FAILED');
|
|
1544
|
+
}
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
// Real Command Execution API Endpoint
|
|
1550
|
+
if ((url.pathname === '/api/exec' || url.pathname === '/api/terminal/exec') && req.method === 'POST') {
|
|
1551
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1552
|
+
|
|
1553
|
+
readBody(req, res, async (body) => {
|
|
1554
|
+
try {
|
|
1555
|
+
const payload = JSON.parse(body || '{}');
|
|
1556
|
+
const commandLine = String(payload.command || '').trim();
|
|
1557
|
+
|
|
1558
|
+
if (!commandLine) {
|
|
1559
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1560
|
+
res.end(JSON.stringify({ ok: true, output: '', exitCode: 0 }));
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const result = await enqueueCommandExecution(commandLine);
|
|
1565
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1566
|
+
res.end(JSON.stringify(result));
|
|
1567
|
+
} catch (err) {
|
|
1568
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1569
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// ZK Proof Layer status. This endpoint is intentionally read-only and
|
|
1576
|
+
// fail-closed: operational failures are always a 200 JSON shape with
|
|
1577
|
+
// available=false, never an HTML error page.
|
|
1578
|
+
if (url.pathname === '/api/zk/status' && req.method === 'GET') {
|
|
1579
|
+
try {
|
|
1580
|
+
const runtime = await getZkRuntime();
|
|
1581
|
+
const result = await runtime.status();
|
|
1582
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1583
|
+
res.end(JSON.stringify({ ok: true, ...result }));
|
|
1584
|
+
} catch (err) {
|
|
1585
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1586
|
+
res.end(JSON.stringify({
|
|
1587
|
+
ok: true,
|
|
1588
|
+
available: false,
|
|
1589
|
+
reason: `ZK runtime initialization failed: ${err.message}`,
|
|
1590
|
+
cluster: options.zk?.cluster || 'localnet',
|
|
1591
|
+
sharedStatePda: null,
|
|
1592
|
+
programId: null,
|
|
1593
|
+
noteRoot: null,
|
|
1594
|
+
nullRoot: null,
|
|
1595
|
+
noteCount: null,
|
|
1596
|
+
nullCount: null,
|
|
1597
|
+
localNoteCount: null,
|
|
1598
|
+
localNullCount: null,
|
|
1599
|
+
provingArtifacts: false,
|
|
1600
|
+
honesty: ZK_HONESTY_NOTE,
|
|
1601
|
+
}));
|
|
1602
|
+
}
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// The prove + settle flow blocks honestly for the full local proof
|
|
1607
|
+
// (normally ~2s) and confirmed Solana transaction. Actions are serialized
|
|
1608
|
+
// so two requests can never mutate the same local tree snapshot.
|
|
1609
|
+
if (url.pathname === '/api/zk/append' && req.method === 'POST') {
|
|
1610
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1611
|
+
readBody(req, res, async (body) => {
|
|
1612
|
+
let payload;
|
|
1613
|
+
try {
|
|
1614
|
+
payload = JSON.parse(body || '{}');
|
|
1615
|
+
} catch {
|
|
1616
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1617
|
+
res.end(JSON.stringify({ ok: false, error: 'request body must be valid JSON' }));
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
const plaintext = typeof payload.plaintext === 'string' ? payload.plaintext : '';
|
|
1621
|
+
const scope = typeof payload.scope === 'string' ? payload.scope.trim() : '';
|
|
1622
|
+
if (!plaintext.trim()) {
|
|
1623
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1624
|
+
res.end(JSON.stringify({ ok: false, error: 'plaintext must be a non-empty string' }));
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
if (!scope) {
|
|
1629
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1630
|
+
res.end(JSON.stringify({ ok: false, error: 'scope must be a non-empty string' }));
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
// saleable: commit the DEK-blinded plaintext fold (ZKCP listable)
|
|
1634
|
+
// instead of the E2EE payloadCommit; see /api/zk/sale/* below.
|
|
1635
|
+
const saleable = payload.saleable === true;
|
|
1636
|
+
try {
|
|
1637
|
+
const runtime = await getZkRuntime();
|
|
1638
|
+
const result = await enqueueZkAction(() => runtime.append({ plaintext, scope, saleable }));
|
|
1639
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1640
|
+
res.end(JSON.stringify({ ok: true, ...result }));
|
|
1641
|
+
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
const statusCode = err.code === 'ZK_UNAVAILABLE' ? 503
|
|
1644
|
+
: err.code === 'BAD_REQUEST' ? 400
|
|
1645
|
+
: err.code === 'PROOF_STATE_MISMATCH' ? 409
|
|
1646
|
+
: err.code === 'VK_GATE_REJECTED' ? 409
|
|
1647
|
+
: err.name === 'ProvingAssetError' ? 501
|
|
1648
|
+
: 500;
|
|
1649
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1650
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (url.pathname === '/api/zk/revoke' && req.method === 'POST') {
|
|
1657
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1658
|
+
readBody(req, res, async (body) => {
|
|
1659
|
+
let payload;
|
|
1660
|
+
try {
|
|
1661
|
+
payload = JSON.parse(body || '{}');
|
|
1662
|
+
} catch {
|
|
1663
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1664
|
+
res.end(JSON.stringify({ ok: false, error: 'request body must be valid JSON' }));
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
const noteIndex = Number(payload.noteIndex);
|
|
1668
|
+
if (!Number.isInteger(noteIndex) || noteIndex < 0) {
|
|
1669
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1670
|
+
res.end(JSON.stringify({ ok: false, error: 'noteIndex must be a non-negative integer' }));
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
try {
|
|
1674
|
+
const runtime = await getZkRuntime();
|
|
1675
|
+
const result = await enqueueZkAction(() => runtime.revoke({ noteIndex }));
|
|
1676
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1677
|
+
res.end(JSON.stringify({ ok: true, ...result }));
|
|
1678
|
+
|
|
1679
|
+
} catch (err) {
|
|
1680
|
+
const statusCode = err.code === 'ZK_UNAVAILABLE' ? 503
|
|
1681
|
+
: err.code === 'NOTE_OPENING_MISSING' ? 409
|
|
1682
|
+
: err.code === 'PROOF_STATE_MISMATCH' ? 409
|
|
1683
|
+
: err.code === 'VK_GATE_REJECTED' ? 409
|
|
1684
|
+
: err.name === 'ProvingAssetError' ? 501
|
|
1685
|
+
: 500;
|
|
1686
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1687
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (url.pathname === '/api/zk/sync-leaves' && req.method === 'POST') {
|
|
1693
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1694
|
+
readBody(req, res, async (body) => {
|
|
1695
|
+
let payload;
|
|
1696
|
+
try {
|
|
1697
|
+
payload = JSON.parse(body || '{}');
|
|
1698
|
+
} catch {
|
|
1699
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1700
|
+
res.end(JSON.stringify({ ok: false, error: 'request body must be valid JSON' }));
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
if (!Array.isArray(payload.leaves)) {
|
|
1704
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1705
|
+
res.end(JSON.stringify({ ok: false, error: 'leaves must be an array of leaf strings' }));
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
try {
|
|
1709
|
+
const runtime = await getZkRuntime();
|
|
1710
|
+
const result = await enqueueZkAction(() => runtime.syncNoteLeaves(payload.leaves));
|
|
1711
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1712
|
+
res.end(JSON.stringify({ ok: true, ...result }));
|
|
1713
|
+
} catch (err) {
|
|
1714
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1715
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
// --- GhostMesh Endpoints ---
|
|
1721
|
+
if (url.pathname === '/api/mesh/status' && req.method === 'GET') {
|
|
1722
|
+
try {
|
|
1723
|
+
const node = await getMeshNode();
|
|
1724
|
+
writeJson(res, 200, node.getStatus());
|
|
1725
|
+
} catch (err) {
|
|
1726
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
1727
|
+
}
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
if (url.pathname === '/api/mesh/peers' && req.method === 'GET') {
|
|
1731
|
+
try {
|
|
1732
|
+
const node = await getMeshNode();
|
|
1733
|
+
const peers = typeof node.getPeers === 'function' ? node.getPeers() : [];
|
|
1734
|
+
writeJson(res, 200, { ok: true, count: peers.length, peers });
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
1737
|
+
}
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
if (url.pathname === '/api/mesh/announce' && req.method === 'POST') {
|
|
1742
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1743
|
+
readBody(req, res, async (body) => {
|
|
1744
|
+
let payload = {};
|
|
1745
|
+
try { payload = JSON.parse(body || '{}'); } catch {}
|
|
1746
|
+
try {
|
|
1747
|
+
const node = await getMeshNode();
|
|
1748
|
+
const packet = await node.announce(payload.metadata || {});
|
|
1749
|
+
writeJson(res, 200, { ok: true, packetId: packet.subarray(packet.length - 16).toString('hex') });
|
|
1750
|
+
} catch (err) {
|
|
1751
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
if (url.pathname === '/api/mesh/send' && req.method === 'POST') {
|
|
1758
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1759
|
+
readBody(req, res, async (body) => {
|
|
1760
|
+
let payload;
|
|
1761
|
+
try { payload = JSON.parse(body || '{}'); } catch {
|
|
1762
|
+
writeJson(res, 400, { ok: false, error: 'request body must be valid JSON' });
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
const recipientPub = payload.recipientX25519Pub || payload.recipientEd25519Pub;
|
|
1766
|
+
if (!payload.destination || !recipientPub || !payload.memory) {
|
|
1767
|
+
writeJson(res, 400, { ok: false, error: 'destination, recipientX25519Pub, and memory are required' });
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
try {
|
|
1771
|
+
const node = await getMeshNode();
|
|
1772
|
+
const result = await node.sendEncryptedCapsule(
|
|
1773
|
+
payload.destination,
|
|
1774
|
+
Buffer.from(recipientPub, 'hex'),
|
|
1775
|
+
payload.memory,
|
|
1776
|
+
payload.options || {}
|
|
1777
|
+
);
|
|
1778
|
+
writeJson(res, 200, { ok: true, ...result });
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
if (url.pathname === '/api/mesh/gossip-proof' && req.method === 'POST') {
|
|
1787
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1788
|
+
readBody(req, res, async (body) => {
|
|
1789
|
+
let payload;
|
|
1790
|
+
try { payload = JSON.parse(body || '{}'); } catch {
|
|
1791
|
+
writeJson(res, 400, { ok: false, error: 'request body must be valid JSON' });
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
try {
|
|
1795
|
+
const node = await getMeshNode();
|
|
1796
|
+
const packet = await node.broadcastZkProof(payload);
|
|
1797
|
+
writeJson(res, 200, { ok: true, packetId: packet.subarray(packet.length - 16).toString('hex') });
|
|
1798
|
+
} catch (err) {
|
|
1799
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
if (url.pathname === '/api/mesh/gossip-sale' && req.method === 'POST') {
|
|
1806
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1807
|
+
readBody(req, res, async (body) => {
|
|
1808
|
+
let payload;
|
|
1809
|
+
try { payload = JSON.parse(body || '{}'); } catch {
|
|
1810
|
+
writeJson(res, 400, { ok: false, error: 'request body must be valid JSON' });
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
try {
|
|
1814
|
+
const node = await getMeshNode();
|
|
1815
|
+
const packet = await node.broadcastSealedSaleOffer(payload);
|
|
1816
|
+
writeJson(res, 200, { ok: true, packetId: packet.subarray(packet.length - 16).toString('hex') });
|
|
1817
|
+
} catch (err) {
|
|
1818
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
1819
|
+
}
|
|
1820
|
+
});
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
if (url.pathname === '/api/mesh/inbox' && req.method === 'GET') {
|
|
1825
|
+
writeJson(res, 200, {
|
|
1826
|
+
ok: true,
|
|
1827
|
+
capsules: meshInbox,
|
|
1828
|
+
saleOffers: meshSaleOffers,
|
|
1829
|
+
zkProofs: meshZkProofs,
|
|
1830
|
+
});
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
if (url.pathname === '/api/rag/remember' && req.method === 'POST') {
|
|
1835
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1836
|
+
readBody(req, res, async (body) => {
|
|
1837
|
+
let payload = {};
|
|
1838
|
+
try { payload = JSON.parse(body || '{}'); } catch {}
|
|
1839
|
+
if (!payload.text) {
|
|
1840
|
+
writeJson(res, 400, { ok: false, error: 'text is required' });
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
const atomId = payload.id || `atom_${Date.now()}_${crypto.randomBytes(3).toString('hex')}`;
|
|
1844
|
+
const result = await vectorStore.add(atomId, payload.text, {
|
|
1845
|
+
scope: payload.scope || 'enigma.global',
|
|
1846
|
+
importance: payload.importance || 1.0,
|
|
1847
|
+
...payload.metadata,
|
|
1848
|
+
});
|
|
1849
|
+
persistVectorStore();
|
|
1850
|
+
writeJson(res, 200, { ok: true, id: atomId, ...result });
|
|
1851
|
+
});
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
if (url.pathname === '/api/rag/recall' && req.method === 'POST') {
|
|
1856
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1857
|
+
readBody(req, res, async (body) => {
|
|
1858
|
+
let payload = {};
|
|
1859
|
+
try { payload = JSON.parse(body || '{}'); } catch {}
|
|
1860
|
+
if (!payload.query) {
|
|
1861
|
+
writeJson(res, 400, { ok: false, error: 'query is required' });
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
const results = await vectorStore.search(payload.query, {
|
|
1865
|
+
topK: payload.topK || 5,
|
|
1866
|
+
threshold: payload.threshold,
|
|
1867
|
+
scope: payload.scope,
|
|
1868
|
+
});
|
|
1869
|
+
writeJson(res, 200, { ok: true, results, count: results.length });
|
|
1870
|
+
});
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
if (url.pathname === '/api/rag/compile-context' && req.method === 'POST') {
|
|
1875
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1876
|
+
readBody(req, res, async (body) => {
|
|
1877
|
+
let payload = {};
|
|
1878
|
+
try { payload = JSON.parse(body || '{}'); } catch {}
|
|
1879
|
+
if (!payload.query) {
|
|
1880
|
+
writeJson(res, 400, { ok: false, error: 'query is required' });
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
const pack = await vectorStore.compileContextPack(payload.query, {
|
|
1884
|
+
maxTokens: payload.maxTokens || 2000,
|
|
1885
|
+
topK: payload.topK || 8,
|
|
1886
|
+
scope: payload.scope,
|
|
1887
|
+
});
|
|
1888
|
+
writeJson(res, 200, { ok: true, ...pack });
|
|
1889
|
+
});
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
// Browser-proving asset manifest. Public ceremony artifacts (no auth), but
|
|
1893
|
+
// fail closed with a clean 404 JSON when any artifact is missing.
|
|
1894
|
+
if (url.pathname === '/api/zk/prover-assets' && req.method === 'GET') {
|
|
1895
|
+
try {
|
|
1896
|
+
const runtime = await getZkRuntime();
|
|
1897
|
+
const files = await proverAssetFiles(runtime);
|
|
1898
|
+
const missing = Object.entries(files).filter(([, file]) => !file || !fs.existsSync(file)).map(([name]) => name);
|
|
1899
|
+
if (missing.length > 0) {
|
|
1900
|
+
writeJson(res, 404, {
|
|
1901
|
+
ok: false,
|
|
1902
|
+
error: `proving artifacts missing: ${missing.map((name) => files[name]).join(', ')}`,
|
|
1903
|
+
});
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
writeJson(res, 200, {
|
|
1907
|
+
ok: true,
|
|
1908
|
+
circuit: 'action_transition',
|
|
1909
|
+
wasmUrl: '/api/zk/prover-assets/action_transition.wasm',
|
|
1910
|
+
zkeyUrl: '/api/zk/prover-assets/action_transition.zkey',
|
|
1911
|
+
snarkjsUrl: '/api/zk/prover-assets/snarkjs.min.js',
|
|
1912
|
+
sizeBytes: {
|
|
1913
|
+
wasm: fs.statSync(files.wasm).size,
|
|
1914
|
+
zkey: fs.statSync(files.zkey).size,
|
|
1915
|
+
snarkjs: fs.statSync(files.snarkjs).size,
|
|
1916
|
+
},
|
|
1917
|
+
});
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
1920
|
+
}
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// Streams one whitelisted prover artifact (WASM / zkey / snarkjs bundle).
|
|
1925
|
+
if (url.pathname.startsWith('/api/zk/prover-assets/') && req.method === 'GET') {
|
|
1926
|
+
const name = decodeURIComponent(url.pathname.slice('/api/zk/prover-assets/'.length));
|
|
1927
|
+
const route = PROVER_ASSET_ROUTE[name];
|
|
1928
|
+
if (!route) {
|
|
1929
|
+
writeJson(res, 404, { ok: false, error: 'unknown prover asset' });
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
try {
|
|
1933
|
+
const runtime = await getZkRuntime();
|
|
1934
|
+
const file = (await proverAssetFiles(runtime))[route.key];
|
|
1935
|
+
if (!file || !fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
|
1936
|
+
writeJson(res, 404, { ok: false, error: `proving artifact not available: ${name}` });
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
res.writeHead(200, {
|
|
1940
|
+
'Content-Type': route.contentType,
|
|
1941
|
+
'Content-Length': fs.statSync(file).size,
|
|
1942
|
+
'Cache-Control': 'no-store',
|
|
1943
|
+
});
|
|
1944
|
+
fs.createReadStream(file).pipe(res);
|
|
1945
|
+
} catch (err) {
|
|
1946
|
+
writeJson(res, 500, { ok: false, error: err.message });
|
|
1947
|
+
}
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// Split phase 1: build and prove entirely inside local custody. The
|
|
1952
|
+
// renderer receives only a single-use opaque handle and public-safe
|
|
1953
|
+
// status metadata; private proving inputs never cross the HTTP boundary.
|
|
1954
|
+
if (url.pathname === '/api/zk/prepare' && req.method === 'POST') {
|
|
1955
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
1956
|
+
readBody(req, res, async (body) => {
|
|
1957
|
+
let payload;
|
|
1958
|
+
try {
|
|
1959
|
+
payload = JSON.parse(body || '{}');
|
|
1960
|
+
} catch {
|
|
1961
|
+
writeJson(res, 400, { ok: false, error: 'request body must be valid JSON' });
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
const mode = typeof payload.mode === 'string' ? payload.mode : '';
|
|
1965
|
+
if (mode !== 'append' && mode !== 'revoke') {
|
|
1966
|
+
writeJson(res, 400, { ok: false, error: 'mode must be "append" or "revoke"' });
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
let args;
|
|
1970
|
+
if (mode === 'append') {
|
|
1971
|
+
const plaintext = typeof payload.plaintext === 'string' ? payload.plaintext : '';
|
|
1972
|
+
const scope = typeof payload.scope === 'string' ? payload.scope.trim() : '';
|
|
1973
|
+
if (!plaintext.trim()) {
|
|
1974
|
+
writeJson(res, 400, { ok: false, error: 'plaintext must be a non-empty string' });
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
if (!scope) {
|
|
1978
|
+
writeJson(res, 400, { ok: false, error: 'scope must be a non-empty string' });
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
args = { plaintext, scope, saleable: payload.saleable === true };
|
|
1983
|
+
} else {
|
|
1984
|
+
const noteIndex = Number(payload.noteIndex);
|
|
1985
|
+
if (!Number.isInteger(noteIndex) || noteIndex < 0) {
|
|
1986
|
+
writeJson(res, 400, { ok: false, error: 'noteIndex must be a non-negative integer' });
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
args = { noteIndex };
|
|
1990
|
+
}
|
|
1991
|
+
try {
|
|
1992
|
+
const runtime = await getZkRuntime();
|
|
1993
|
+
const prepared = await enqueueZkAction(() => (mode === 'append'
|
|
1994
|
+
? runtime.prepareAppend(args)
|
|
1995
|
+
: runtime.prepareRevoke(args)));
|
|
1996
|
+
const proved = await runtime.provePreparedAction(prepared.witnessInput);
|
|
1997
|
+
const now = Date.now();
|
|
1998
|
+
for (const [id, entry] of pendingPrepares) {
|
|
1999
|
+
if (entry.expiresAt <= now) {
|
|
2000
|
+
clearPendingPrepare(entry);
|
|
2001
|
+
pendingPrepares.delete(id);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
while (pendingPrepares.size >= PREPARE_MAX_SESSIONS) {
|
|
2005
|
+
const oldestId = pendingPrepares.keys().next().value;
|
|
2006
|
+
clearPendingPrepare(pendingPrepares.get(oldestId));
|
|
2007
|
+
pendingPrepares.delete(oldestId);
|
|
2008
|
+
}
|
|
2009
|
+
const prepareId = crypto.randomBytes(16).toString('hex');
|
|
2010
|
+
pendingPrepares.set(prepareId, {
|
|
2011
|
+
mode,
|
|
2012
|
+
pending: prepared.pending,
|
|
2013
|
+
proofBytes: proved.proofBytes,
|
|
2014
|
+
publicInputs: proved.publicInputs,
|
|
2015
|
+
expiresAt: now + PREPARE_TTL_MS,
|
|
2016
|
+
});
|
|
2017
|
+
writeJson(res, 200, {
|
|
2018
|
+
ok: true,
|
|
2019
|
+
prepareId,
|
|
2020
|
+
mode,
|
|
2021
|
+
noteIndex: mode === 'revoke' ? args.noteIndex : null,
|
|
2022
|
+
proofReady: true,
|
|
2023
|
+
publicInputCount: proved.publicInputs.length,
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
|
|
2027
|
+
} catch (err) {
|
|
2028
|
+
const statusCode = err.code === 'ZK_UNAVAILABLE' ? 503
|
|
2029
|
+
: err.code === 'NOTE_OPENING_MISSING' ? 409
|
|
2030
|
+
: err.code === 'BAD_REQUEST' ? 400
|
|
2031
|
+
: err.code === 'VK_GATE_REJECTED' ? 409
|
|
2032
|
+
: err.name === 'ProvingAssetError' ? 501
|
|
2033
|
+
: 500;
|
|
2034
|
+
writeJson(res, statusCode, { ok: false, error: err.message });
|
|
2035
|
+
}
|
|
2036
|
+
});
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// Split phase 2: append/revoke accept only { mode, prepareId }; the proof
|
|
2041
|
+
// stays attached to the opaque server-side session. Intent mode has no
|
|
2042
|
+
// private note opening and continues to accept public proof material.
|
|
2043
|
+
// The server payer settles, and one in-flight action per mode is allowed.
|
|
2044
|
+
if (url.pathname === '/api/zk/relay' && req.method === 'POST') {
|
|
2045
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2046
|
+
readBody(req, res, async (body) => {
|
|
2047
|
+
let payload;
|
|
2048
|
+
try {
|
|
2049
|
+
payload = JSON.parse(body || '{}');
|
|
2050
|
+
} catch {
|
|
2051
|
+
writeJson(res, 400, { ok: false, error: 'request body must be valid JSON' });
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
2055
|
+
writeJson(res, 400, { ok: false, error: 'request body must be a JSON object' });
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
for (const field of RELAY_FORBIDDEN_FIELDS) {
|
|
2059
|
+
if (field in payload) {
|
|
2060
|
+
writeJson(res, 400, {
|
|
2061
|
+
ok: false,
|
|
2062
|
+
error: `relay request contains forbidden private material ("${field}")`,
|
|
2063
|
+
});
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
const mode = typeof payload.mode === 'string' ? payload.mode : '';
|
|
2068
|
+
if (mode !== 'append' && mode !== 'revoke' && mode !== 'intent') {
|
|
2069
|
+
writeJson(res, 400, { ok: false, error: 'mode must be "append", "revoke", or "intent"' });
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
if (mode === 'intent') {
|
|
2073
|
+
const shapeError = validateRelayShape(payload.proof, payload.publicInputs, 4);
|
|
2074
|
+
if (shapeError) {
|
|
2075
|
+
writeJson(res, 400, { ok: false, error: shapeError });
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
} else if ('proof' in payload || 'publicInputs' in payload) {
|
|
2079
|
+
writeJson(res, 400, {
|
|
2080
|
+
ok: false,
|
|
2081
|
+
error: 'append/revoke relay accepts only { mode, prepareId }; proof material remains in the opaque local prepare session',
|
|
2082
|
+
});
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
if (relayInFlight.has(mode)) {
|
|
2086
|
+
writeJson(res, 429, { ok: false, error: `relay already in flight for mode "${mode}"` });
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
let prepareEntry = null;
|
|
2090
|
+
if (mode !== 'intent') {
|
|
2091
|
+
const prepareId = typeof payload.prepareId === 'string' ? payload.prepareId : '';
|
|
2092
|
+
prepareEntry = prepareId ? pendingPrepares.get(prepareId) : null;
|
|
2093
|
+
if (!prepareEntry || prepareEntry.expiresAt <= Date.now()) {
|
|
2094
|
+
clearPendingPrepare(prepareEntry);
|
|
2095
|
+
pendingPrepares.delete(prepareId);
|
|
2096
|
+
writeJson(res, 409, { ok: false, error: 'unknown or expired prepare session; re-run /api/zk/prepare and prove again' });
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
if (prepareEntry.mode !== mode) {
|
|
2100
|
+
writeJson(res, 400, { ok: false, error: 'prepareId was issued for a different mode' });
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
relayInFlight.add(mode);
|
|
2105
|
+
try {
|
|
2106
|
+
const runtime = await getZkRuntime();
|
|
2107
|
+
let proofBytes;
|
|
2108
|
+
let publicInputs;
|
|
2109
|
+
if (mode === 'intent') {
|
|
2110
|
+
proofBytes = {
|
|
2111
|
+
a: Buffer.from(payload.proof.a, 'hex'),
|
|
2112
|
+
b: Buffer.from(payload.proof.b, 'hex'),
|
|
2113
|
+
c: Buffer.from(payload.proof.c, 'hex'),
|
|
2114
|
+
};
|
|
2115
|
+
publicInputs = payload.publicInputs.map((hex) => Buffer.from(hex, 'hex'));
|
|
2116
|
+
} else {
|
|
2117
|
+
proofBytes = prepareEntry.proofBytes;
|
|
2118
|
+
publicInputs = prepareEntry.publicInputs;
|
|
2119
|
+
}
|
|
2120
|
+
let result;
|
|
2121
|
+
if (mode === 'intent') {
|
|
2122
|
+
result = await runtime.relayIntent({ proofBytes, publicInputs });
|
|
2123
|
+
} else {
|
|
2124
|
+
pendingPrepares.delete(payload.prepareId); // prepare sessions are single-use
|
|
2125
|
+
result = await enqueueZkAction(() => runtime.relayAction({ pending: prepareEntry.pending, proofBytes, publicInputs }));
|
|
2126
|
+
}
|
|
2127
|
+
writeJson(res, 200, { ok: true, mode, ...result });
|
|
2128
|
+
|
|
2129
|
+
} catch (err) {
|
|
2130
|
+
const statusCode = err.code === 'ZK_UNAVAILABLE' ? 503
|
|
2131
|
+
: err.code === 'NOTE_OPENING_MISSING' ? 409
|
|
2132
|
+
: err.code === 'PROOF_STATE_MISMATCH' ? 409
|
|
2133
|
+
: err.code === 'VK_GATE_REJECTED' ? 409
|
|
2134
|
+
: err.name === 'ProvingAssetError' ? 501
|
|
2135
|
+
: 500;
|
|
2136
|
+
writeJson(res, statusCode, { ok: false, error: err.message });
|
|
2137
|
+
} finally {
|
|
2138
|
+
if (mode !== 'intent') clearPendingPrepare(prepareEntry);
|
|
2139
|
+
relayInFlight.delete(mode);
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
|
|
2146
|
+
// Phase 1 (seller): build the sale witness from local custody and prove
|
|
2147
|
+
// locally (~2s). Secrets stay server-side in memory for the request only.
|
|
2148
|
+
if (url.pathname === '/api/zk/sale/prepare-listing' && req.method === 'POST') {
|
|
2149
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2150
|
+
readBody(req, res, async (body) => {
|
|
2151
|
+
let payload;
|
|
2152
|
+
try {
|
|
2153
|
+
payload = JSON.parse(body || '{}');
|
|
2154
|
+
} catch {
|
|
2155
|
+
writeJson(res, 400, { ok: false, error: 'request body must be valid JSON' });
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
const forbidden = saleForbiddenField(payload);
|
|
2159
|
+
if (forbidden) {
|
|
2160
|
+
writeJson(res, 400, { ok: false, error: forbidden });
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
const noteIndex = Number(payload.noteIndex ?? payload.noteId);
|
|
2164
|
+
if (!Number.isInteger(noteIndex) || noteIndex < 0) {
|
|
2165
|
+
writeJson(res, 400, { ok: false, error: 'noteIndex (or noteId) must be a non-negative integer' });
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
const price = parseUint64(payload.priceLamports, 'priceLamports');
|
|
2169
|
+
if (price.error) {
|
|
2170
|
+
writeJson(res, 400, { ok: false, error: price.error });
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
const expirySlots = Number(payload.expirySlots);
|
|
2174
|
+
if (!Number.isInteger(expirySlots) || expirySlots < 1 || expirySlots > 2 ** 32) {
|
|
2175
|
+
writeJson(res, 400, { ok: false, error: 'expirySlots must be an integer between 1 and 2^32' });
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
let buyerPub = null;
|
|
2179
|
+
const hasBuyerXY = payload.buyerPubkeyX !== undefined || payload.buyerPubkeyY !== undefined;
|
|
2180
|
+
if (hasBuyerXY) {
|
|
2181
|
+
for (const key of ['buyerPubkeyX', 'buyerPubkeyY']) {
|
|
2182
|
+
if (typeof payload[key] !== 'string' || !/^(0|[1-9][0-9]*)$/.test(payload[key]) || BigInt(payload[key]) >= FR_MODULUS) {
|
|
2183
|
+
writeJson(res, 400, { ok: false, error: `${key} must be a canonical BN254 field element as a decimal string` });
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
buyerPub = { x: payload.buyerPubkeyX, y: payload.buyerPubkeyY };
|
|
2188
|
+
}
|
|
2189
|
+
const buyerKeyLabel = payload.buyerKeyLabel === undefined ? null : String(payload.buyerKeyLabel);
|
|
2190
|
+
if (!buyerPub && buyerKeyLabel !== null && !/^[a-z0-9_-]{1,64}$/i.test(buyerKeyLabel)) {
|
|
2191
|
+
writeJson(res, 400, { ok: false, error: 'buyerKeyLabel must be 1-64 alphanumeric/-/_ characters' });
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
const mint = payload.mint === undefined ? null : String(payload.mint);
|
|
2195
|
+
try {
|
|
2196
|
+
const runtime = await getZkRuntime();
|
|
2197
|
+
const result = await enqueueZkAction(() => runtime.prepareSaleListing({
|
|
2198
|
+
noteIndex,
|
|
2199
|
+
priceLamports: price.value,
|
|
2200
|
+
buyerPub,
|
|
2201
|
+
buyerKeyLabel,
|
|
2202
|
+
expirySlots,
|
|
2203
|
+
mint,
|
|
2204
|
+
}));
|
|
2205
|
+
writeJson(res, 200, { ok: true, ...result });
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
writeJson(res, saleErrorStatus(err), { ok: false, error: err.message, ...(err.name === 'ProvingAssetError' ? { code: err.code } : {}) });
|
|
2208
|
+
}
|
|
2209
|
+
});
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// Phase 2 (seller): attest on-chain. Accepts public { proofBytes,
|
|
2214
|
+
// publicInputs, predecessorReceiptRefs? }; the proof binds note state,
|
|
2215
|
+
// commitments, and terms while receipt refs bind lifecycle evidence.
|
|
2216
|
+
if (url.pathname === '/api/zk/sale/submit-listing' && req.method === 'POST') {
|
|
2217
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2218
|
+
readBody(req, res, async (body) => {
|
|
2219
|
+
let payload;
|
|
2220
|
+
try {
|
|
2221
|
+
payload = JSON.parse(body || '{}');
|
|
2222
|
+
} catch {
|
|
2223
|
+
writeJson(res, 400, { ok: false, error: 'request body must be valid JSON' });
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
const forbidden = saleForbiddenField(payload);
|
|
2227
|
+
if (forbidden) {
|
|
2228
|
+
writeJson(res, 400, { ok: false, error: forbidden });
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
const shapeError = validateRelayShape(payload.proofBytes, payload.publicInputs, 6);
|
|
2232
|
+
if (shapeError) {
|
|
2233
|
+
writeJson(res, 400, { ok: false, error: shapeError.replace(/^proof\./, 'proofBytes.') });
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
const predecessorRefs = parsePredecessorReceiptRefs(payload.predecessorReceiptRefs);
|
|
2237
|
+
if (predecessorRefs.error) {
|
|
2238
|
+
writeJson(res, 400, { ok: false, error: predecessorRefs.error });
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
try {
|
|
2242
|
+
const runtime = await getZkRuntime();
|
|
2243
|
+
const proofBytes = {
|
|
2244
|
+
a: Buffer.from(payload.proofBytes.a, 'hex'),
|
|
2245
|
+
b: Buffer.from(payload.proofBytes.b, 'hex'),
|
|
2246
|
+
c: Buffer.from(payload.proofBytes.c, 'hex'),
|
|
2247
|
+
};
|
|
2248
|
+
const publicInputs = payload.publicInputs.map((hex) => Buffer.from(hex, 'hex'));
|
|
2249
|
+
const result = await enqueueZkAction(() => runtime.submitSaleListing({
|
|
2250
|
+
proofBytes,
|
|
2251
|
+
publicInputs,
|
|
2252
|
+
predecessorReceiptRefs: predecessorRefs.refs,
|
|
2253
|
+
}));
|
|
2254
|
+
writeJson(res, 200, { ok: true, ...result });
|
|
2255
|
+
} catch (err) {
|
|
2256
|
+
writeJson(res, saleErrorStatus(err), { ok: false, error: err.message });
|
|
2257
|
+
}
|
|
2258
|
+
});
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
// Buyer pre-payment verification against the on-chain receipt. A failed
|
|
2263
|
+
// check is a verdict, not an error: 200 with ok:false and per-check detail.
|
|
2264
|
+
if (url.pathname === '/api/zk/sale/verify' && req.method === 'POST') {
|
|
2265
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2266
|
+
readBody(req, res, async (body) => {
|
|
2267
|
+
let payload;
|
|
2268
|
+
try {
|
|
2269
|
+
payload = JSON.parse(body || '{}');
|
|
2270
|
+
} catch {
|
|
2271
|
+
writeJson(res, 400, { ok: false, error: 'request body must be valid JSON' });
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
const forbidden = saleForbiddenField(payload);
|
|
2275
|
+
if (forbidden) {
|
|
2276
|
+
writeJson(res, 400, { ok: false, error: forbidden });
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
const terms = parseHex32(payload.saleTermsHashHex, 'saleTermsHashHex');
|
|
2280
|
+
const keyCommit = parseHex32(payload.keyCommitHex, 'keyCommitHex');
|
|
2281
|
+
const envelopeCommit = parseHex32(payload.envelopeCommitHex, 'envelopeCommitHex');
|
|
2282
|
+
const chunks = parseCiphertextChunks(payload.ciphertextChunks);
|
|
2283
|
+
for (const parsed of [terms, keyCommit, envelopeCommit, chunks]) {
|
|
2284
|
+
if (parsed.error) {
|
|
2285
|
+
writeJson(res, 400, { ok: false, error: parsed.error });
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
try {
|
|
2290
|
+
const runtime = await getZkRuntime();
|
|
2291
|
+
const result = await runtime.verifySale({
|
|
2292
|
+
saleTermsHashBytes: terms.bytes,
|
|
2293
|
+
keyCommitBytes: keyCommit.bytes,
|
|
2294
|
+
envelopeCommitBytes: envelopeCommit.bytes,
|
|
2295
|
+
ciphertextChunks: chunks.chunks,
|
|
2296
|
+
});
|
|
2297
|
+
writeJson(res, 200, result);
|
|
2298
|
+
} catch (err) {
|
|
2299
|
+
writeJson(res, saleErrorStatus(err), { ok: false, error: err.message });
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// Buyer escrow, pinned to the attested receipt; buyer keypair comes from
|
|
2306
|
+
// local custody and never appears on the wire.
|
|
2307
|
+
if (url.pathname === '/api/zk/sale/escrow' && req.method === 'POST') {
|
|
2308
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2309
|
+
readBody(req, res, async (body) => {
|
|
2310
|
+
let payload;
|
|
2311
|
+
try {
|
|
2312
|
+
payload = JSON.parse(body || '{}');
|
|
2313
|
+
} catch {
|
|
2314
|
+
writeJson(res, 400, { ok: false, error: 'request body must be valid JSON' });
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
const forbidden = saleForbiddenField(payload);
|
|
2318
|
+
if (forbidden) {
|
|
2319
|
+
writeJson(res, 400, { ok: false, error: forbidden });
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
const terms = parseHex32(payload.saleTermsHashHex, 'saleTermsHashHex');
|
|
2323
|
+
const keyCommit = parseHex32(payload.keyCommitHex, 'keyCommitHex');
|
|
2324
|
+
const envelopeCommit = parseHex32(payload.envelopeCommitHex, 'envelopeCommitHex');
|
|
2325
|
+
const price = parseUint64(payload.priceLamports, 'priceLamports');
|
|
2326
|
+
for (const parsed of [terms, keyCommit, envelopeCommit, price]) {
|
|
2327
|
+
if (parsed.error) {
|
|
2328
|
+
writeJson(res, 400, { ok: false, error: parsed.error });
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
const expirySlot = Number(payload.expirySlot);
|
|
2333
|
+
if (!Number.isSafeInteger(expirySlot) || expirySlot < 0) {
|
|
2334
|
+
writeJson(res, 400, { ok: false, error: 'expirySlot must be a non-negative safe integer' });
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
const sellerPubkey = typeof payload.sellerPubkey === 'string' ? payload.sellerPubkey.trim() : '';
|
|
2338
|
+
if (!sellerPubkey) {
|
|
2339
|
+
writeJson(res, 400, { ok: false, error: 'sellerPubkey must be a non-empty base58 string' });
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
const mint = typeof payload.mint === 'string' ? payload.mint.trim() : '';
|
|
2343
|
+
if (!mint) {
|
|
2344
|
+
writeJson(res, 400, { ok: false, error: 'mint must be a non-empty base58 string (terms recompute)' });
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
const payloadLen = Number(payload.payloadLen);
|
|
2348
|
+
if (!Number.isSafeInteger(payloadLen) || payloadLen < 1) {
|
|
2349
|
+
writeJson(res, 400, { ok: false, error: 'payloadLen must be a positive safe integer (terms recompute)' });
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
const escrowChunks = parseCiphertextChunks(payload.ciphertextChunks);
|
|
2353
|
+
if (escrowChunks.error) {
|
|
2354
|
+
writeJson(res, 400, { ok: false, error: escrowChunks.error });
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
try {
|
|
2358
|
+
const runtime = await getZkRuntime();
|
|
2359
|
+
const result = await enqueueZkAction(() => runtime.createSaleEscrow({
|
|
2360
|
+
saleTermsHashBytes: terms.bytes,
|
|
2361
|
+
priceLamports: price.value,
|
|
2362
|
+
expirySlot,
|
|
2363
|
+
sellerPubkey,
|
|
2364
|
+
mint,
|
|
2365
|
+
payloadLen,
|
|
2366
|
+
ciphertextChunks: escrowChunks.chunks,
|
|
2367
|
+
buyerKeyLabel: typeof payload.buyerKeyLabel === 'string' ? payload.buyerKeyLabel : undefined,
|
|
2368
|
+
keyCommitBytes: keyCommit.bytes,
|
|
2369
|
+
envelopeCommitBytes: envelopeCommit.bytes,
|
|
2370
|
+
}));
|
|
2371
|
+
writeJson(res, 200, { ok: true, ...result });
|
|
2372
|
+
} catch (err) {
|
|
2373
|
+
writeJson(res, saleErrorStatus(err), { ok: false, error: err.message });
|
|
2374
|
+
}
|
|
2375
|
+
});
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
|
|
2380
|
+
// Seller claim: loads the sealed envelope from custody and reveals it
|
|
2381
|
+
// on-chain atomically with payment. The ONLY envelope egress path.
|
|
2382
|
+
if (url.pathname === '/api/zk/sale/claim' && req.method === 'POST') {
|
|
2383
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2384
|
+
readBody(req, res, async (body) => {
|
|
2385
|
+
let payload;
|
|
2386
|
+
try {
|
|
2387
|
+
payload = JSON.parse(body || '{}');
|
|
2388
|
+
} catch {
|
|
2389
|
+
writeJson(res, 400, { ok: false, error: 'request body must be valid JSON' });
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
const forbidden = saleForbiddenField(payload);
|
|
2393
|
+
if (forbidden) {
|
|
2394
|
+
writeJson(res, 400, { ok: false, error: forbidden });
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
const terms = parseHex32(payload.saleTermsHashHex, 'saleTermsHashHex');
|
|
2398
|
+
const keyCommit = parseHex32(payload.keyCommitHex, 'keyCommitHex');
|
|
2399
|
+
const envelopeCommit = parseHex32(payload.envelopeCommitHex, 'envelopeCommitHex');
|
|
2400
|
+
for (const parsed of [terms, keyCommit, envelopeCommit]) {
|
|
2401
|
+
if (parsed.error) {
|
|
2402
|
+
writeJson(res, 400, { ok: false, error: parsed.error });
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
const predecessorRefs = parsePredecessorReceiptRefs(payload.predecessorReceiptRefs);
|
|
2407
|
+
if (predecessorRefs.error) {
|
|
2408
|
+
writeJson(res, 400, { ok: false, error: predecessorRefs.error });
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
const buyerPubkey = typeof payload.buyerPubkey === 'string' ? payload.buyerPubkey.trim() : '';
|
|
2412
|
+
if (!buyerPubkey) {
|
|
2413
|
+
writeJson(res, 400, { ok: false, error: 'buyerPubkey must be a non-empty base58 string' });
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
try {
|
|
2417
|
+
const runtime = await getZkRuntime();
|
|
2418
|
+
const result = await enqueueZkAction(() => runtime.claimSale({
|
|
2419
|
+
saleTermsHashBytes: terms.bytes,
|
|
2420
|
+
buyerPubkey,
|
|
2421
|
+
keyCommitBytes: keyCommit.bytes,
|
|
2422
|
+
envelopeCommitBytes: envelopeCommit.bytes,
|
|
2423
|
+
predecessorReceiptRefs: predecessorRefs.refs,
|
|
2424
|
+
}));
|
|
2425
|
+
writeJson(res, 200, { ok: true, ...result });
|
|
2426
|
+
} catch (err) {
|
|
2427
|
+
writeJson(res, saleErrorStatus(err), { ok: false, error: err.message });
|
|
2428
|
+
}
|
|
2429
|
+
});
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// Buyer identity: public keys + SOL balance only — what a fresh install
|
|
2434
|
+
// must fund and hand to sellers. No secrets. Auth required (it is this
|
|
2435
|
+
// device's identity), like the other stateful endpoints.
|
|
2436
|
+
if (url.pathname === '/api/zk/sale/buyer-identity' && req.method === 'GET') {
|
|
2437
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2438
|
+
(async () => {
|
|
2439
|
+
try {
|
|
2440
|
+
const runtime = await getZkRuntime();
|
|
2441
|
+
const identity = await enqueueZkAction(() => runtime.buyerIdentity());
|
|
2442
|
+
writeJson(res, 200, { ok: true, ...identity });
|
|
2443
|
+
} catch (err) {
|
|
2444
|
+
writeJson(res, saleErrorStatus(err), { ok: false, error: err.message });
|
|
2445
|
+
}
|
|
2446
|
+
})();
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// Public receipt view (read-only chain data — no auth, like /api/zk/status).
|
|
2451
|
+
if (url.pathname === '/api/zk/sale/receipt' && req.method === 'GET') {
|
|
2452
|
+
const keyCommit = parseHex32(url.searchParams.get('keyCommit'), 'keyCommit');
|
|
2453
|
+
const envelopeCommit = parseHex32(url.searchParams.get('envelopeCommit'), 'envelopeCommit');
|
|
2454
|
+
for (const parsed of [keyCommit, envelopeCommit]) {
|
|
2455
|
+
if (parsed.error) {
|
|
2456
|
+
writeJson(res, 400, { ok: false, error: parsed.error });
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
try {
|
|
2461
|
+
const runtime = await getZkRuntime();
|
|
2462
|
+
const { receipt, receiptPda } = await runtime.saleReceipt({
|
|
2463
|
+
keyCommitBytes: keyCommit.bytes,
|
|
2464
|
+
envelopeCommitBytes: envelopeCommit.bytes,
|
|
2465
|
+
});
|
|
2466
|
+
if (!receipt) {
|
|
2467
|
+
writeJson(res, 404, { ok: false, error: 'no sale receipt on-chain for this keyCommit + envelopeCommit pair', receiptPda });
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
writeJson(res, 200, { ok: true, receipt: receiptJson(receipt), receiptPda });
|
|
2471
|
+
} catch (err) {
|
|
2472
|
+
writeJson(res, saleErrorStatus(err), { ok: false, error: err.message });
|
|
2473
|
+
}
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// Buyer post-claim open: decrypts from the on-chain receipt with the
|
|
2478
|
+
// custodied buyer secret. Fails closed (409) while the envelope is zero.
|
|
2479
|
+
if (url.pathname === '/api/zk/sale/open' && req.method === 'POST') {
|
|
2480
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2481
|
+
readBody(req, res, async (body) => {
|
|
2482
|
+
let payload;
|
|
2483
|
+
try {
|
|
2484
|
+
payload = JSON.parse(body || '{}');
|
|
2485
|
+
} catch {
|
|
2486
|
+
writeJson(res, 400, { ok: false, error: 'request body must be valid JSON' });
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
const forbidden = saleForbiddenField(payload);
|
|
2490
|
+
if (forbidden) {
|
|
2491
|
+
writeJson(res, 400, { ok: false, error: forbidden });
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
const keyCommit = parseHex32(payload.keyCommitHex, 'keyCommitHex');
|
|
2495
|
+
const envelopeCommit = parseHex32(payload.envelopeCommitHex, 'envelopeCommitHex');
|
|
2496
|
+
const chunks = parseCiphertextChunks(payload.ciphertextChunks);
|
|
2497
|
+
for (const parsed of [keyCommit, envelopeCommit, chunks]) {
|
|
2498
|
+
if (parsed.error) {
|
|
2499
|
+
writeJson(res, 400, { ok: false, error: parsed.error });
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
const predecessorRefs = parsePredecessorReceiptRefs(payload.predecessorReceiptRefs);
|
|
2504
|
+
if (predecessorRefs.error) {
|
|
2505
|
+
writeJson(res, 400, { ok: false, error: predecessorRefs.error });
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
let payloadLen = null;
|
|
2509
|
+
if (payload.payloadLen !== undefined) {
|
|
2510
|
+
payloadLen = Number(payload.payloadLen);
|
|
2511
|
+
if (!Number.isInteger(payloadLen) || payloadLen < 0 || payloadLen > 992) {
|
|
2512
|
+
writeJson(res, 400, { ok: false, error: 'payloadLen must be an integer between 0 and 992' });
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
const buyerKeyLabel = payload.buyerKeyLabel === undefined ? null : String(payload.buyerKeyLabel);
|
|
2517
|
+
try {
|
|
2518
|
+
const runtime = await getZkRuntime();
|
|
2519
|
+
const result = await enqueueZkAction(() => runtime.openSale({
|
|
2520
|
+
keyCommitBytes: keyCommit.bytes,
|
|
2521
|
+
envelopeCommitBytes: envelopeCommit.bytes,
|
|
2522
|
+
ciphertextChunks: chunks.chunks,
|
|
2523
|
+
payloadLen,
|
|
2524
|
+
buyerKeyLabel,
|
|
2525
|
+
predecessorReceiptRefs: predecessorRefs.refs,
|
|
2526
|
+
}));
|
|
2527
|
+
writeJson(res, 200, { ok: true, ...result });
|
|
2528
|
+
} catch (err) {
|
|
2529
|
+
writeJson(res, saleErrorStatus(err), { ok: false, error: err.message });
|
|
2530
|
+
}
|
|
2531
|
+
});
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// Telemetry API
|
|
2536
|
+
if (url.pathname === '/api/telemetry') {
|
|
2537
|
+
try {
|
|
2538
|
+
const metrics = await fetchSolanaStreamMetrics({ cluster: 'mainnet-beta', rpcUrl: SOLANA_MAINNET_RPC });
|
|
2539
|
+
const latencyMs = metrics.latency_ms ?? 0;
|
|
2540
|
+
let epochPct = null;
|
|
2541
|
+
if (typeof metrics.slot_index === 'number' && typeof metrics.slots_in_epoch === 'number' && metrics.slots_in_epoch > 0) {
|
|
2542
|
+
epochPct = Math.min(100, Math.max(0, Number(((metrics.slot_index / metrics.slots_in_epoch) * 100).toFixed(1))));
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2546
|
+
res.end(JSON.stringify({
|
|
2547
|
+
ok: Boolean(metrics.ok),
|
|
2548
|
+
slot: metrics.slot ?? null,
|
|
2549
|
+
epoch: metrics.epoch ?? null,
|
|
2550
|
+
epochPct,
|
|
2551
|
+
latencyMs,
|
|
2552
|
+
latency_ms: latencyMs,
|
|
2553
|
+
slot_index: metrics.slot_index ?? null,
|
|
2554
|
+
slots_in_epoch: metrics.slots_in_epoch ?? null,
|
|
2555
|
+
cluster: 'mainnet-beta',
|
|
2556
|
+
tokenCa: OFFICIAL_TOKEN_CA,
|
|
2557
|
+
connectedWallet: session.connectedWallet || null,
|
|
2558
|
+
}));
|
|
2559
|
+
} catch (err) {
|
|
2560
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2561
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
2562
|
+
}
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// Wallet Status API
|
|
2567
|
+
if (url.pathname === '/api/wallet/status') {
|
|
2568
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2569
|
+
res.end(JSON.stringify({
|
|
2570
|
+
ok: true,
|
|
2571
|
+
connected: Boolean(session.connectedWallet),
|
|
2572
|
+
wallet: session.connectedWallet || null,
|
|
2573
|
+
tokenCa: OFFICIAL_TOKEN_CA,
|
|
2574
|
+
}));
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// Wallet Connect Trigger
|
|
2579
|
+
if (url.pathname === '/api/wallet/connect' && req.method === 'POST') {
|
|
2580
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2581
|
+
|
|
2582
|
+
try {
|
|
2583
|
+
if (!phantomBridge.server) {
|
|
2584
|
+
await phantomBridge.startServer();
|
|
2585
|
+
}
|
|
2586
|
+
const sessionData = phantomBridge.createSession();
|
|
2587
|
+
const companionUrl = await phantomBridge.openInSystemBrowser(sessionData.nonce);
|
|
2588
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2589
|
+
res.end(JSON.stringify({
|
|
2590
|
+
ok: true,
|
|
2591
|
+
companionUrl,
|
|
2592
|
+
nonce: sessionData.nonce,
|
|
2593
|
+
status: 'opened_in_browser',
|
|
2594
|
+
}));
|
|
2595
|
+
} catch (err) {
|
|
2596
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2597
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
2598
|
+
}
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// Solana RPC Proxy API (eliminates browser CORS/Origin 403 blocks)
|
|
2603
|
+
if (url.pathname === '/api/rpc' && req.method === 'POST') {
|
|
2604
|
+
readBody(req, res, async (body) => {
|
|
2605
|
+
try {
|
|
2606
|
+
const rpcRes = await fetch(SOLANA_MAINNET_RPC, {
|
|
2607
|
+
method: 'POST',
|
|
2608
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2609
|
+
body,
|
|
2610
|
+
});
|
|
2611
|
+
const rpcText = await rpcRes.text();
|
|
2612
|
+
res.writeHead(rpcRes.status, { 'Content-Type': 'application/json' });
|
|
2613
|
+
res.end(rpcText);
|
|
2614
|
+
} catch (err) {
|
|
2615
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2616
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: err.message }, id: null }));
|
|
2617
|
+
}
|
|
2618
|
+
});
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
// Active Anchor Memo payload API
|
|
2623
|
+
if (url.pathname === '/api/active-anchor-memo') {
|
|
2624
|
+
try {
|
|
2625
|
+
if (!fs.existsSync(bundlePath)) {
|
|
2626
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2627
|
+
res.end(JSON.stringify({ ok: false, error: 'Vault uninitialized. Please initialize your memory vault first (e.g. remember or quickstart).' }));
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
let bundleData;
|
|
2632
|
+
try {
|
|
2633
|
+
bundleData = JSON.parse(fs.readFileSync(bundlePath, 'utf8'));
|
|
2634
|
+
} catch {
|
|
2635
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2636
|
+
res.end(JSON.stringify({ ok: false, error: 'Unreadable or malformed vault bundle JSON.' }));
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
const candidate = bundleData?.bundle && typeof bundleData.bundle === 'object' ? bundleData.bundle : bundleData;
|
|
2641
|
+
const normalized = normalizeSolanaLocalArtifact(candidate, 'mainnet-beta', new Map());
|
|
2642
|
+
const { artifact: anchorBatch, schema } = normalized;
|
|
2643
|
+
const memoRef = createSolanaProofMemoRef(schema, anchorBatch, 'mainnet-beta');
|
|
2644
|
+
|
|
2645
|
+
// Atomically snapshot the exact anchor batch file next to bundle.json
|
|
2646
|
+
const anchorBatchPath = bundlePath.replace(/[^/\\]+$/, 'anchor-batch.json');
|
|
2647
|
+
fs.writeFileSync(anchorBatchPath, JSON.stringify(anchorBatch, null, 2), 'utf8');
|
|
2648
|
+
|
|
2649
|
+
// Fetch latest Mainnet blockhash server-side to guarantee valid transaction blockhash
|
|
2650
|
+
let blockhash = null;
|
|
2651
|
+
let lastValidBlockHeight = null;
|
|
2652
|
+
try {
|
|
2653
|
+
const bhRes = await fetch(SOLANA_MAINNET_RPC, {
|
|
2654
|
+
method: 'POST',
|
|
2655
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2656
|
+
body: JSON.stringify({
|
|
2657
|
+
jsonrpc: '2.0',
|
|
2658
|
+
id: 'get-latest-blockhash',
|
|
2659
|
+
method: 'getLatestBlockhash',
|
|
2660
|
+
params: [{ commitment: 'confirmed' }],
|
|
2661
|
+
}),
|
|
2662
|
+
});
|
|
2663
|
+
const bhData = await bhRes.json();
|
|
2664
|
+
blockhash = bhData?.result?.value?.blockhash || null;
|
|
2665
|
+
lastValidBlockHeight = bhData?.result?.value?.lastValidBlockHeight || null;
|
|
2666
|
+
} catch {}
|
|
2667
|
+
|
|
2668
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2669
|
+
res.end(JSON.stringify({
|
|
2670
|
+
ok: true,
|
|
2671
|
+
bundlePath,
|
|
2672
|
+
anchorBatchPath,
|
|
2673
|
+
anchorBatch,
|
|
2674
|
+
memoRef,
|
|
2675
|
+
blockhash,
|
|
2676
|
+
lastValidBlockHeight,
|
|
2677
|
+
cluster: 'mainnet-beta',
|
|
2678
|
+
memoProgramId: 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr',
|
|
2679
|
+
}));
|
|
2680
|
+
} catch (err) {
|
|
2681
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2682
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
2683
|
+
}
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
if (url.pathname === '/api/auto-anchor/status') {
|
|
2687
|
+
try {
|
|
2688
|
+
const isConfigured = autoAnchorEngine.isConfigured();
|
|
2689
|
+
const signerPubkey = autoAnchorEngine.signerStore.getPublicKey();
|
|
2690
|
+
const delegation = autoAnchorEngine.getDelegation();
|
|
2691
|
+
const outboxStats = autoAnchorEngine.outbox.getStats();
|
|
2692
|
+
let balance = null;
|
|
2693
|
+
if (signerPubkey) {
|
|
2694
|
+
try {
|
|
2695
|
+
balance = await autoAnchorEngine.transport.getBalance(signerPubkey);
|
|
2696
|
+
} catch {}
|
|
2697
|
+
}
|
|
2698
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2699
|
+
res.end(JSON.stringify({
|
|
2700
|
+
ok: true,
|
|
2701
|
+
isConfigured,
|
|
2702
|
+
signerPubkey,
|
|
2703
|
+
delegation,
|
|
2704
|
+
outboxStats,
|
|
2705
|
+
balance,
|
|
2706
|
+
cluster: 'mainnet-beta',
|
|
2707
|
+
}));
|
|
2708
|
+
} catch (err) {
|
|
2709
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2710
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
2711
|
+
}
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
// Auto-Anchor Setup Signer API
|
|
2716
|
+
if (url.pathname === '/api/auto-anchor/setup-signer' && req.method === 'POST') {
|
|
2717
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2718
|
+
try {
|
|
2719
|
+
const result = autoAnchorEngine.setupSigner();
|
|
2720
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2721
|
+
res.end(JSON.stringify({ ok: true, signerPubkey: result.publicKeyBase58 }));
|
|
2722
|
+
} catch (err) {
|
|
2723
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2724
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
2725
|
+
}
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
// Auto-Anchor Delegation Challenge API
|
|
2730
|
+
if (url.pathname === '/api/auto-anchor/challenge' && req.method === 'POST') {
|
|
2731
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2732
|
+
readBody(req, res, async (body) => {
|
|
2733
|
+
try {
|
|
2734
|
+
const payload = JSON.parse(body || '{}');
|
|
2735
|
+
const ownerPubkey = payload.ownerPubkey || session.connectedWallet?.publicKey;
|
|
2736
|
+
if (!ownerPubkey) {
|
|
2737
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2738
|
+
res.end(JSON.stringify({ ok: false, error: 'ownerPubkey required' }));
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
const challengeData = autoAnchorEngine.getDelegationChallenge({ ownerPubkey });
|
|
2742
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2743
|
+
res.end(JSON.stringify({ ok: true, ...challengeData }));
|
|
2744
|
+
} catch (err) {
|
|
2745
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2746
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
2747
|
+
}
|
|
2748
|
+
});
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// Auto-Anchor Delegation Set API
|
|
2753
|
+
if (url.pathname === '/api/auto-anchor/delegate' && req.method === 'POST') {
|
|
2754
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2755
|
+
readBody(req, res, async (body) => {
|
|
2756
|
+
try {
|
|
2757
|
+
const delegation = JSON.parse(body || '{}');
|
|
2758
|
+
autoAnchorEngine.setDelegation(delegation);
|
|
2759
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2760
|
+
res.end(JSON.stringify({ ok: true, isConfigured: true, authorizationHash: delegation.authorization_hash }));
|
|
2761
|
+
} catch (err) {
|
|
2762
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2763
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
// Auto-Anchor Process Next Outbox Item API
|
|
2770
|
+
if (url.pathname === '/api/auto-anchor/process' && req.method === 'POST') {
|
|
2771
|
+
if (!validateSecurityHeaders(req, res, { requireAuth: true })) return;
|
|
2772
|
+
try {
|
|
2773
|
+
const result = await autoAnchorEngine.processNextOutboxItem();
|
|
2774
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2775
|
+
res.end(JSON.stringify({ ok: true, result }));
|
|
2776
|
+
} catch (err) {
|
|
2777
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2778
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
2779
|
+
}
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
// Static File Server
|
|
2784
|
+
// Defense in depth: the persisted ZK snapshot contains recipient secret
|
|
2785
|
+
// material and note openings. Reject its filename before static resolution,
|
|
2786
|
+
// even if a caller misconfigures the state path into this directory.
|
|
2787
|
+
if (path.basename(url.pathname).toLowerCase() === 'zk-tree-state.json') {
|
|
2788
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
2789
|
+
res.end(JSON.stringify({ ok: false, error: 'Not Found' }));
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
const reqUrl = url.pathname === '/' ? 'index.html' : url.pathname.replace(/^\/+/, '');
|
|
2793
|
+
const safePath = path.normalize(reqUrl).replace(/^(\.\.[\/\\])+/, '').replace(/^[\\\/]+/, '');
|
|
2794
|
+
const filePath = path.join(DESKTOP_DIR, safePath);
|
|
2795
|
+
|
|
2796
|
+
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
|
|
2797
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
2798
|
+
res.end('Not Found');
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
2803
|
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
2804
|
+
|
|
2805
|
+
if (ext === '.html') {
|
|
2806
|
+
let html = fs.readFileSync(filePath, 'utf8');
|
|
2807
|
+
html = html.replace('__ENIGMA_AUTH_TOKEN__', authToken);
|
|
2808
|
+
// Version is injected at serve time from the package manifest — never
|
|
2809
|
+
// hard-code it in the markup (a stale string ships the wrong identity).
|
|
2810
|
+
html = html.replaceAll('__ENIGMA_VERSION__', ENIGMA_VERSION);
|
|
2811
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
2812
|
+
res.end(html);
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
2817
|
+
fs.createReadStream(filePath).pipe(res);
|
|
2818
|
+
});
|
|
2819
|
+
|
|
2820
|
+
return {
|
|
2821
|
+
server,
|
|
2822
|
+
session,
|
|
2823
|
+
phantomBridge,
|
|
2824
|
+
autoAnchorEngine,
|
|
2825
|
+
authToken,
|
|
2826
|
+
apiKeys,
|
|
2827
|
+
usage,
|
|
2828
|
+
credits,
|
|
2829
|
+
payments,
|
|
2830
|
+
get port() { return boundPort; },
|
|
2831
|
+
host,
|
|
2832
|
+
listen: () => new Promise((resolve, reject) => {
|
|
2833
|
+
server.listen(boundPort, host, () => {
|
|
2834
|
+
const addr = server.address();
|
|
2835
|
+
boundPort = addr.port;
|
|
2836
|
+
resolve({
|
|
2837
|
+
url: `http://${host}:${boundPort}`,
|
|
2838
|
+
port: boundPort,
|
|
2839
|
+
host,
|
|
2840
|
+
authToken,
|
|
2841
|
+
});
|
|
2842
|
+
});
|
|
2843
|
+
server.on('error', reject);
|
|
2844
|
+
}),
|
|
2845
|
+
close: async () => {
|
|
2846
|
+
if (typeof platformRuntime.close === 'function') {
|
|
2847
|
+
await platformRuntime.close();
|
|
2848
|
+
} else {
|
|
2849
|
+
await platformRuntime.browser?.close?.();
|
|
2850
|
+
await platformRuntime.code?.close?.();
|
|
2851
|
+
}
|
|
2852
|
+
for (const entry of pendingPrepares.values()) clearPendingPrepare(entry);
|
|
2853
|
+
pendingPrepares.clear();
|
|
2854
|
+
await phantomBridge.close();
|
|
2855
|
+
if (federationBridge) {
|
|
2856
|
+
persistOperationalState();
|
|
2857
|
+
await federationBridge.stop();
|
|
2858
|
+
}
|
|
2859
|
+
if (meshNode) await meshNode.stop();
|
|
2860
|
+
if (sdkPromise) {
|
|
2861
|
+
try {
|
|
2862
|
+
const enigma = await sdkPromise;
|
|
2863
|
+
if (typeof enigma.dispose === 'function') await enigma.dispose();
|
|
2864
|
+
else await enigma.rag?.dispose?.();
|
|
2865
|
+
} catch {}
|
|
2866
|
+
}
|
|
2867
|
+
if (ownsVectorStore && ownsEmbeddingProvider) {
|
|
2868
|
+
try {
|
|
2869
|
+
await vectorStore.embedder?.dispose?.();
|
|
2870
|
+
} catch {}
|
|
2871
|
+
}
|
|
2872
|
+
if (ephemeralTempDir && fs.existsSync(ephemeralTempDir)) {
|
|
2873
|
+
try { fs.rmSync(ephemeralTempDir, { recursive: true, force: true }); } catch {}
|
|
2874
|
+
}
|
|
2875
|
+
return new Promise((resolve) => server.close(resolve));
|
|
2876
|
+
},
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
export async function startDesktopServer(options = {}) {
|
|
2881
|
+
const desktopServer = createDesktopServer(options);
|
|
2882
|
+
const info = await desktopServer.listen();
|
|
2883
|
+
|
|
2884
|
+
console.log(`\n============================================================`);
|
|
2885
|
+
console.log(`⚡ ENIGMA SOVEREIGN TERMINAL DESKTOP RUNNING`);
|
|
2886
|
+
console.log(` Origin: ${info.url}`);
|
|
2887
|
+
console.log(` Cluster: Solana Mainnet (mainnet-beta)`);
|
|
2888
|
+
console.log(` Security Mode: Hardened Localhost Auth (Strict Host/Origin Check)`);
|
|
2889
|
+
console.log(`============================================================\n`);
|
|
2890
|
+
|
|
2891
|
+
if (options.openBrowser !== false) {
|
|
2892
|
+
const chromePaths = [
|
|
2893
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
2894
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
2895
|
+
];
|
|
2896
|
+
const chromeExe = chromePaths.find((p) => fs.existsSync(p));
|
|
2897
|
+
|
|
2898
|
+
if (chromeExe) {
|
|
2899
|
+
spawn(chromeExe, [`--app=${info.url}`, '--window-size=1240,780'], {
|
|
2900
|
+
detached: true,
|
|
2901
|
+
stdio: 'ignore',
|
|
2902
|
+
}).unref();
|
|
2903
|
+
} else {
|
|
2904
|
+
const startCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
2905
|
+
if (process.platform === 'win32') {
|
|
2906
|
+
spawn('cmd.exe', ['/c', 'start', '', info.url], { detached: true, stdio: 'ignore' }).unref();
|
|
2907
|
+
} else {
|
|
2908
|
+
spawn(startCmd, [info.url], { detached: true, stdio: 'ignore' }).unref();
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
return desktopServer;
|
|
2914
|
+
}
|