enigma-memory 0.1.17 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -27
- package/apps/cli/bin/enigma-desktop.mjs +140 -0
- package/apps/cli/bin/enigma-terminal.mjs +78 -0
- package/apps/cli/bin/enigma.mjs +4979 -3263
- 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/SIMULATION.md +14 -9
- package/deploy/docker-compose.local-production-simulation.yml +54 -12
- package/docs/benchmark-attestation-network.md +487 -487
- package/docs/benchmark-reproducibility.md +289 -289
- package/docs/blockchain-only-mechanisms.md +400 -400
- package/docs/browser-extension-install.md +8 -6
- package/docs/client-connectors.md +16 -12
- package/docs/demo-proof-network.md +275 -275
- package/docs/developer-ecosystem.md +15 -13
- package/docs/developer-proof-quickstart.md +325 -325
- package/docs/enigma-memory-ready-conformance.md +378 -376
- package/docs/install-anywhere.md +64 -30
- package/docs/installers-and-desktop.md +8 -7
- package/docs/memory-benchmarks.md +1 -1
- package/docs/memory-drive-health-model.md +690 -690
- package/docs/novelty-invention-candidates.md +161 -161
- package/docs/proof-network-build-notes.md +240 -240
- package/docs/proof-network-claim-boundaries.md +320 -318
- package/docs/proof-network.md +339 -339
- package/docs/sdk-api.md +324 -324
- package/docs/solana-proof-rail.md +453 -453
- 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 +410 -278
- 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 +249 -2
- 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 +2498 -1185
- 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 +436 -7
- 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 +1150 -125
- 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 +292 -274
- package/scripts/build-hosted-customer-lifecycle.mjs +493 -476
- package/scripts/build-hosted-probe-worker.mjs +19 -4
- package/scripts/build-installer-assets.mjs +409 -389
- 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 +4 -1
- package/scripts/build-proof-network-packet.mjs +213 -213
- package/scripts/check.mjs +25 -4
- package/scripts/collect-hosted-backend-live-evidence.mjs +49 -12
- package/scripts/install-enigma-local.mjs +18 -5
- package/scripts/release-audit.mjs +74 -115
- package/scripts/release-provenance.mjs +12 -2
- package/scripts/run-backend-readiness-smoke.mjs +112 -10
- package/scripts/run-standard-memory-benchmarks.mjs +1354 -1352
- package/scripts/scan-secrets.mjs +178 -0
- package/scripts/simulate-production-env.mjs +71 -10
- 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,279 @@
|
|
|
1
|
+
// Enigma Encrypted Mesh Protocol ("GhostMesh") — Packet Router & Distance-Vector Engine.
|
|
2
|
+
// Maintains on-demand hop-by-hop routing tables, packet deduplication caches,
|
|
3
|
+
// and store-and-forward queues for multi-hop mesh propagation.
|
|
4
|
+
import { PACKET_TYPES, decodePacket, deriveDestination, encodePacket, getPacketReplayIdentity } from './protocol.js';
|
|
5
|
+
import { verifyPacketSignature } from './crypto.js';
|
|
6
|
+
|
|
7
|
+
export const ROUTE_TTL_MS = 600_000; // 10 minutes route expiry
|
|
8
|
+
export const DEDUP_CACHE_TTL_MS = 120_000; // 2 minutes deduplication cache
|
|
9
|
+
export const MAX_STORE_FORWARD_ITEMS = 1000;
|
|
10
|
+
export const MAX_DEDUP_CACHE_ITEMS = 10_000;
|
|
11
|
+
export const MAX_KNOWN_PEERS = 1_024;
|
|
12
|
+
|
|
13
|
+
export class PacketRouter {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {Buffer} options.nodeIdentity - This node's Ed25519 identity keypair
|
|
17
|
+
* @param {string} [options.nodeId] - Optional readable label
|
|
18
|
+
*/
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.identity = options.nodeIdentity;
|
|
21
|
+
this.nodeId = options.nodeId || this.identity?.destinationHex?.slice(0, 12) || 'mesh-node';
|
|
22
|
+
this.authenticatePacket = options.verifyPacketSignature ?? verifyPacketSignature;
|
|
23
|
+
|
|
24
|
+
// Destination routing table: destinationHex -> { peerId, hopCount, transportId, updatedAt, expiresAt }
|
|
25
|
+
this.routes = new Map();
|
|
26
|
+
|
|
27
|
+
// Known peer identities: peerId -> { pubkey, destinationHex, transportId, lastSeen }
|
|
28
|
+
this.peers = new Map();
|
|
29
|
+
|
|
30
|
+
// Packet deduplication cache: packetHashHex -> timestamp
|
|
31
|
+
this.seenPackets = new Map();
|
|
32
|
+
|
|
33
|
+
// Store-and-Forward queue for offline / delayed hops: id -> { packet, destinationHex, queuedAt, attempts }
|
|
34
|
+
this.storeForwardQueue = new Map();
|
|
35
|
+
|
|
36
|
+
// Event listeners
|
|
37
|
+
this.handlers = {
|
|
38
|
+
packet: new Set(),
|
|
39
|
+
delivered: new Set(),
|
|
40
|
+
routed: new Set(),
|
|
41
|
+
announce: new Set(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Registers a transport interface or direct peer connection.
|
|
47
|
+
*/
|
|
48
|
+
registerPeer(peerId, peerInfo) {
|
|
49
|
+
if (!this.peers.has(peerId) && this.peers.size >= MAX_KNOWN_PEERS) return false;
|
|
50
|
+
this.peers.set(peerId, {
|
|
51
|
+
...this.peers.get(peerId),
|
|
52
|
+
...peerInfo,
|
|
53
|
+
lastSeen: Date.now(),
|
|
54
|
+
});
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
unregisterPeer(peerId) {
|
|
59
|
+
this.peers.delete(peerId);
|
|
60
|
+
for (const [dest, route] of this.routes.entries()) {
|
|
61
|
+
if (route.peerId === peerId) {
|
|
62
|
+
this.routes.delete(dest);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Processes an incoming raw packet buffer from a peer/transport.
|
|
69
|
+
* @param {Buffer} rawPacket
|
|
70
|
+
* @param {string} fromPeerId
|
|
71
|
+
* @param {string} transportId
|
|
72
|
+
* @returns {Promise<{action: 'DELIVERED'|'ROUTED'|'DROPPED'|'QUEUED'|'ANNOUNCED', packet?: Object, reason?: string}>}
|
|
73
|
+
*/
|
|
74
|
+
async handleIncomingPacket(rawPacket, fromPeerId, transportId) {
|
|
75
|
+
// Decode the bounded frame once before deriving its stable replay identity.
|
|
76
|
+
// Authenticated cache hits may skip Ed25519 work, but unauthenticated frames
|
|
77
|
+
// are never inserted and therefore cannot poison the replay cache.
|
|
78
|
+
let decoded;
|
|
79
|
+
let packetHash;
|
|
80
|
+
try {
|
|
81
|
+
decoded = decodePacket(rawPacket);
|
|
82
|
+
packetHash = getPacketReplayIdentity(decoded.rawBytes);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return { action: 'DROPPED', reason: `decode_error: ${err.message}` };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
this.pruneDedupCache(now);
|
|
89
|
+
if (this.seenPackets.has(packetHash)) {
|
|
90
|
+
return { action: 'DROPPED', reason: 'duplicate_packet' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const registeredPeer = this.peers.get(fromPeerId);
|
|
94
|
+
if (
|
|
95
|
+
registeredPeer?.publicKey
|
|
96
|
+
&& !Buffer.from(registeredPeer.publicKey).equals(decoded.senderPubkey)
|
|
97
|
+
) {
|
|
98
|
+
return { action: 'DROPPED', reason: 'peer_identity_mismatch' };
|
|
99
|
+
}
|
|
100
|
+
if (!this.authenticatePacket(rawPacket)) {
|
|
101
|
+
return { action: 'DROPPED', reason: 'invalid_signature' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let announceMetadata = null;
|
|
105
|
+
if (decoded.type === PACKET_TYPES.ANNOUNCE) {
|
|
106
|
+
try {
|
|
107
|
+
announceMetadata = JSON.parse(decoded.payload.toString('utf8'));
|
|
108
|
+
const scope = announceMetadata?.scope;
|
|
109
|
+
const encryptionKeyHex = announceMetadata?.encryptionPublicKeyHex;
|
|
110
|
+
if (
|
|
111
|
+
!announceMetadata
|
|
112
|
+
|| typeof announceMetadata !== 'object'
|
|
113
|
+
|| Array.isArray(announceMetadata)
|
|
114
|
+
|| typeof scope !== 'string'
|
|
115
|
+
|| scope.length === 0
|
|
116
|
+
|| typeof encryptionKeyHex !== 'string'
|
|
117
|
+
|| !/^[a-f0-9]{64}$/i.test(encryptionKeyHex)
|
|
118
|
+
) {
|
|
119
|
+
throw new Error('announce metadata must contain a scope and 32-byte X25519 key');
|
|
120
|
+
}
|
|
121
|
+
const expectedDestination = deriveDestination(
|
|
122
|
+
decoded.senderPubkey,
|
|
123
|
+
Buffer.from(encryptionKeyHex, 'hex'),
|
|
124
|
+
scope,
|
|
125
|
+
);
|
|
126
|
+
if (!expectedDestination.equals(decoded.destination)) {
|
|
127
|
+
throw new Error('announce destination does not match its signing key, encryption key, and scope');
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return { action: 'DROPPED', reason: `invalid_announce_binding: ${error.message}` };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.seenPackets.size >= MAX_DEDUP_CACHE_ITEMS) {
|
|
135
|
+
this.seenPackets.delete(this.seenPackets.keys().next().value);
|
|
136
|
+
}
|
|
137
|
+
this.seenPackets.set(packetHash, now);
|
|
138
|
+
|
|
139
|
+
// 4. Update route if packet is an ANNOUNCE
|
|
140
|
+
if (decoded.type === PACKET_TYPES.ANNOUNCE) {
|
|
141
|
+
this.updateRoute(decoded.destinationHex, fromPeerId, decoded.hopTtl, transportId, now);
|
|
142
|
+
this.emit('announce', { packet: decoded, metadata: announceMetadata, fromPeerId });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 5. Check if packet is addressed to THIS node or is a broadcast packet
|
|
146
|
+
const isBroadcast = Buffer.from(decoded.destination).every((b) => b === 0xff) ||
|
|
147
|
+
decoded.type === PACKET_TYPES.SEALED_SALE_OFFER ||
|
|
148
|
+
decoded.type === PACKET_TYPES.ZK_PROOF_GOSSIP;
|
|
149
|
+
|
|
150
|
+
const isForMe = (this.identity && Buffer.from(decoded.destination).equals(this.identity.destination)) || isBroadcast;
|
|
151
|
+
|
|
152
|
+
if (isForMe) {
|
|
153
|
+
this.emit('delivered', { packet: decoded, fromPeerId });
|
|
154
|
+
if (!isBroadcast) {
|
|
155
|
+
return { action: 'DELIVERED', packet: decoded };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// 6. Multi-hop Forwarding / Routing
|
|
159
|
+
if (decoded.hopTtl <= 1) {
|
|
160
|
+
return { action: 'DROPPED', reason: 'ttl_expired' };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Decrement TTL for the next hop
|
|
164
|
+
const nextHopTtl = decoded.hopTtl - 1;
|
|
165
|
+
const forwardBuffer = encodePacket({
|
|
166
|
+
...decoded,
|
|
167
|
+
hopTtl: nextHopTtl,
|
|
168
|
+
signature: decoded.signature,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const route = this.getBestRoute(decoded.destinationHex);
|
|
172
|
+
if (route && route.peerId !== fromPeerId) {
|
|
173
|
+
this.emit('routed', { packet: decoded, nextHopPeerId: route.peerId, transportId: route.transportId, forwardBuffer });
|
|
174
|
+
return { action: 'ROUTED', packet: decoded, nextHopPeerId: route.peerId, transportId: route.transportId, forwardBuffer };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 7. Store-and-forward queue if route is currently unreachable
|
|
178
|
+
this.queueStoreForward(packetHash, forwardBuffer, decoded.destinationHex, now);
|
|
179
|
+
return { action: 'QUEUED', packet: decoded, reason: 'route_unreachable_stored_for_relay' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Updates the routing table with distance-vector next-hop data.
|
|
184
|
+
*/
|
|
185
|
+
updateRoute(destinationHex, peerId, hopCount, transportId, now = Date.now()) {
|
|
186
|
+
const existing = this.routes.get(destinationHex);
|
|
187
|
+
// Lower hop count or fresher update takes precedence
|
|
188
|
+
if (!existing || hopCount < existing.hopCount || (hopCount === existing.hopCount && now > existing.updatedAt)) {
|
|
189
|
+
this.routes.set(destinationHex, {
|
|
190
|
+
peerId,
|
|
191
|
+
hopCount,
|
|
192
|
+
transportId,
|
|
193
|
+
updatedAt: now,
|
|
194
|
+
expiresAt: now + ROUTE_TTL_MS,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getBestRoute(destinationHex) {
|
|
200
|
+
const route = this.routes.get(destinationHex);
|
|
201
|
+
if (!route) return null;
|
|
202
|
+
if (Date.now() > route.expiresAt) {
|
|
203
|
+
this.routes.delete(destinationHex);
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return route;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
queueStoreForward(id, packetBuffer, destinationHex, now = Date.now()) {
|
|
210
|
+
if (this.storeForwardQueue.size >= MAX_STORE_FORWARD_ITEMS) {
|
|
211
|
+
const oldestKey = this.storeForwardQueue.keys().next().value;
|
|
212
|
+
this.storeForwardQueue.delete(oldestKey);
|
|
213
|
+
}
|
|
214
|
+
this.storeForwardQueue.set(id, {
|
|
215
|
+
packet: packetBuffer,
|
|
216
|
+
destinationHex,
|
|
217
|
+
queuedAt: now,
|
|
218
|
+
attempts: 0,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Flushes store-and-forward packets when a new peer/route becomes available.
|
|
224
|
+
* @param {Function} forwardCallback - async (packetBuffer, peerId, transportId) => boolean
|
|
225
|
+
*/
|
|
226
|
+
async flushStoreForward(forwardCallback) {
|
|
227
|
+
let flushed = 0;
|
|
228
|
+
for (const [id, item] of Array.from(this.storeForwardQueue.entries())) {
|
|
229
|
+
const route = this.getBestRoute(item.destinationHex);
|
|
230
|
+
if (route) {
|
|
231
|
+
try {
|
|
232
|
+
const success = await forwardCallback(item.packet, route.peerId, route.transportId);
|
|
233
|
+
if (success) {
|
|
234
|
+
this.storeForwardQueue.delete(id);
|
|
235
|
+
flushed++;
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
item.attempts++;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return flushed;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
pruneDedupCache(now = Date.now()) {
|
|
246
|
+
for (const [hash, time] of this.seenPackets.entries()) {
|
|
247
|
+
if (now - time > DEDUP_CACHE_TTL_MS) {
|
|
248
|
+
this.seenPackets.delete(hash);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
on(event, handler) {
|
|
254
|
+
if (this.handlers[event]) this.handlers[event].add(handler);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
off(event, handler) {
|
|
258
|
+
if (this.handlers[event]) this.handlers[event].delete(handler);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
emit(event, data) {
|
|
262
|
+
if (this.handlers[event]) {
|
|
263
|
+
for (const h of this.handlers[event]) {
|
|
264
|
+
try { h(data); } catch {}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getRoutingStats() {
|
|
270
|
+
return {
|
|
271
|
+
nodeId: this.nodeId,
|
|
272
|
+
destinationHex: this.identity?.destinationHex || null,
|
|
273
|
+
activePeers: this.peers.size,
|
|
274
|
+
knownRoutes: this.routes.size,
|
|
275
|
+
storeForwardQueueSize: this.storeForwardQueue.size,
|
|
276
|
+
seenPacketsCacheSize: this.seenPackets.size,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// Enigma Encrypted Mesh Protocol ("GhostMesh") — Multi-Transport Layer.
|
|
2
|
+
// Provides AbstractTransport, InMemoryTransport (for multi-node simulation),
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { EventEmitter } from 'node:events';
|
|
5
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
6
|
+
import { generateMeshIdentity, importRawEd25519PrivateKey, importRawEd25519PublicKey } from './crypto.js';
|
|
7
|
+
import { MAX_PACKET_SIZE } from './protocol.js';
|
|
8
|
+
function receivedBinaryFrame(data, isBinary) {
|
|
9
|
+
if (typeof isBinary === 'boolean') return isBinary;
|
|
10
|
+
return Buffer.isBuffer(data) || ArrayBuffer.isView(data) || data instanceof ArrayBuffer;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class AbstractTransport extends EventEmitter {
|
|
14
|
+
constructor(id) {
|
|
15
|
+
super();
|
|
16
|
+
this.id = id || 'transport-' + Math.random().toString(16).slice(2, 8);
|
|
17
|
+
this.running = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async start() { this.running = true; }
|
|
21
|
+
async stop() { this.running = false; }
|
|
22
|
+
async send(rawPacketBuffer, peerId) { throw new Error('Not implemented'); }
|
|
23
|
+
async broadcast(rawPacketBuffer) { throw new Error('Not implemented'); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* In-Memory Transport Bus for multi-node testing and simulation.
|
|
28
|
+
* Allows multiple MeshNode instances to communicate over a shared memory bus with configurable packet drops/delays.
|
|
29
|
+
*/
|
|
30
|
+
export class InMemoryBus {
|
|
31
|
+
constructor() {
|
|
32
|
+
this.nodes = new Map(); // nodeId -> InMemoryTransport
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
register(nodeId, transport) {
|
|
36
|
+
this.nodes.set(nodeId, transport);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
unregister(nodeId) {
|
|
40
|
+
this.nodes.delete(nodeId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
deliver(fromNodeId, toNodeId, packetBuffer) {
|
|
44
|
+
const target = this.nodes.get(toNodeId);
|
|
45
|
+
if (target && target.running) {
|
|
46
|
+
target.emit('packet', { packetBuffer, fromPeerId: fromNodeId, transportId: target.id });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
broadcast(fromNodeId, packetBuffer) {
|
|
51
|
+
for (const [nodeId, transport] of this.nodes.entries()) {
|
|
52
|
+
if (nodeId !== fromNodeId && transport.running) {
|
|
53
|
+
transport.emit('packet', { packetBuffer, fromPeerId: fromNodeId, transportId: transport.id });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class InMemoryTransport extends AbstractTransport {
|
|
60
|
+
constructor(nodeId, bus, id) {
|
|
61
|
+
super(id || `inmem-${nodeId}`);
|
|
62
|
+
this.nodeId = nodeId;
|
|
63
|
+
this.bus = bus;
|
|
64
|
+
this.bus.register(nodeId, this);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async send(rawPacketBuffer, peerId) {
|
|
68
|
+
if (!this.running) return false;
|
|
69
|
+
this.bus.deliver(this.nodeId, peerId, rawPacketBuffer);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async broadcast(rawPacketBuffer) {
|
|
74
|
+
if (!this.running) return false;
|
|
75
|
+
this.bus.broadcast(this.nodeId, rawPacketBuffer);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async stop() {
|
|
80
|
+
await super.stop();
|
|
81
|
+
this.bus.unregister(this.nodeId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* WebSocket Peer-to-Peer Transport for LAN and WAN Mesh Peering.
|
|
87
|
+
*/
|
|
88
|
+
export class WebSocketTransport extends AbstractTransport {
|
|
89
|
+
constructor(options = {}) {
|
|
90
|
+
super(options.id || 'ws-transport');
|
|
91
|
+
this.port = options.port ?? 0;
|
|
92
|
+
this.host = options.host || '127.0.0.1';
|
|
93
|
+
this.server = null;
|
|
94
|
+
this.actualPort = null;
|
|
95
|
+
this.sockets = new Map();
|
|
96
|
+
this.pendingSockets = new Set();
|
|
97
|
+
this.peerAddresses = new Set(options.peers || []);
|
|
98
|
+
this.identity = options.identity || generateMeshIdentity(options.scope || 'enigma.transport');
|
|
99
|
+
this.allowedPeerPublicKeys = new Set(
|
|
100
|
+
(options.allowedPeerPublicKeys || []).map((key) => (
|
|
101
|
+
Buffer.isBuffer(key) || key instanceof Uint8Array
|
|
102
|
+
? Buffer.from(key).toString('hex')
|
|
103
|
+
: String(key).toLowerCase()
|
|
104
|
+
)),
|
|
105
|
+
);
|
|
106
|
+
this.allowUnlistedPeers = options.allowUnlistedPeers
|
|
107
|
+
?? (this.host === '127.0.0.1' || this.host === 'localhost' || this.host === '::1');
|
|
108
|
+
this.maxConnections = Number.isSafeInteger(options.maxConnections) && options.maxConnections > 0
|
|
109
|
+
? options.maxConnections
|
|
110
|
+
: 64;
|
|
111
|
+
this.handshakeTimeoutMs = Number.isSafeInteger(options.handshakeTimeoutMs) && options.handshakeTimeoutMs > 0
|
|
112
|
+
? options.handshakeTimeoutMs
|
|
113
|
+
: 5_000;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async start() {
|
|
117
|
+
if (!this.allowUnlistedPeers && this.allowedPeerPublicKeys.size === 0) {
|
|
118
|
+
throw new Error('externally reachable WebSocket transport requires an allowedPeerPublicKeys allowlist');
|
|
119
|
+
}
|
|
120
|
+
await super.start();
|
|
121
|
+
if (this.port !== null && this.port !== undefined) {
|
|
122
|
+
await new Promise((resolve, reject) => {
|
|
123
|
+
this.server = new WebSocketServer({
|
|
124
|
+
port: this.port,
|
|
125
|
+
host: this.host,
|
|
126
|
+
maxPayload: MAX_PACKET_SIZE,
|
|
127
|
+
}, () => {
|
|
128
|
+
this.actualPort = this.server.address().port;
|
|
129
|
+
resolve();
|
|
130
|
+
});
|
|
131
|
+
this.server.on('error', reject);
|
|
132
|
+
this.server.on('connection', (ws, req) => {
|
|
133
|
+
if (this.sockets.size + this.pendingSockets.size >= this.maxConnections) {
|
|
134
|
+
ws.close(1013, 'connection capacity reached');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.setupSocket(ws, `${req.socket.remoteAddress}:${req.socket.remotePort}`);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
for (const addr of this.peerAddresses) this.connectToPeer(addr).catch(() => {});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
handshakeBytes(nonce) {
|
|
145
|
+
return Buffer.concat([Buffer.from('enigma.ghostmesh.ws.handshake.v1\0'), nonce]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setupSocket(ws, address, onAuthenticated = null, onError = null) {
|
|
149
|
+
const state = {
|
|
150
|
+
address,
|
|
151
|
+
authenticated: false,
|
|
152
|
+
peerId: null,
|
|
153
|
+
localNonce: crypto.randomBytes(32),
|
|
154
|
+
};
|
|
155
|
+
this.pendingSockets.add(ws);
|
|
156
|
+
const timer = setTimeout(() => ws.close(1008, 'handshake timeout'), this.handshakeTimeoutMs);
|
|
157
|
+
ws.send(JSON.stringify({
|
|
158
|
+
kind: 'enigma.ghostmesh.ws.challenge.v1',
|
|
159
|
+
nonce: state.localNonce.toString('base64'),
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
ws.on('message', (data, isBinary) => {
|
|
163
|
+
if (!state.authenticated) {
|
|
164
|
+
if (receivedBinaryFrame(data, isBinary)) {
|
|
165
|
+
ws.close(1003, 'handshake requires text frames');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const message = JSON.parse(Buffer.from(data).toString('utf8'));
|
|
170
|
+
if (message.kind === 'enigma.ghostmesh.ws.challenge.v1') {
|
|
171
|
+
const nonce = Buffer.from(message.nonce || '', 'base64');
|
|
172
|
+
if (nonce.length !== 32) throw new Error('invalid handshake nonce');
|
|
173
|
+
const signature = crypto.sign(
|
|
174
|
+
null,
|
|
175
|
+
this.handshakeBytes(nonce),
|
|
176
|
+
importRawEd25519PrivateKey(this.identity.secretKey),
|
|
177
|
+
);
|
|
178
|
+
ws.send(JSON.stringify({
|
|
179
|
+
kind: 'enigma.ghostmesh.ws.response.v1',
|
|
180
|
+
nonce: message.nonce,
|
|
181
|
+
public_key: this.identity.publicKey.toString('hex'),
|
|
182
|
+
signature: signature.toString('base64'),
|
|
183
|
+
}));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (message.kind !== 'enigma.ghostmesh.ws.response.v1') throw new Error('unexpected handshake message');
|
|
187
|
+
const publicKey = Buffer.from(message.public_key || '', 'hex');
|
|
188
|
+
const nonce = Buffer.from(message.nonce || '', 'base64');
|
|
189
|
+
const signature = Buffer.from(message.signature || '', 'base64');
|
|
190
|
+
if (
|
|
191
|
+
publicKey.length !== 32
|
|
192
|
+
|| nonce.length !== 32
|
|
193
|
+
|| signature.length !== 64
|
|
194
|
+
|| !crypto.timingSafeEqual(nonce, state.localNonce)
|
|
195
|
+
|| !crypto.verify(null, this.handshakeBytes(nonce), importRawEd25519PublicKey(publicKey), signature)
|
|
196
|
+
) {
|
|
197
|
+
throw new Error('invalid signed handshake response');
|
|
198
|
+
}
|
|
199
|
+
const publicKeyHex = publicKey.toString('hex');
|
|
200
|
+
if (!this.allowUnlistedPeers && !this.allowedPeerPublicKeys.has(publicKeyHex)) {
|
|
201
|
+
throw new Error('peer identity is not allowlisted');
|
|
202
|
+
}
|
|
203
|
+
if (this.sockets.size >= this.maxConnections && !this.sockets.has(publicKeyHex)) {
|
|
204
|
+
throw new Error('connection capacity reached');
|
|
205
|
+
}
|
|
206
|
+
state.authenticated = true;
|
|
207
|
+
state.peerId = publicKeyHex;
|
|
208
|
+
clearTimeout(timer);
|
|
209
|
+
this.pendingSockets.delete(ws);
|
|
210
|
+
const prior = this.sockets.get(publicKeyHex);
|
|
211
|
+
if (prior && prior !== ws) prior.close(1000, 'superseded authenticated connection');
|
|
212
|
+
this.sockets.set(publicKeyHex, ws);
|
|
213
|
+
this.emit('peer:connect', { peerId: publicKeyHex, publicKey, address });
|
|
214
|
+
onAuthenticated?.(publicKeyHex);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
onError?.(error);
|
|
217
|
+
ws.close(1008, 'authentication failed');
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!receivedBinaryFrame(data, isBinary)) {
|
|
222
|
+
ws.close(1003, 'GhostMesh packets must be binary');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const packetBuffer = Buffer.from(data);
|
|
226
|
+
this.emit('packet', { packetBuffer, fromPeerId: state.peerId, transportId: this.id });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
ws.on('close', () => {
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
this.pendingSockets.delete(ws);
|
|
232
|
+
if (!state.authenticated) onError?.(new Error('WebSocket peer closed before authentication completed'));
|
|
233
|
+
if (state.peerId && this.sockets.get(state.peerId) === ws) {
|
|
234
|
+
this.sockets.delete(state.peerId);
|
|
235
|
+
this.emit('peer:disconnect', { peerId: state.peerId });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
ws.on('error', (error) => {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
this.pendingSockets.delete(ws);
|
|
241
|
+
onError?.(error);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async connectToPeer(address) {
|
|
246
|
+
return await new Promise((resolve, reject) => {
|
|
247
|
+
const url = address.startsWith('ws://') || address.startsWith('wss://') ? address : `ws://${address}`;
|
|
248
|
+
const ws = new WebSocket(url, { maxPayload: MAX_PACKET_SIZE });
|
|
249
|
+
let settled = false;
|
|
250
|
+
const finish = (fn, value) => {
|
|
251
|
+
if (settled) return;
|
|
252
|
+
settled = true;
|
|
253
|
+
fn(value);
|
|
254
|
+
};
|
|
255
|
+
ws.once('open', () => this.setupSocket(
|
|
256
|
+
ws,
|
|
257
|
+
address,
|
|
258
|
+
(peerId) => finish(resolve, peerId),
|
|
259
|
+
(error) => finish(reject, error),
|
|
260
|
+
));
|
|
261
|
+
ws.once('error', (error) => finish(reject, error));
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async send(rawPacketBuffer, peerId) {
|
|
266
|
+
const ws = this.sockets.get(peerId);
|
|
267
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
268
|
+
ws.send(rawPacketBuffer, { binary: true });
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async broadcast(rawPacketBuffer) {
|
|
273
|
+
let sent = 0;
|
|
274
|
+
for (const ws of this.sockets.values()) {
|
|
275
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
276
|
+
ws.send(rawPacketBuffer, { binary: true });
|
|
277
|
+
sent++;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return sent > 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async stop() {
|
|
284
|
+
await super.stop();
|
|
285
|
+
for (const ws of [...this.sockets.values(), ...this.pendingSockets]) {
|
|
286
|
+
try { ws.close(); } catch {}
|
|
287
|
+
}
|
|
288
|
+
this.sockets.clear();
|
|
289
|
+
this.pendingSockets.clear();
|
|
290
|
+
if (this.server) {
|
|
291
|
+
await new Promise((resolve) => this.server.close(resolve));
|
|
292
|
+
this.server = null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
getStats() {
|
|
297
|
+
return {
|
|
298
|
+
id: this.id,
|
|
299
|
+
running: this.running,
|
|
300
|
+
listenPort: this.actualPort,
|
|
301
|
+
connectedPeers: this.sockets.size,
|
|
302
|
+
pendingHandshakes: this.pendingSockets.size,
|
|
303
|
+
maxConnections: this.maxConnections,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|