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,618 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
|
|
4
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
PACKET_TYPES,
|
|
8
|
+
decodePacket,
|
|
9
|
+
verifyPacketSignature,
|
|
10
|
+
} from '../../../packages/mesh/src/index.js';
|
|
11
|
+
|
|
12
|
+
export const FEDERATION_SERVE_PATH = '/ghostmesh/federation';
|
|
13
|
+
export const FEDERATION_STORAGE_ADAPTER_SCHEMA = 'enigma.federation_storage_adapter.v1';
|
|
14
|
+
|
|
15
|
+
const FEDERATION_PACKET_TYPES = new Set([
|
|
16
|
+
PACKET_TYPES.CAPABILITY_GRANT,
|
|
17
|
+
PACKET_TYPES.FEDERATED_QUERY_REQUEST,
|
|
18
|
+
PACKET_TYPES.FEDERATED_QUERY_RESPONSE,
|
|
19
|
+
PACKET_TYPES.CAPABILITY_REVOCATION,
|
|
20
|
+
]);
|
|
21
|
+
const PRODUCTION_MODES = new Set(['production', 'hosted', 'byoc', 'customer_byoc', 'on_prem', 'onprem']);
|
|
22
|
+
const PUBLIC_KEY_RE = /^[0-9a-f]{64}$/;
|
|
23
|
+
const PEER_ID_RE = /^[A-Za-z0-9._:@/-]{1,128}$/;
|
|
24
|
+
const DEFAULT_MAX_FRAME_BYTES = 1024 * 1024;
|
|
25
|
+
const DEFAULT_MAX_BUFFERED_BYTES = 2 * 1024 * 1024;
|
|
26
|
+
const DEFAULT_MAX_QUEUED_BYTES = 2 * 1024 * 1024;
|
|
27
|
+
const DEFAULT_MAX_QUEUED_FRAMES = 64;
|
|
28
|
+
const DEFAULT_RECONNECT_ATTEMPTS = 3;
|
|
29
|
+
const DEFAULT_RECONNECT_DELAY_MS = 100;
|
|
30
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 5000;
|
|
31
|
+
|
|
32
|
+
function nonEmptyString(value) {
|
|
33
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function positiveSafeInteger(value, fallback, name) {
|
|
37
|
+
if (value === undefined) return fallback;
|
|
38
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${name} must be a positive safe integer`);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizePublicKey(value, name = 'public key') {
|
|
43
|
+
const key = Buffer.isBuffer(value) || value instanceof Uint8Array
|
|
44
|
+
? Buffer.from(value).toString('hex')
|
|
45
|
+
: String(value ?? '').toLowerCase();
|
|
46
|
+
if (!PUBLIC_KEY_RE.test(key)) throw new Error(`${name} must be a 32-byte hex value`);
|
|
47
|
+
return key;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizePeerId(value) {
|
|
51
|
+
const peerId = String(value ?? '');
|
|
52
|
+
if (!PEER_ID_RE.test(peerId)) throw new Error('authenticated peer id is invalid');
|
|
53
|
+
return peerId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function headerValue(headers, name) {
|
|
57
|
+
const value = headers?.[name.toLowerCase()];
|
|
58
|
+
return Array.isArray(value) ? value[0] : value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function constantTimeStringEqual(left, right) {
|
|
62
|
+
const leftHash = createHash('sha256').update(String(left)).digest();
|
|
63
|
+
const rightHash = createHash('sha256').update(String(right)).digest();
|
|
64
|
+
return timingSafeEqual(leftHash, rightHash);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateFederationFrame(value, maxFrameBytes, expectedSenderPublicKey) {
|
|
68
|
+
const frame = Buffer.from(value);
|
|
69
|
+
if (frame.length === 0 || frame.length > maxFrameBytes) {
|
|
70
|
+
throw new Error('federation frame exceeds the authenticated binary frame limit');
|
|
71
|
+
}
|
|
72
|
+
const decoded = decodePacket(frame);
|
|
73
|
+
if (!FEDERATION_PACKET_TYPES.has(decoded.type)) throw new Error('frame is not a GhostMesh federation packet');
|
|
74
|
+
if (!verifyPacketSignature(frame)) throw new Error('federation frame signature is invalid');
|
|
75
|
+
if (expectedSenderPublicKey !== undefined &&
|
|
76
|
+
!constantTimeStringEqual(decoded.senderPubkey.toString('hex'), expectedSenderPublicKey)) {
|
|
77
|
+
throw new Error('federation frame sender is not bound to the authenticated connection identity');
|
|
78
|
+
}
|
|
79
|
+
return { frame, decoded };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function closeSocket(socket, code, reason) {
|
|
83
|
+
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
|
84
|
+
socket.close(code, String(reason).slice(0, 123));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function receivedBinaryFrame(data, isBinary) {
|
|
89
|
+
if (typeof isBinary === 'boolean') return isBinary;
|
|
90
|
+
return Buffer.isBuffer(data) || ArrayBuffer.isView(data) || data instanceof ArrayBuffer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sendBinary(socket, frame, maxBufferedBytes) {
|
|
94
|
+
if (socket.readyState !== WebSocket.OPEN) return Promise.resolve(false);
|
|
95
|
+
if (socket.bufferedAmount + frame.length > maxBufferedBytes) return Promise.resolve(false);
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
socket.send(frame, { binary: true }, (error) => resolve(!error));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function storageRecord(direction, peerId, decoded, frame) {
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
schema: 'enigma.federation_opaque_frame.v1',
|
|
104
|
+
direction,
|
|
105
|
+
peer_id: peerId,
|
|
106
|
+
packet_type: decoded.typeName,
|
|
107
|
+
destination: decoded.destinationHex,
|
|
108
|
+
sender_public_key: decoded.senderPubkey.toString('hex'),
|
|
109
|
+
frame: Buffer.from(frame),
|
|
110
|
+
received_at: new Date().toISOString(),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function validateFederationStorageAdapter(adapter, { requireDurable = false } = {}) {
|
|
115
|
+
if (!adapter || typeof adapter !== 'object') throw new TypeError('federation storage adapter is required');
|
|
116
|
+
if (adapter.schema !== FEDERATION_STORAGE_ADAPTER_SCHEMA) {
|
|
117
|
+
throw new TypeError(`federation storage adapter schema must be ${FEDERATION_STORAGE_ADAPTER_SCHEMA}`);
|
|
118
|
+
}
|
|
119
|
+
if (!nonEmptyString(adapter.ref)) throw new TypeError('federation storage adapter ref is required');
|
|
120
|
+
if (typeof adapter.isReady !== 'function' || typeof adapter.appendOpaqueFrame !== 'function') {
|
|
121
|
+
throw new TypeError('federation storage adapter must implement isReady() and appendOpaqueFrame(record)');
|
|
122
|
+
}
|
|
123
|
+
if (requireDurable && adapter.durable !== true) {
|
|
124
|
+
throw new TypeError('production federation serve mode requires a durable external storage adapter');
|
|
125
|
+
}
|
|
126
|
+
return adapter;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export class MemoryFederationStorageAdapter {
|
|
130
|
+
constructor(options = {}) {
|
|
131
|
+
this.schema = FEDERATION_STORAGE_ADAPTER_SCHEMA;
|
|
132
|
+
this.ref = options.ref ?? 'memory://explicit-federation-frames';
|
|
133
|
+
this.durable = false;
|
|
134
|
+
this.maxRecords = positiveSafeInteger(options.maxRecords, 1024, 'maxRecords');
|
|
135
|
+
this.records = [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async isReady() {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async appendOpaqueFrame(record) {
|
|
143
|
+
if (!Buffer.isBuffer(record?.frame)) throw new TypeError('opaque federation storage record frame must be a Buffer');
|
|
144
|
+
if (this.records.length >= this.maxRecords) this.records.shift();
|
|
145
|
+
this.records.push({ ...record, frame: Buffer.from(record.frame) });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function createMemoryFederationStorageAdapter(options) {
|
|
150
|
+
return new MemoryFederationStorageAdapter(options);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export class FederationServeTransport extends EventEmitter {
|
|
154
|
+
constructor(options = {}) {
|
|
155
|
+
super();
|
|
156
|
+
this.id = options.id ?? 'federation-serve-transport';
|
|
157
|
+
this.running = false;
|
|
158
|
+
this._sender = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async start() {
|
|
162
|
+
this.running = true;
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async stop() {
|
|
167
|
+
this.running = false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async send(packetBuffer, peerId) {
|
|
171
|
+
if (!this.running || typeof this._sender !== 'function') return false;
|
|
172
|
+
return await this._sender(packetBuffer, peerId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class AuthenticatedWebSocketFederationTransport extends EventEmitter {
|
|
177
|
+
constructor(options = {}) {
|
|
178
|
+
super();
|
|
179
|
+
if (!nonEmptyString(options.url)) throw new TypeError('federation WebSocket url is required');
|
|
180
|
+
this.id = options.id ?? 'authenticated-ws-federation-client';
|
|
181
|
+
this.url = options.url;
|
|
182
|
+
this.peerId = normalizePeerId(options.peerId);
|
|
183
|
+
this.publicKey = normalizePublicKey(options.publicKey, 'client public key');
|
|
184
|
+
if (!nonEmptyString(options.authorization)) throw new TypeError('client authorization is required');
|
|
185
|
+
this.authorization = options.authorization;
|
|
186
|
+
this.maxFrameBytes = positiveSafeInteger(options.maxFrameBytes, DEFAULT_MAX_FRAME_BYTES, 'maxFrameBytes');
|
|
187
|
+
this.maxBufferedBytes = positiveSafeInteger(options.maxBufferedBytes, DEFAULT_MAX_BUFFERED_BYTES, 'maxBufferedBytes');
|
|
188
|
+
this.connectTimeoutMs = positiveSafeInteger(options.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS, 'connectTimeoutMs');
|
|
189
|
+
this.running = false;
|
|
190
|
+
this.socket = null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async start() {
|
|
194
|
+
if (this.running && this.socket?.readyState === WebSocket.OPEN) return this;
|
|
195
|
+
const socket = new WebSocket(this.url, {
|
|
196
|
+
headers: {
|
|
197
|
+
authorization: this.authorization,
|
|
198
|
+
'x-enigma-peer-id': this.peerId,
|
|
199
|
+
'x-enigma-public-key': this.publicKey,
|
|
200
|
+
},
|
|
201
|
+
maxPayload: this.maxFrameBytes,
|
|
202
|
+
perMessageDeflate: false,
|
|
203
|
+
});
|
|
204
|
+
this.socket = socket;
|
|
205
|
+
await new Promise((resolve, reject) => {
|
|
206
|
+
const timer = setTimeout(() => {
|
|
207
|
+
socket.terminate();
|
|
208
|
+
reject(new Error('federation WebSocket connection timed out'));
|
|
209
|
+
}, this.connectTimeoutMs);
|
|
210
|
+
timer.unref?.();
|
|
211
|
+
socket.once('open', () => {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
resolve();
|
|
214
|
+
});
|
|
215
|
+
socket.once('error', (error) => {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
reject(error);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
this.running = true;
|
|
221
|
+
socket.on('message', (data, isBinary) => {
|
|
222
|
+
try {
|
|
223
|
+
if (!receivedBinaryFrame(data, isBinary)) throw new Error('federation WebSocket accepts binary frames only');
|
|
224
|
+
const { frame } = validateFederationFrame(data, this.maxFrameBytes);
|
|
225
|
+
this.emit('packet', { packetBuffer: frame, fromPeerId: 'federation-service', transportId: this.id });
|
|
226
|
+
} catch (error) {
|
|
227
|
+
this.emit('rejected', { error });
|
|
228
|
+
closeSocket(socket, 1008, 'invalid federation frame');
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
socket.once('close', () => {
|
|
232
|
+
this.running = false;
|
|
233
|
+
this.emit('disconnect', { peerId: 'federation-service' });
|
|
234
|
+
});
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async stop() {
|
|
239
|
+
const socket = this.socket;
|
|
240
|
+
this.running = false;
|
|
241
|
+
this.socket = null;
|
|
242
|
+
if (!socket || socket.readyState === WebSocket.CLOSED) return;
|
|
243
|
+
await new Promise((resolve) => {
|
|
244
|
+
socket.once('close', resolve);
|
|
245
|
+
closeSocket(socket, 1000, 'transport stopped');
|
|
246
|
+
setTimeout(resolve, 250).unref?.();
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async send(packetBuffer) {
|
|
251
|
+
if (!this.running || !this.socket) return false;
|
|
252
|
+
const { frame } = validateFederationFrame(packetBuffer, this.maxFrameBytes, this.publicKey);
|
|
253
|
+
return await sendBinary(this.socket, frame, this.maxBufferedBytes);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function authenticateRequest(authenticator, request) {
|
|
258
|
+
const declaredPeerId = headerValue(request.headers, 'x-enigma-peer-id');
|
|
259
|
+
const declaredPublicKey = headerValue(request.headers, 'x-enigma-public-key');
|
|
260
|
+
if (!nonEmptyString(declaredPeerId) || !nonEmptyString(declaredPublicKey)) {
|
|
261
|
+
throw new Error('authenticated federation identity headers are required');
|
|
262
|
+
}
|
|
263
|
+
const result = await authenticator.authenticate({
|
|
264
|
+
authorization: headerValue(request.headers, 'authorization'),
|
|
265
|
+
peerId: declaredPeerId,
|
|
266
|
+
publicKey: declaredPublicKey,
|
|
267
|
+
remoteAddress: request.socket?.remoteAddress,
|
|
268
|
+
});
|
|
269
|
+
if (!result || result.ok === false) throw new Error('federation authentication failed');
|
|
270
|
+
const peerId = normalizePeerId(result.peerId ?? result.peer_id);
|
|
271
|
+
const publicKey = normalizePublicKey(result.publicKey ?? result.public_key, 'authenticated public key');
|
|
272
|
+
if (!constantTimeStringEqual(peerId, declaredPeerId) ||
|
|
273
|
+
!constantTimeStringEqual(publicKey, normalizePublicKey(declaredPublicKey, 'declared public key'))) {
|
|
274
|
+
throw new Error('declared federation identity does not match authenticated identity');
|
|
275
|
+
}
|
|
276
|
+
return { peerId, publicKey };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function rejectUpgrade(socket, statusCode, reason) {
|
|
280
|
+
if (!socket.writable) return;
|
|
281
|
+
const body = `${reason}\n`;
|
|
282
|
+
socket.end(
|
|
283
|
+
`HTTP/1.1 ${statusCode} ${statusCode === 401 ? 'Unauthorized' : 'Bad Request'}\r\n` +
|
|
284
|
+
'Connection: close\r\nContent-Type: text/plain; charset=utf-8\r\n' +
|
|
285
|
+
`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export class FederationServeRuntime extends EventEmitter {
|
|
290
|
+
constructor(server, options = {}) {
|
|
291
|
+
super();
|
|
292
|
+
if (!server || typeof server.on !== 'function') throw new TypeError('HTTP server is required');
|
|
293
|
+
const mode = String(options.mode ?? 'local').toLowerCase();
|
|
294
|
+
const production = PRODUCTION_MODES.has(mode);
|
|
295
|
+
const storage = validateFederationStorageAdapter(options.storage, { requireDurable: production });
|
|
296
|
+
const authenticator = options.authenticator;
|
|
297
|
+
if (!authenticator || typeof authenticator.authenticate !== 'function') {
|
|
298
|
+
throw new TypeError('federation authenticator with authenticate() is required');
|
|
299
|
+
}
|
|
300
|
+
const authRef = options.authRef ?? authenticator.ref;
|
|
301
|
+
if (!nonEmptyString(authRef)) throw new TypeError('federation auth ref is required');
|
|
302
|
+
if (production && !nonEmptyString(options.kmsRef)) {
|
|
303
|
+
throw new TypeError('production federation serve mode requires a KMS/key-custody ref');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.server = server;
|
|
307
|
+
this.mode = mode;
|
|
308
|
+
this.production = production;
|
|
309
|
+
this.path = options.path ?? FEDERATION_SERVE_PATH;
|
|
310
|
+
this.storage = storage;
|
|
311
|
+
this.authenticator = authenticator;
|
|
312
|
+
this.authRef = authRef;
|
|
313
|
+
this.kmsRef = options.kmsRef ?? null;
|
|
314
|
+
this.maxFrameBytes = positiveSafeInteger(options.maxFrameBytes, DEFAULT_MAX_FRAME_BYTES, 'maxFrameBytes');
|
|
315
|
+
this.maxBufferedBytes = positiveSafeInteger(options.maxBufferedBytes, DEFAULT_MAX_BUFFERED_BYTES, 'maxBufferedBytes');
|
|
316
|
+
this.maxQueuedBytes = positiveSafeInteger(options.maxQueuedBytes, DEFAULT_MAX_QUEUED_BYTES, 'maxQueuedBytes');
|
|
317
|
+
this.maxQueuedFrames = positiveSafeInteger(options.maxQueuedFrames, DEFAULT_MAX_QUEUED_FRAMES, 'maxQueuedFrames');
|
|
318
|
+
this.reconnectAttempts = positiveSafeInteger(options.reconnectAttempts, DEFAULT_RECONNECT_ATTEMPTS, 'reconnectAttempts');
|
|
319
|
+
this.reconnectDelayMs = positiveSafeInteger(options.reconnectDelayMs, DEFAULT_RECONNECT_DELAY_MS, 'reconnectDelayMs');
|
|
320
|
+
this.connectTimeoutMs = positiveSafeInteger(options.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS, 'connectTimeoutMs');
|
|
321
|
+
this.upstreamUrl = options.upstreamUrl ?? null;
|
|
322
|
+
this.upstreamHeaders = options.upstreamHeaders ?? null;
|
|
323
|
+
this.expectedUpstreamPublicKeys = new Set((options.expectedUpstreamPublicKeys ?? []).map((value) => normalizePublicKey(value, 'expected upstream public key')));
|
|
324
|
+
if (production && this.upstreamUrl !== null && this.expectedUpstreamPublicKeys.size === 0) {
|
|
325
|
+
throw new TypeError('production federation forwarding requires expected upstream bridge public keys');
|
|
326
|
+
}
|
|
327
|
+
this.connections = new Map();
|
|
328
|
+
this.forwardConnections = new Set();
|
|
329
|
+
this.storageReady = false;
|
|
330
|
+
this.authReady = false;
|
|
331
|
+
this.running = false;
|
|
332
|
+
this.upstreamReady = this.upstreamUrl === null;
|
|
333
|
+
this.lastDependencyError = null;
|
|
334
|
+
this.transport = options.transport ?? new FederationServeTransport();
|
|
335
|
+
if (!(this.transport instanceof EventEmitter) || typeof this.transport.send !== 'function') {
|
|
336
|
+
throw new TypeError('federation bridge transport must be EventEmitter-compatible and implement send()');
|
|
337
|
+
}
|
|
338
|
+
this.bridge = options.bridge ?? null;
|
|
339
|
+
if (this.upstreamUrl === null && !this.bridge) throw new TypeError('terminal federation serve mode requires a GhostMesh federation bridge');
|
|
340
|
+
if (this.upstreamUrl !== null && this.bridge) throw new TypeError('federation serve mode cannot configure both an upstream and a terminal bridge');
|
|
341
|
+
if (this.bridge) {
|
|
342
|
+
if (this.bridge.started) throw new Error('federation bridge must not be started before attaching serve mode');
|
|
343
|
+
this.bridge.transport = this.transport;
|
|
344
|
+
this.bridge.manageTransport = false;
|
|
345
|
+
}
|
|
346
|
+
this.transport._sender = (packetBuffer, peerId) => this.sendToPeer(packetBuffer, peerId);
|
|
347
|
+
|
|
348
|
+
this.webSocketServer = new WebSocketServer({
|
|
349
|
+
noServer: true,
|
|
350
|
+
maxPayload: this.maxFrameBytes,
|
|
351
|
+
perMessageDeflate: false,
|
|
352
|
+
clientTracking: false,
|
|
353
|
+
});
|
|
354
|
+
this._upgradeHandler = (request, socket, head) => void this.handleUpgrade(request, socket, head);
|
|
355
|
+
server.on('upgrade', this._upgradeHandler);
|
|
356
|
+
server.once('listening', () => void this.start());
|
|
357
|
+
server.once('close', () => void this.stop());
|
|
358
|
+
if (server.listening) void this.start();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async refreshDependencies() {
|
|
362
|
+
try {
|
|
363
|
+
this.storageReady = await this.storage.isReady() === true;
|
|
364
|
+
this.authReady = typeof this.authenticator.isReady === 'function'
|
|
365
|
+
? await this.authenticator.isReady() === true
|
|
366
|
+
: true;
|
|
367
|
+
if (!this.storageReady) throw new Error('federation storage adapter is not ready');
|
|
368
|
+
if (!this.authReady) throw new Error('federation authenticator is not ready');
|
|
369
|
+
this.lastDependencyError = null;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
this.storageReady = false;
|
|
372
|
+
this.lastDependencyError = error instanceof Error ? error.message : 'federation dependency failed';
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async start() {
|
|
377
|
+
if (this.running) return this;
|
|
378
|
+
await this.refreshDependencies();
|
|
379
|
+
if (!this.storageReady || !this.authReady) return this;
|
|
380
|
+
if (!this.transport.running) await this.transport.start();
|
|
381
|
+
if (this.bridge && !this.bridge.started) await this.bridge.start();
|
|
382
|
+
this.running = true;
|
|
383
|
+
if (this.upstreamUrl && this.upstreamHeaders) this.startReadinessProbe();
|
|
384
|
+
return this;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async stop() {
|
|
388
|
+
if (!this.running && this.connections.size === 0) return;
|
|
389
|
+
this.running = false;
|
|
390
|
+
this.upstreamReady = this.upstreamUrl === null;
|
|
391
|
+
if (this.readinessProbe) {
|
|
392
|
+
this.readinessProbe.intentionalClose = true;
|
|
393
|
+
this.readinessProbe.socket?.terminate();
|
|
394
|
+
clearTimeout(this.readinessProbe.timer);
|
|
395
|
+
this.readinessProbe = null;
|
|
396
|
+
}
|
|
397
|
+
for (const connection of this.connections.values()) closeSocket(connection.socket, 1001, 'service stopping');
|
|
398
|
+
for (const connection of this.forwardConnections) connection.upstream?.terminate();
|
|
399
|
+
this.connections.clear();
|
|
400
|
+
this.forwardConnections.clear();
|
|
401
|
+
if (this.bridge?.started) await this.bridge.stop();
|
|
402
|
+
if (this.transport.running) await this.transport.stop();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
status() {
|
|
406
|
+
const bridgeReady = this.upstreamUrl !== null || (this.bridge?.started === true && this.transport.running === true);
|
|
407
|
+
const upstreamReady = this.upstreamUrl === null || this.upstreamReady;
|
|
408
|
+
const checks = [
|
|
409
|
+
{ check: 'federation_runtime_started', ok: this.running === true },
|
|
410
|
+
{ check: 'federation_storage', ok: this.storageReady === true },
|
|
411
|
+
{ check: 'federation_authenticator', ok: this.authReady === true },
|
|
412
|
+
{ check: 'federation_bridge', ok: bridgeReady },
|
|
413
|
+
{ check: 'federation_upstream', ok: upstreamReady },
|
|
414
|
+
];
|
|
415
|
+
return {
|
|
416
|
+
enabled: true,
|
|
417
|
+
ready: checks.every((check) => check.ok),
|
|
418
|
+
path: this.path,
|
|
419
|
+
transport: 'authenticated_websocket_binary',
|
|
420
|
+
storage_ref: this.storage.ref,
|
|
421
|
+
durable_storage: this.storage.durable === true,
|
|
422
|
+
auth_ref: this.authRef,
|
|
423
|
+
kms_ref_configured: nonEmptyString(this.kmsRef),
|
|
424
|
+
active_connections: this.connections.size,
|
|
425
|
+
checks,
|
|
426
|
+
last_dependency_error: this.lastDependencyError,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async persist(direction, peerId, decoded, frame) {
|
|
431
|
+
try {
|
|
432
|
+
await this.storage.appendOpaqueFrame(storageRecord(direction, peerId, decoded, frame));
|
|
433
|
+
} catch (error) {
|
|
434
|
+
this.storageReady = false;
|
|
435
|
+
this.lastDependencyError = error instanceof Error ? error.message : 'federation storage write failed';
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async handleUpgrade(request, socket, head) {
|
|
441
|
+
let pathname;
|
|
442
|
+
try {
|
|
443
|
+
pathname = new URL(request.url ?? '/', 'http://enigma.local').pathname;
|
|
444
|
+
} catch {
|
|
445
|
+
rejectUpgrade(socket, 400, 'invalid request path');
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (pathname !== this.path) {
|
|
449
|
+
rejectUpgrade(socket, 404, 'WebSocket path is not available');
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (!this.running || !this.storageReady || !this.authReady) {
|
|
453
|
+
rejectUpgrade(socket, 503, 'federation service is not ready');
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
let principal;
|
|
457
|
+
try {
|
|
458
|
+
principal = await authenticateRequest(this.authenticator, request);
|
|
459
|
+
} catch {
|
|
460
|
+
rejectUpgrade(socket, 401, 'federation authentication failed');
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
this.webSocketServer.handleUpgrade(request, socket, head, (webSocket) => {
|
|
464
|
+
this.acceptConnection(webSocket, principal, request);
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
acceptConnection(socket, principal, request) {
|
|
469
|
+
const existing = this.connections.get(principal.peerId);
|
|
470
|
+
if (existing) closeSocket(existing.socket, 4009, 'authenticated peer reconnected');
|
|
471
|
+
const connection = { socket, principal, request, upstream: null, queue: [], queuedBytes: 0, reconnects: 0, closed: false };
|
|
472
|
+
this.connections.set(principal.peerId, connection);
|
|
473
|
+
socket.on('message', (data, isBinary) => void this.handleClientFrame(connection, data, isBinary));
|
|
474
|
+
socket.once('close', () => {
|
|
475
|
+
connection.closed = true;
|
|
476
|
+
connection.upstream?.terminate();
|
|
477
|
+
this.forwardConnections.delete(connection);
|
|
478
|
+
if (this.connections.get(principal.peerId) === connection) this.connections.delete(principal.peerId);
|
|
479
|
+
});
|
|
480
|
+
socket.on('error', () => undefined);
|
|
481
|
+
if (this.upstreamUrl) this.connectUpstream(connection);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async handleClientFrame(connection, data, isBinary) {
|
|
485
|
+
try {
|
|
486
|
+
if (!receivedBinaryFrame(data, isBinary)) throw new Error('federation WebSocket accepts binary frames only');
|
|
487
|
+
const { frame, decoded } = validateFederationFrame(data, this.maxFrameBytes, connection.principal.publicKey);
|
|
488
|
+
await this.persist('ingress', connection.principal.peerId, decoded, frame);
|
|
489
|
+
if (this.upstreamUrl) {
|
|
490
|
+
if (connection.upstream?.readyState === WebSocket.OPEN) {
|
|
491
|
+
const sent = await sendBinary(connection.upstream, frame, this.maxBufferedBytes);
|
|
492
|
+
if (!sent) throw new Error('federation upstream backpressure limit exceeded');
|
|
493
|
+
} else {
|
|
494
|
+
this.enqueueFrame(connection, frame);
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
this.transport.emit('packet', {
|
|
498
|
+
packetBuffer: frame,
|
|
499
|
+
fromPeerId: connection.principal.peerId,
|
|
500
|
+
transportId: this.transport.id,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
this.lastDependencyError = error instanceof Error ? error.message : 'invalid federation client frame';
|
|
505
|
+
this.emit('frameRejected', { direction: 'ingress', peerId: connection.principal.peerId, error });
|
|
506
|
+
closeSocket(connection.socket, 1008, 'invalid federation frame');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
enqueueFrame(connection, frame) {
|
|
511
|
+
if (connection.queue.length >= this.maxQueuedFrames || connection.queuedBytes + frame.length > this.maxQueuedBytes) {
|
|
512
|
+
throw new Error('federation reconnect queue limit exceeded');
|
|
513
|
+
}
|
|
514
|
+
connection.queue.push(Buffer.from(frame));
|
|
515
|
+
connection.queuedBytes += frame.length;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
connectUpstream(connection) {
|
|
519
|
+
if (connection.closed) return;
|
|
520
|
+
const headers = {
|
|
521
|
+
authorization: headerValue(connection.request.headers, 'authorization'),
|
|
522
|
+
'x-enigma-peer-id': connection.principal.peerId,
|
|
523
|
+
'x-enigma-public-key': connection.principal.publicKey,
|
|
524
|
+
};
|
|
525
|
+
const upstream = new WebSocket(this.upstreamUrl, {
|
|
526
|
+
headers,
|
|
527
|
+
maxPayload: this.maxFrameBytes,
|
|
528
|
+
perMessageDeflate: false,
|
|
529
|
+
handshakeTimeout: this.connectTimeoutMs,
|
|
530
|
+
});
|
|
531
|
+
connection.upstream = upstream;
|
|
532
|
+
this.forwardConnections.add(connection);
|
|
533
|
+
upstream.once('open', async () => {
|
|
534
|
+
connection.reconnects = 0;
|
|
535
|
+
this.upstreamReady = true;
|
|
536
|
+
try {
|
|
537
|
+
while (connection.queue.length > 0) {
|
|
538
|
+
const frame = connection.queue.shift();
|
|
539
|
+
connection.queuedBytes -= frame.length;
|
|
540
|
+
if (!await sendBinary(upstream, frame, this.maxBufferedBytes)) {
|
|
541
|
+
throw new Error('federation upstream backpressure limit exceeded');
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} catch {
|
|
545
|
+
closeSocket(connection.socket, 1013, 'federation upstream backpressure');
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
upstream.on('message', (data, isBinary) => void this.handleUpstreamFrame(connection, data, isBinary));
|
|
549
|
+
upstream.on('error', () => undefined);
|
|
550
|
+
upstream.once('close', () => {
|
|
551
|
+
if (connection.upstream === upstream) connection.upstream = null;
|
|
552
|
+
this.upstreamReady = [...this.forwardConnections].some((item) => item.upstream?.readyState === WebSocket.OPEN) || this.readinessProbe?.socket?.readyState === WebSocket.OPEN;
|
|
553
|
+
if (connection.closed) return;
|
|
554
|
+
connection.reconnects += 1;
|
|
555
|
+
if (connection.reconnects > this.reconnectAttempts) {
|
|
556
|
+
closeSocket(connection.socket, 1013, 'federation upstream unavailable');
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
setTimeout(() => this.connectUpstream(connection), this.reconnectDelayMs * connection.reconnects).unref?.();
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async handleUpstreamFrame(connection, data, isBinary) {
|
|
564
|
+
try {
|
|
565
|
+
if (!receivedBinaryFrame(data, isBinary)) throw new Error('federation upstream sent a non-binary frame');
|
|
566
|
+
const { frame, decoded } = validateFederationFrame(data, this.maxFrameBytes);
|
|
567
|
+
if (this.expectedUpstreamPublicKeys.size > 0 && !this.expectedUpstreamPublicKeys.has(decoded.senderPubkey.toString('hex'))) {
|
|
568
|
+
throw new Error('federation upstream sender is not trusted');
|
|
569
|
+
}
|
|
570
|
+
await this.persist('egress', connection.principal.peerId, decoded, frame);
|
|
571
|
+
if (!await sendBinary(connection.socket, frame, this.maxBufferedBytes)) {
|
|
572
|
+
throw new Error('federation client backpressure limit exceeded');
|
|
573
|
+
}
|
|
574
|
+
} catch (error) {
|
|
575
|
+
this.lastDependencyError = error instanceof Error ? error.message : 'invalid federation upstream frame';
|
|
576
|
+
this.emit('frameRejected', { direction: 'upstream', peerId: connection.principal.peerId, error });
|
|
577
|
+
closeSocket(connection.socket, 1008, 'invalid federation upstream frame');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async sendToPeer(packetBuffer, peerId) {
|
|
582
|
+
const connection = this.connections.get(peerId);
|
|
583
|
+
if (!connection) return false;
|
|
584
|
+
const { frame, decoded } = validateFederationFrame(packetBuffer, this.maxFrameBytes);
|
|
585
|
+
await this.persist('egress', peerId, decoded, frame);
|
|
586
|
+
return await sendBinary(connection.socket, frame, this.maxBufferedBytes);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
startReadinessProbe() {
|
|
590
|
+
if (!this.upstreamUrl || !this.upstreamHeaders || this.readinessProbe) return;
|
|
591
|
+
const probe = { socket: null, timer: null, intentionalClose: false };
|
|
592
|
+
this.readinessProbe = probe;
|
|
593
|
+
const connect = () => {
|
|
594
|
+
if (!this.running || probe.intentionalClose) return;
|
|
595
|
+
const socket = new WebSocket(this.upstreamUrl, {
|
|
596
|
+
headers: this.upstreamHeaders,
|
|
597
|
+
maxPayload: this.maxFrameBytes,
|
|
598
|
+
perMessageDeflate: false,
|
|
599
|
+
handshakeTimeout: this.connectTimeoutMs,
|
|
600
|
+
});
|
|
601
|
+
probe.socket = socket;
|
|
602
|
+
socket.once('open', () => { this.upstreamReady = true; });
|
|
603
|
+
socket.on('message', () => closeSocket(socket, 1008, 'readiness connection cannot carry frames'));
|
|
604
|
+
socket.on('error', () => undefined);
|
|
605
|
+
socket.once('close', () => {
|
|
606
|
+
if (probe.intentionalClose) return;
|
|
607
|
+
this.upstreamReady = [...this.forwardConnections].some((item) => item.upstream?.readyState === WebSocket.OPEN);
|
|
608
|
+
probe.timer = setTimeout(connect, this.reconnectDelayMs);
|
|
609
|
+
probe.timer.unref?.();
|
|
610
|
+
});
|
|
611
|
+
};
|
|
612
|
+
connect();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function attachFederationServeMode(server, options) {
|
|
617
|
+
return new FederationServeRuntime(server, options);
|
|
618
|
+
}
|