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,100 @@
|
|
|
1
|
+
// Enigma Vector Benchmark Suite.
|
|
2
|
+
// Benchmarks embedding generation throughput, semantic search latency,
|
|
3
|
+
// and Int8 quantization memory savings and cosine fidelity.
|
|
4
|
+
import { performance } from 'node:perf_hooks';
|
|
5
|
+
import {
|
|
6
|
+
computeLexicalEmbedding,
|
|
7
|
+
cosineSimilarity,
|
|
8
|
+
quantizeVector,
|
|
9
|
+
dequantizeVector,
|
|
10
|
+
EncryptedVectorStore,
|
|
11
|
+
} from '../../rag/src/index.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} [options]
|
|
15
|
+
* @param {number} [options.itemCount]
|
|
16
|
+
* @param {number} [options.queryCount]
|
|
17
|
+
* @param {Object} [options.embeddingProvider]
|
|
18
|
+
* @param {EncryptedVectorStore} [options.vectorStore]
|
|
19
|
+
*/
|
|
20
|
+
export async function runVectorBenchmark(options = {}) {
|
|
21
|
+
const itemCount = Math.max(10, options.itemCount || 100);
|
|
22
|
+
const queryCount = Math.max(5, options.queryCount || 50);
|
|
23
|
+
|
|
24
|
+
const sampleCorpus = [
|
|
25
|
+
'Always enforce 0.5% maximum slippage on Jupiter DEX Solana swaps',
|
|
26
|
+
'Verify on-chain zero-knowledge commitments before releasing private memory data',
|
|
27
|
+
'Use Aztec Indexed Merkle Trees for constant-time non-membership nullifier proof verification',
|
|
28
|
+
'GhostMesh peer communications require X25519 ECDH key exchange with AES-256-GCM AEAD',
|
|
29
|
+
'Autonomous agents must fail closed when cryptographic custody is absent',
|
|
30
|
+
'Token budget allocation for prompt context compilation should prioritize high importance memory atoms',
|
|
31
|
+
'Sealed Sale fair exchange atomically reveals data encryption keys upon Solana claim settlement',
|
|
32
|
+
'Poseidon hash sponge arithmetic over BN254 scalar field guarantees SNARK proof efficiency',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// 1. Benchmark Embedding Throughput
|
|
36
|
+
const startEmbed = performance.now();
|
|
37
|
+
const embeddings = [];
|
|
38
|
+
for (let i = 0; i < itemCount; i++) {
|
|
39
|
+
const text = `${sampleCorpus[i % sampleCorpus.length]} (instance #${i})`;
|
|
40
|
+
embeddings.push(computeLexicalEmbedding(text));
|
|
41
|
+
}
|
|
42
|
+
const embedDurationMs = performance.now() - startEmbed;
|
|
43
|
+
const embedThroughput = Math.round((itemCount / (embedDurationMs / 1000)) * 10) / 10;
|
|
44
|
+
|
|
45
|
+
// 2. Benchmark Quantization Memory & Fidelity
|
|
46
|
+
let totalFidelity = 0;
|
|
47
|
+
for (let i = 0; i < Math.min(itemCount, 50); i++) {
|
|
48
|
+
const orig = embeddings[i];
|
|
49
|
+
const quantized = quantizeVector(orig);
|
|
50
|
+
const restored = dequantizeVector(quantized);
|
|
51
|
+
totalFidelity += cosineSimilarity(orig, restored);
|
|
52
|
+
}
|
|
53
|
+
const meanQuantizationFidelity = Math.round((totalFidelity / Math.min(itemCount, 50)) * 10000) / 10000;
|
|
54
|
+
const floatBytes = itemCount * 384 * 4;
|
|
55
|
+
const int8Bytes = itemCount * 384 * 1;
|
|
56
|
+
const memoryCompressionPercent = Math.round(((floatBytes - int8Bytes) / floatBytes) * 100);
|
|
57
|
+
|
|
58
|
+
// 3. Benchmark Vector Store Search Latency
|
|
59
|
+
const store = options.vectorStore || new EncryptedVectorStore({
|
|
60
|
+
quantize: true,
|
|
61
|
+
...(options.embeddingProvider ? { embeddingProvider: options.embeddingProvider } : {}),
|
|
62
|
+
});
|
|
63
|
+
for (let i = 0; i < itemCount; i++) {
|
|
64
|
+
const text = `${sampleCorpus[i % sampleCorpus.length]} (instance #${i})`;
|
|
65
|
+
await store.add(`item_${i}`, text, { scope: 'benchmark' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const startQuery = performance.now();
|
|
69
|
+
for (let q = 0; q < queryCount; q++) {
|
|
70
|
+
const query = sampleCorpus[q % sampleCorpus.length];
|
|
71
|
+
await store.search(query, { topK: 5 });
|
|
72
|
+
}
|
|
73
|
+
const queryDurationMs = performance.now() - startQuery;
|
|
74
|
+
const queryThroughput = Math.round((queryCount / (queryDurationMs / 1000)) * 10) / 10;
|
|
75
|
+
const meanLatencyMs = Math.round((queryDurationMs / queryCount) * 1000) / 1000;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
benchmark: 'enigma_vector_performance_v1',
|
|
80
|
+
itemCount,
|
|
81
|
+
queryCount,
|
|
82
|
+
embedding: {
|
|
83
|
+
totalDurationMs: Math.round(embedDurationMs * 100) / 100,
|
|
84
|
+
throughputPerSecond: embedThroughput,
|
|
85
|
+
},
|
|
86
|
+
quantization: {
|
|
87
|
+
dimension: 384,
|
|
88
|
+
float32Bytes: floatBytes,
|
|
89
|
+
int8Bytes: int8Bytes,
|
|
90
|
+
memoryReductionPercent: memoryCompressionPercent,
|
|
91
|
+
meanCosineFidelity: meanQuantizationFidelity,
|
|
92
|
+
},
|
|
93
|
+
retrieval: {
|
|
94
|
+
totalDurationMs: Math.round(queryDurationMs * 100) / 100,
|
|
95
|
+
queriesPerSecond: queryThroughput,
|
|
96
|
+
meanLatencyMs,
|
|
97
|
+
},
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { randomBytes, randomUUID, scrypt as scryptCallback, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile, rename } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
export const ENIGMA_API_KEY_STORE_SCHEMA = 'enigma.api_key_store.v1';
|
|
7
|
+
export const ENIGMA_API_KEY_SCHEMA = 'enigma.api_key.v1';
|
|
8
|
+
const scrypt = promisify(scryptCallback);
|
|
9
|
+
const KEY_PATTERN = /^enigma_sk_(live|test)_([A-Za-z0-9_-]{12})_([A-Za-z0-9_-]{43})$/;
|
|
10
|
+
|
|
11
|
+
function clockIso(clock) {
|
|
12
|
+
return new Date(clock()).toISOString();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function boundedInteger(value, name, minimum, maximum, fallback) {
|
|
16
|
+
const resolved = value === undefined ? fallback : value;
|
|
17
|
+
if (!Number.isInteger(resolved) || resolved < minimum || resolved > maximum) {
|
|
18
|
+
throw new TypeError(`${name} must be an integer from ${minimum} through ${maximum}`);
|
|
19
|
+
}
|
|
20
|
+
return resolved;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requiredString(value, name, maximum = 512) {
|
|
24
|
+
if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${name} is required`);
|
|
25
|
+
const normalized = value.trim();
|
|
26
|
+
if (normalized.length > maximum) throw new TypeError(`${name} exceeds ${maximum} characters`);
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeScopes(value, supportedScopes) {
|
|
31
|
+
const scopes = [...new Set((value || []).map((scope) => requiredString(scope, 'scope', 128)))].sort();
|
|
32
|
+
if (!scopes.length) throw new TypeError('at least one API key scope is required');
|
|
33
|
+
if (scopes.some((scope) => !supportedScopes.has(scope))) throw new TypeError('API key contains an unsupported scope');
|
|
34
|
+
return scopes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function emptyState() {
|
|
38
|
+
return { schema: ENIGMA_API_KEY_STORE_SCHEMA, revision: 0, keys: {}, prefixIndex: {}, audit: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function publicRecord(record) {
|
|
42
|
+
return {
|
|
43
|
+
schema: ENIGMA_API_KEY_SCHEMA,
|
|
44
|
+
id: record.id,
|
|
45
|
+
prefix: record.prefix,
|
|
46
|
+
environment: record.environment,
|
|
47
|
+
label: record.label,
|
|
48
|
+
ownerId: record.ownerId,
|
|
49
|
+
tenantId: record.tenantId,
|
|
50
|
+
scopes: [...record.scopes],
|
|
51
|
+
createdAt: record.createdAt,
|
|
52
|
+
expiresAt: record.expiresAt,
|
|
53
|
+
lastUsedAt: record.lastUsedAt,
|
|
54
|
+
revokedAt: record.revokedAt,
|
|
55
|
+
revokeReason: record.revokeReason,
|
|
56
|
+
rotatedFrom: record.rotatedFrom,
|
|
57
|
+
rotatedTo: record.rotatedTo,
|
|
58
|
+
status: record.revokedAt ? 'revoked' : Date.parse(record.expiresAt) <= Date.now() ? 'expired' : 'active',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createStoreAdapter(load, save, { clock, auditLimit }) {
|
|
63
|
+
let queue = Promise.resolve();
|
|
64
|
+
const transaction = (operation) => {
|
|
65
|
+
const run = queue.then(async () => {
|
|
66
|
+
const state = await load();
|
|
67
|
+
const result = await operation(state);
|
|
68
|
+
state.revision += 1;
|
|
69
|
+
if (state.audit.length > auditLimit) state.audit.splice(0, state.audit.length - auditLimit);
|
|
70
|
+
await save(state);
|
|
71
|
+
return result;
|
|
72
|
+
});
|
|
73
|
+
queue = run.catch(() => undefined);
|
|
74
|
+
return run;
|
|
75
|
+
};
|
|
76
|
+
const view = async (operation) => {
|
|
77
|
+
await queue;
|
|
78
|
+
return operation(await load());
|
|
79
|
+
};
|
|
80
|
+
const appendAudit = (state, event) => state.audit.push({ eventId: randomUUID(), at: clockIso(clock), ...event });
|
|
81
|
+
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
async insert(record, event) {
|
|
84
|
+
return transaction((state) => {
|
|
85
|
+
if (state.keys[record.id] || state.prefixIndex[record.prefix]) throw new Error('API key identifier collision');
|
|
86
|
+
state.keys[record.id] = structuredClone(record);
|
|
87
|
+
state.prefixIndex[record.prefix] = record.id;
|
|
88
|
+
appendAudit(state, event);
|
|
89
|
+
return structuredClone(record);
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
async rotate(previousId, nextRecord, event) {
|
|
93
|
+
return transaction((state) => {
|
|
94
|
+
const previous = state.keys[previousId];
|
|
95
|
+
if (!previous) return null;
|
|
96
|
+
if (previous.revokedAt) throw new Error('revoked API key cannot be rotated');
|
|
97
|
+
if (state.prefixIndex[nextRecord.prefix]) throw new Error('API key identifier collision');
|
|
98
|
+
previous.revokedAt = clockIso(clock);
|
|
99
|
+
previous.revokeReason = 'rotated';
|
|
100
|
+
previous.rotatedTo = nextRecord.id;
|
|
101
|
+
state.keys[nextRecord.id] = structuredClone(nextRecord);
|
|
102
|
+
state.prefixIndex[nextRecord.prefix] = nextRecord.id;
|
|
103
|
+
appendAudit(state, event);
|
|
104
|
+
return { previous: structuredClone(previous), next: structuredClone(nextRecord) };
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
async revoke(id, reason, event) {
|
|
108
|
+
return transaction((state) => {
|
|
109
|
+
const record = state.keys[id];
|
|
110
|
+
if (!record) return null;
|
|
111
|
+
if (!record.revokedAt) {
|
|
112
|
+
record.revokedAt = clockIso(clock);
|
|
113
|
+
record.revokeReason = reason;
|
|
114
|
+
appendAudit(state, event);
|
|
115
|
+
}
|
|
116
|
+
return structuredClone(record);
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
async getById(id) {
|
|
120
|
+
return view((state) => state.keys[id] ? structuredClone(state.keys[id]) : null);
|
|
121
|
+
},
|
|
122
|
+
async getByPrefix(prefix) {
|
|
123
|
+
return view((state) => {
|
|
124
|
+
const id = state.prefixIndex[prefix];
|
|
125
|
+
return id && state.keys[id] ? structuredClone(state.keys[id]) : null;
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
async list(ownerId, tenantId) {
|
|
129
|
+
return view((state) => Object.values(state.keys)
|
|
130
|
+
.filter((record) => record.ownerId === ownerId && record.tenantId === tenantId)
|
|
131
|
+
.map((record) => structuredClone(record))
|
|
132
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt)));
|
|
133
|
+
},
|
|
134
|
+
async markUsed(id, at) {
|
|
135
|
+
return transaction((state) => {
|
|
136
|
+
const record = state.keys[id];
|
|
137
|
+
if (!record || record.revokedAt || Date.parse(record.expiresAt) <= clock()) return false;
|
|
138
|
+
record.lastUsedAt = at;
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
async audit(ownerId, tenantId, limit = 100) {
|
|
143
|
+
return view((state) => state.audit
|
|
144
|
+
.filter((event) => event.ownerId === ownerId && event.tenantId === tenantId)
|
|
145
|
+
.slice(-limit)
|
|
146
|
+
.reverse()
|
|
147
|
+
.map((event) => structuredClone(event)));
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createEphemeralApiKeyStore(options = {}) {
|
|
153
|
+
const clock = options.clock || Date.now;
|
|
154
|
+
const auditLimit = boundedInteger(options.auditLimit, 'auditLimit', 100, 100_000, 10_000);
|
|
155
|
+
let state = emptyState();
|
|
156
|
+
return createStoreAdapter(async () => structuredClone(state), async (next) => { state = structuredClone(next); }, { clock, auditLimit });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function createFileApiKeyStore(options = {}) {
|
|
160
|
+
const path = resolve(String(options.path || '.enigma/api-keys.json'));
|
|
161
|
+
const clock = options.clock || Date.now;
|
|
162
|
+
const auditLimit = boundedInteger(options.auditLimit, 'auditLimit', 100, 100_000, 10_000);
|
|
163
|
+
async function load() {
|
|
164
|
+
try {
|
|
165
|
+
const state = JSON.parse(await readFile(path, 'utf8'));
|
|
166
|
+
if (state?.schema !== ENIGMA_API_KEY_STORE_SCHEMA) throw new Error('unsupported API key store schema');
|
|
167
|
+
return state;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error.code === 'ENOENT') return emptyState();
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function save(state) {
|
|
174
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
175
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
176
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
177
|
+
try {
|
|
178
|
+
await handle.writeFile(JSON.stringify(state));
|
|
179
|
+
await handle.sync();
|
|
180
|
+
} finally {
|
|
181
|
+
await handle.close();
|
|
182
|
+
}
|
|
183
|
+
await rename(temporary, path);
|
|
184
|
+
}
|
|
185
|
+
return createStoreAdapter(load, save, { clock, auditLimit });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function deriveSecret(secret, salt) {
|
|
189
|
+
return Buffer.from(await scrypt(secret, Buffer.from(salt, 'base64url'), 32, { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseRawKey(value) {
|
|
193
|
+
const match = KEY_PATTERN.exec(String(value || ''));
|
|
194
|
+
return match ? { environment: match[1], prefix: match[2], secret: match[3] } : null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function principalIdentity(input = {}) {
|
|
198
|
+
return {
|
|
199
|
+
ownerId: requiredString(input.ownerId || input.subject, 'ownerId'),
|
|
200
|
+
tenantId: requiredString(input.tenantId || input.tenant_id || 'default', 'tenantId'),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function createApiKeyService(options = {}) {
|
|
205
|
+
const store = options.store;
|
|
206
|
+
if (!store || typeof store.insert !== 'function' || typeof store.getByPrefix !== 'function') throw new TypeError('API key store is required');
|
|
207
|
+
const clock = options.clock || Date.now;
|
|
208
|
+
const environment = options.environment === 'test' ? 'test' : 'live';
|
|
209
|
+
const supportedScopes = new Set(options.scopes || [
|
|
210
|
+
'mcp:invoke', 'models:invoke', 'images:generate', 'video:generate', 'code:execute', 'browser:control', 'usage:read', 'credits:create',
|
|
211
|
+
'memory:read', 'memory:write', 'memory:delete', 'vault:admin', 'swarm:read', 'swarm:write',
|
|
212
|
+
'security:inspect', 'metering:write', 'settlement:read', 'settlement:write', 'settlement:operator', 'nullifier:prepare', 'payments:execute',
|
|
213
|
+
]);
|
|
214
|
+
const defaultScopes = normalizeScopes(options.defaultScopes || ['mcp:invoke', 'models:invoke'], supportedScopes);
|
|
215
|
+
const maxActiveKeys = boundedInteger(options.maxActiveKeys, 'maxActiveKeys', 1, 100, 20);
|
|
216
|
+
const maximumTtlSeconds = boundedInteger(options.maximumTtlSeconds, 'maximumTtlSeconds', 300, 366 * 24 * 60 * 60, 90 * 24 * 60 * 60);
|
|
217
|
+
const defaultTtlSeconds = boundedInteger(options.defaultTtlSeconds, 'defaultTtlSeconds', 300, maximumTtlSeconds, 30 * 24 * 60 * 60);
|
|
218
|
+
|
|
219
|
+
async function createRecord(input, rotation = {}) {
|
|
220
|
+
const identity = principalIdentity(input);
|
|
221
|
+
const scopes = normalizeScopes(input.scopes || defaultScopes, supportedScopes);
|
|
222
|
+
const ttlSeconds = boundedInteger(input.ttlSeconds, 'ttlSeconds', 300, maximumTtlSeconds, defaultTtlSeconds);
|
|
223
|
+
const issuedAt = clock();
|
|
224
|
+
const prefix = randomBytes(9).toString('base64url');
|
|
225
|
+
const secret = randomBytes(32).toString('base64url');
|
|
226
|
+
const salt = randomBytes(16).toString('base64url');
|
|
227
|
+
const secretDigest = (await deriveSecret(secret, salt)).toString('base64url');
|
|
228
|
+
const record = {
|
|
229
|
+
id: randomUUID(),
|
|
230
|
+
prefix,
|
|
231
|
+
environment,
|
|
232
|
+
label: requiredString(input.label || 'API key', 'label', 120),
|
|
233
|
+
ownerId: identity.ownerId,
|
|
234
|
+
tenantId: identity.tenantId,
|
|
235
|
+
scopes,
|
|
236
|
+
salt,
|
|
237
|
+
secretDigest,
|
|
238
|
+
createdAt: new Date(issuedAt).toISOString(),
|
|
239
|
+
expiresAt: new Date(issuedAt + ttlSeconds * 1_000).toISOString(),
|
|
240
|
+
lastUsedAt: null,
|
|
241
|
+
revokedAt: null,
|
|
242
|
+
revokeReason: null,
|
|
243
|
+
rotatedFrom: rotation.rotatedFrom || null,
|
|
244
|
+
rotatedTo: null,
|
|
245
|
+
};
|
|
246
|
+
return { record, rawKey: `enigma_sk_${environment}_${prefix}_${secret}` };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function assertOwned(record, identity) {
|
|
250
|
+
if (!record || record.ownerId !== identity.ownerId || record.tenantId !== identity.tenantId) {
|
|
251
|
+
const error = new Error('API key not found');
|
|
252
|
+
error.code = 'not_found';
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return Object.freeze({
|
|
258
|
+
supportedScopes: Object.freeze([...supportedScopes].sort()),
|
|
259
|
+
async issue(input = {}) {
|
|
260
|
+
const identity = principalIdentity(input);
|
|
261
|
+
const active = (await store.list(identity.ownerId, identity.tenantId))
|
|
262
|
+
.filter((record) => !record.revokedAt && Date.parse(record.expiresAt) > clock());
|
|
263
|
+
if (active.length >= maxActiveKeys) throw new Error(`active API key limit of ${maxActiveKeys} reached`);
|
|
264
|
+
const { record, rawKey } = await createRecord(input);
|
|
265
|
+
await store.insert(record, {
|
|
266
|
+
action: 'issued', keyId: record.id, prefix: record.prefix, ownerId: record.ownerId, tenantId: record.tenantId,
|
|
267
|
+
actorId: requiredString(input.actorId || identity.ownerId, 'actorId'),
|
|
268
|
+
});
|
|
269
|
+
return { key: publicRecord(record), secret: rawKey, secretShownOnce: true };
|
|
270
|
+
},
|
|
271
|
+
async list(input = {}) {
|
|
272
|
+
const identity = principalIdentity(input);
|
|
273
|
+
return (await store.list(identity.ownerId, identity.tenantId)).map(publicRecord);
|
|
274
|
+
},
|
|
275
|
+
async rotate(id, input = {}) {
|
|
276
|
+
const identity = principalIdentity(input);
|
|
277
|
+
const previous = await store.getById(requiredString(id, 'key id'));
|
|
278
|
+
assertOwned(previous, identity);
|
|
279
|
+
if (previous.revokedAt || Date.parse(previous.expiresAt) <= clock()) throw new Error('only an active API key can be rotated');
|
|
280
|
+
const { record, rawKey } = await createRecord({
|
|
281
|
+
...input,
|
|
282
|
+
ownerId: identity.ownerId,
|
|
283
|
+
tenantId: identity.tenantId,
|
|
284
|
+
label: input.label || previous.label,
|
|
285
|
+
scopes: input.scopes || previous.scopes,
|
|
286
|
+
}, { rotatedFrom: previous.id });
|
|
287
|
+
const rotated = await store.rotate(previous.id, record, {
|
|
288
|
+
action: 'rotated', keyId: record.id, previousKeyId: previous.id, prefix: record.prefix,
|
|
289
|
+
ownerId: identity.ownerId, tenantId: identity.tenantId,
|
|
290
|
+
actorId: requiredString(input.actorId || identity.ownerId, 'actorId'),
|
|
291
|
+
});
|
|
292
|
+
if (!rotated) throw new Error('API key not found');
|
|
293
|
+
return { key: publicRecord(rotated.next), previous: publicRecord(rotated.previous), secret: rawKey, secretShownOnce: true };
|
|
294
|
+
},
|
|
295
|
+
async revoke(id, input = {}) {
|
|
296
|
+
const identity = principalIdentity(input);
|
|
297
|
+
const existing = await store.getById(requiredString(id, 'key id'));
|
|
298
|
+
assertOwned(existing, identity);
|
|
299
|
+
const reason = requiredString(input.reason || 'revoked_by_owner', 'reason', 200);
|
|
300
|
+
const record = await store.revoke(existing.id, reason, {
|
|
301
|
+
action: 'revoked', keyId: existing.id, prefix: existing.prefix, reason,
|
|
302
|
+
ownerId: identity.ownerId, tenantId: identity.tenantId,
|
|
303
|
+
actorId: requiredString(input.actorId || identity.ownerId, 'actorId'),
|
|
304
|
+
});
|
|
305
|
+
return publicRecord(record);
|
|
306
|
+
},
|
|
307
|
+
async authenticate(value, input = {}) {
|
|
308
|
+
const parsed = parseRawKey(value);
|
|
309
|
+
if (!parsed || parsed.environment !== environment) return null;
|
|
310
|
+
const record = await store.getByPrefix(parsed.prefix);
|
|
311
|
+
if (!record || record.revokedAt || Date.parse(record.expiresAt) <= clock()) return null;
|
|
312
|
+
const actual = await deriveSecret(parsed.secret, record.salt);
|
|
313
|
+
const expected = Buffer.from(record.secretDigest, 'base64url');
|
|
314
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) return null;
|
|
315
|
+
const requiredScopes = [...new Set(input.requiredScopes || [])];
|
|
316
|
+
if (requiredScopes.some((scope) => !record.scopes.includes(scope))) return null;
|
|
317
|
+
if (input.tenantId && input.tenantId !== record.tenantId) return null;
|
|
318
|
+
const usedAt = clockIso(clock);
|
|
319
|
+
if (!await store.markUsed(record.id, usedAt)) return null;
|
|
320
|
+
return Object.freeze({
|
|
321
|
+
subject: record.ownerId,
|
|
322
|
+
ownerId: record.ownerId,
|
|
323
|
+
tenantId: record.tenantId,
|
|
324
|
+
keyId: record.id,
|
|
325
|
+
scopes: [...record.scopes],
|
|
326
|
+
authentication: 'api_key',
|
|
327
|
+
});
|
|
328
|
+
},
|
|
329
|
+
async audit(input = {}) {
|
|
330
|
+
const identity = principalIdentity(input);
|
|
331
|
+
const limit = boundedInteger(input.limit, 'limit', 1, 500, 100);
|
|
332
|
+
return store.audit(identity.ownerId, identity.tenantId, limit);
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function apiKeyFromAuthorization(value) {
|
|
338
|
+
if (!value || !String(value).startsWith('Bearer ')) return null;
|
|
339
|
+
const candidate = String(value).slice(7);
|
|
340
|
+
return KEY_PATTERN.test(candidate) ? candidate : null;
|
|
341
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
2
|
+
const KEY_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3
|
+
|
|
4
|
+
class ApiKeyHttpError extends Error {
|
|
5
|
+
constructor(status, code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ApiKeyHttpError';
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function writeJson(response, status, payload) {
|
|
14
|
+
const body = JSON.stringify(payload);
|
|
15
|
+
response.statusCode = status;
|
|
16
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
17
|
+
response.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
18
|
+
response.setHeader('Content-Length', Buffer.byteLength(body));
|
|
19
|
+
response.setHeader('X-Content-Type-Options', 'nosniff');
|
|
20
|
+
response.end(body);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readJson(request) {
|
|
24
|
+
const contentType = String(request.headers['content-type'] || '').split(';', 1)[0].trim().toLowerCase();
|
|
25
|
+
if (contentType !== 'application/json') throw new ApiKeyHttpError(415, 'unsupported_media_type', 'Content-Type must be application/json');
|
|
26
|
+
const chunks = [];
|
|
27
|
+
let bytes = 0;
|
|
28
|
+
let tooLarge = false;
|
|
29
|
+
for await (const chunk of request) {
|
|
30
|
+
bytes += chunk.length;
|
|
31
|
+
if (bytes > MAX_BODY_BYTES) tooLarge = true;
|
|
32
|
+
else chunks.push(chunk);
|
|
33
|
+
}
|
|
34
|
+
if (tooLarge) throw new ApiKeyHttpError(413, 'payload_too_large', 'request body exceeds 64 KiB');
|
|
35
|
+
try {
|
|
36
|
+
const value = JSON.parse(Buffer.concat(chunks, bytes).toString('utf8') || '{}');
|
|
37
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error();
|
|
38
|
+
return value;
|
|
39
|
+
} catch {
|
|
40
|
+
throw new ApiKeyHttpError(400, 'invalid_json', 'request body must be a JSON object');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rejectUnknown(input, allowed) {
|
|
45
|
+
const unknown = Object.keys(input).find((key) => !allowed.has(key));
|
|
46
|
+
if (unknown) throw new ApiKeyHttpError(400, 'invalid_request', `unknown field: ${unknown}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function issueInput(input, principal) {
|
|
50
|
+
rejectUnknown(input, new Set(['label', 'scopes', 'ttlSeconds']));
|
|
51
|
+
return {
|
|
52
|
+
ownerId: principal.ownerId || principal.subject,
|
|
53
|
+
tenantId: principal.tenantId || 'default',
|
|
54
|
+
actorId: principal.subject || principal.ownerId,
|
|
55
|
+
label: input.label,
|
|
56
|
+
scopes: input.scopes,
|
|
57
|
+
ttlSeconds: input.ttlSeconds,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function operationStatus(error) {
|
|
62
|
+
if (error instanceof ApiKeyHttpError) return error;
|
|
63
|
+
if (error?.code === 'not_found') return new ApiKeyHttpError(404, 'not_found', 'API key not found');
|
|
64
|
+
if (error instanceof TypeError) return new ApiKeyHttpError(400, 'invalid_request', error.message);
|
|
65
|
+
if (/not found/i.test(error?.message || '')) return new ApiKeyHttpError(404, 'not_found', 'API key not found');
|
|
66
|
+
if (/limit|active API key|revoked API key/i.test(error?.message || '')) return new ApiKeyHttpError(409, 'conflict', error.message);
|
|
67
|
+
return new ApiKeyHttpError(500, 'internal_error', 'API key operation failed');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createApiKeyHttpHandler(options = {}) {
|
|
71
|
+
const apiKeys = options.apiKeys;
|
|
72
|
+
if (!apiKeys || typeof apiKeys.issue !== 'function' || typeof apiKeys.authenticate !== 'function') throw new TypeError('apiKeys service is required');
|
|
73
|
+
if (typeof options.authorizeAdmin !== 'function') throw new TypeError('authorizeAdmin is required');
|
|
74
|
+
const basePath = String(options.basePath || '/v1/api-keys').replace(/\/+$/, '');
|
|
75
|
+
|
|
76
|
+
return async function apiKeyHttpHandler(request, response) {
|
|
77
|
+
const url = new URL(request.url || '/', 'http://localhost');
|
|
78
|
+
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return false;
|
|
79
|
+
try {
|
|
80
|
+
let principal;
|
|
81
|
+
try { principal = await options.authorizeAdmin({ request, headers: request.headers }); } catch { principal = null; }
|
|
82
|
+
if (!principal?.subject && !principal?.ownerId) throw new ApiKeyHttpError(401, 'unauthorized', 'administrator authentication is required');
|
|
83
|
+
const identity = {
|
|
84
|
+
ownerId: principal.ownerId || principal.subject,
|
|
85
|
+
tenantId: principal.tenantId || 'default',
|
|
86
|
+
actorId: principal.subject || principal.ownerId,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (request.method === 'GET' && url.pathname === `${basePath}/scopes`) {
|
|
90
|
+
writeJson(response, 200, { schema: 'enigma.api_key_scopes.v1', scopes: apiKeys.supportedScopes });
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (request.method === 'GET' && url.pathname === `${basePath}/audit`) {
|
|
94
|
+
const limitValue = url.searchParams.get('limit');
|
|
95
|
+
const limit = limitValue === null ? undefined : Number(limitValue);
|
|
96
|
+
writeJson(response, 200, { schema: 'enigma.api_key_audit.v1', events: await apiKeys.audit({ ...identity, limit }) });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (request.method === 'GET' && url.pathname === basePath) {
|
|
100
|
+
writeJson(response, 200, { schema: 'enigma.api_key_list.v1', keys: await apiKeys.list(identity) });
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (request.method === 'POST' && url.pathname === basePath) {
|
|
104
|
+
const input = await readJson(request);
|
|
105
|
+
writeJson(response, 201, await apiKeys.issue(issueInput(input, identity)));
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const suffix = url.pathname.slice(basePath.length + 1);
|
|
110
|
+
const [id, action, extra] = suffix.split('/');
|
|
111
|
+
if (!KEY_ID_PATTERN.test(id || '') || extra) throw new ApiKeyHttpError(404, 'not_found', 'API key endpoint not found');
|
|
112
|
+
if (request.method === 'POST' && action === 'rotate') {
|
|
113
|
+
const input = await readJson(request);
|
|
114
|
+
rejectUnknown(input, new Set(['label', 'scopes', 'ttlSeconds']));
|
|
115
|
+
writeJson(response, 201, await apiKeys.rotate(id, { ...issueInput(input, identity) }));
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (request.method === 'DELETE' && action === undefined) {
|
|
119
|
+
const input = await readJson(request);
|
|
120
|
+
rejectUnknown(input, new Set(['reason']));
|
|
121
|
+
writeJson(response, 200, await apiKeys.revoke(id, { ...identity, reason: input.reason }));
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
throw new ApiKeyHttpError(405, 'method_not_allowed', 'API key endpoint method is not allowed');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const normalized = operationStatus(error);
|
|
127
|
+
if (normalized.status === 401) response.setHeader('WWW-Authenticate', 'Bearer');
|
|
128
|
+
writeJson(response, normalized.status, { error: { code: normalized.code, message: normalized.message } });
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
function json(response, status, payload) {
|
|
2
|
+
const body = JSON.stringify(payload);
|
|
3
|
+
response.statusCode = status;
|
|
4
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
5
|
+
response.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
6
|
+
response.setHeader('Content-Length', Buffer.byteLength(body));
|
|
7
|
+
response.setHeader('X-Content-Type-Options', 'nosniff');
|
|
8
|
+
response.end(body);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function queryInput(url, principal) {
|
|
12
|
+
const daysValue = url.searchParams.get('days');
|
|
13
|
+
const days = daysValue === null ? 30 : Number(daysValue);
|
|
14
|
+
if (!Number.isInteger(days) || days < 1 || days > 400) throw new TypeError('days must be an integer from 1 through 400');
|
|
15
|
+
const to = url.searchParams.get('to') || undefined;
|
|
16
|
+
const parsedTo = to === undefined ? Date.now() + 1 : Date.parse(to);
|
|
17
|
+
if (!Number.isFinite(parsedTo)) throw new TypeError('to must be an ISO timestamp');
|
|
18
|
+
const from = url.searchParams.get('from') || new Date(parsedTo - days * 24 * 60 * 60_000).toISOString();
|
|
19
|
+
if (!Number.isFinite(Date.parse(from))) throw new TypeError('from must be an ISO timestamp');
|
|
20
|
+
const limitValue = url.searchParams.get('limit');
|
|
21
|
+
const limit = limitValue === null ? undefined : Number(limitValue);
|
|
22
|
+
return { principal, from, to: new Date(parsedTo).toISOString(), limit };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createUsageHttpHandler(options = {}) {
|
|
26
|
+
const usage = options.usage;
|
|
27
|
+
if (!usage || typeof usage.aggregate !== 'function' || typeof usage.events !== 'function') throw new TypeError('usage service is required');
|
|
28
|
+
if (typeof options.authorize !== 'function') throw new TypeError('usage HTTP authorizer is required');
|
|
29
|
+
const basePath = String(options.basePath || '/v1/usage').replace(/\/+$/, '');
|
|
30
|
+
|
|
31
|
+
return async function usageHttpHandler(request, response) {
|
|
32
|
+
const url = new URL(request.url || '/', 'http://localhost');
|
|
33
|
+
if (url.pathname !== basePath && url.pathname !== `${basePath}/events`) return false;
|
|
34
|
+
try {
|
|
35
|
+
let principal;
|
|
36
|
+
try { principal = await options.authorize({ request, headers: request.headers, requiredScopes: ['usage:read'] }); } catch { principal = null; }
|
|
37
|
+
if (!principal) {
|
|
38
|
+
response.setHeader('WWW-Authenticate', 'Bearer scope="usage:read"');
|
|
39
|
+
json(response, 401, { error: { code: 'unauthorized', message: 'usage:read authorization is required' } });
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (request.method !== 'GET') {
|
|
43
|
+
response.setHeader('Allow', 'GET');
|
|
44
|
+
json(response, 405, { error: { code: 'method_not_allowed', message: 'usage endpoints are read-only' } });
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const input = queryInput(url, principal);
|
|
48
|
+
if (url.pathname === `${basePath}/events`) {
|
|
49
|
+
json(response, 200, { schema: 'enigma.platform_usage_event_list.v1', events: await usage.events(input) });
|
|
50
|
+
} else {
|
|
51
|
+
json(response, 200, await usage.aggregate(input));
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const status = error instanceof TypeError ? 400 : 500;
|
|
56
|
+
json(response, status, { error: { code: status === 400 ? 'invalid_request' : 'usage_error', message: error.message || 'usage query failed' } });
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|