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,152 @@
|
|
|
1
|
+
import AVKit
|
|
2
|
+
import EnigmaCore
|
|
3
|
+
import SwiftUI
|
|
4
|
+
|
|
5
|
+
private enum CreationMode: String, CaseIterable, Identifiable {
|
|
6
|
+
case image = "Images"
|
|
7
|
+
case video = "Video"
|
|
8
|
+
case code = "Code"
|
|
9
|
+
var id: String { rawValue }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct CreateView: View {
|
|
13
|
+
@EnvironmentObject private var model: AppModel
|
|
14
|
+
@State private var mode: CreationMode = .image
|
|
15
|
+
|
|
16
|
+
var body: some View {
|
|
17
|
+
ScrollView {
|
|
18
|
+
VStack(alignment: .leading, spacing: 20) {
|
|
19
|
+
Picker("Mode", selection: $mode) {
|
|
20
|
+
ForEach(CreationMode.allCases) { option in Text(option.rawValue).tag(option) }
|
|
21
|
+
}
|
|
22
|
+
.pickerStyle(.segmented)
|
|
23
|
+
switch mode {
|
|
24
|
+
case .image: ImageToolView()
|
|
25
|
+
case .video: VideoToolView()
|
|
26
|
+
case .code: CodeToolView()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
.padding(20)
|
|
30
|
+
.padding(.bottom, 32)
|
|
31
|
+
}
|
|
32
|
+
.navigationTitle("Create")
|
|
33
|
+
.navigationBarTitleDisplayMode(.inline)
|
|
34
|
+
.enigmaCanvas()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private struct ImageToolView: View {
|
|
39
|
+
@EnvironmentObject private var model: AppModel
|
|
40
|
+
@State private var prompt = ""
|
|
41
|
+
@State private var selectedModel = ""
|
|
42
|
+
|
|
43
|
+
var body: some View {
|
|
44
|
+
VStack(alignment: .leading, spacing: 18) {
|
|
45
|
+
intro("ENIGMA IMAGE", "Generate without building a prompt archive.", "Prompts are minimized on-device. Returned assets remain caller-controlled.")
|
|
46
|
+
Picker("Image model", selection: $selectedModel) {
|
|
47
|
+
Text("Choose image model").tag("")
|
|
48
|
+
ForEach(model.imageModels) { route in Text(route.id).tag(route.id) }
|
|
49
|
+
}
|
|
50
|
+
.pickerStyle(.menu)
|
|
51
|
+
TextField("Describe the image…", text: $prompt, axis: .vertical)
|
|
52
|
+
.lineLimit(5...10)
|
|
53
|
+
.padding(12)
|
|
54
|
+
.background(.white)
|
|
55
|
+
.overlay(RoundedRectangle(cornerRadius: 10).stroke(EnigmaTheme.line))
|
|
56
|
+
Button("Generate image") { Task { await model.generateImage(prompt: prompt, model: selectedModel) } }
|
|
57
|
+
.buttonStyle(PrimaryButtonStyle())
|
|
58
|
+
.disabled(prompt.isEmpty || selectedModel.isEmpty || model.isBusy)
|
|
59
|
+
ForEach(Array(model.generatedImages.enumerated()), id: \.offset) { _, asset in
|
|
60
|
+
image(asset)
|
|
61
|
+
.frame(maxWidth: .infinity)
|
|
62
|
+
.aspectRatio(1, contentMode: .fit)
|
|
63
|
+
.background(Color.black.opacity(0.04))
|
|
64
|
+
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
.onAppear { if selectedModel.isEmpty { selectedModel = model.imageModels.first?.id ?? "" } }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@ViewBuilder private func image(_ asset: GeneratedImageAsset) -> some View {
|
|
71
|
+
if let raw = asset.base64, let data = Data(base64Encoded: raw), let uiImage = UIImage(data: data) {
|
|
72
|
+
Image(uiImage: uiImage).resizable().scaledToFit()
|
|
73
|
+
} else if let value = asset.url, let url = URL(string: value) {
|
|
74
|
+
AsyncImage(url: url) { phase in
|
|
75
|
+
if let image = phase.image { image.resizable().scaledToFit() }
|
|
76
|
+
else if phase.error != nil { ContentUnavailableView("Image unavailable", systemImage: "photo") }
|
|
77
|
+
else { ProgressView() }
|
|
78
|
+
}
|
|
79
|
+
} else { ContentUnavailableView("Image unavailable", systemImage: "photo") }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private struct VideoToolView: View {
|
|
84
|
+
@EnvironmentObject private var model: AppModel
|
|
85
|
+
@State private var prompt = ""
|
|
86
|
+
@State private var selectedModel = ""
|
|
87
|
+
|
|
88
|
+
var body: some View {
|
|
89
|
+
VStack(alignment: .leading, spacing: 18) {
|
|
90
|
+
intro("ENIGMA VIDEO", "Queue private motion generation.", "Progress is bounded job metadata. The final artifact belongs to you.")
|
|
91
|
+
Picker("Video model", selection: $selectedModel) {
|
|
92
|
+
Text("Choose video model").tag("")
|
|
93
|
+
ForEach(model.videoModels) { route in Text(route.id).tag(route.id) }
|
|
94
|
+
}
|
|
95
|
+
TextField("Describe motion, scene, and camera…", text: $prompt, axis: .vertical)
|
|
96
|
+
.lineLimit(5...10)
|
|
97
|
+
.padding(12)
|
|
98
|
+
.background(.white)
|
|
99
|
+
.overlay(RoundedRectangle(cornerRadius: 10).stroke(EnigmaTheme.line))
|
|
100
|
+
Button("Queue video") { Task { await model.queueVideo(prompt: prompt, model: selectedModel) } }
|
|
101
|
+
.buttonStyle(PrimaryButtonStyle())
|
|
102
|
+
.disabled(prompt.isEmpty || selectedModel.isEmpty || model.isBusy)
|
|
103
|
+
if let job = model.videoJob {
|
|
104
|
+
VStack(alignment: .leading, spacing: 9) {
|
|
105
|
+
HStack { Text(job.status.capitalized).font(.subheadline.weight(.semibold)); Spacer(); Text("\(Int(job.progress))%") }
|
|
106
|
+
ProgressView(value: job.progress, total: 100).tint(EnigmaTheme.ember)
|
|
107
|
+
if let error = job.error { Text(error).foregroundStyle(.red).font(.footnote) }
|
|
108
|
+
}
|
|
109
|
+
.enigmaSurface()
|
|
110
|
+
}
|
|
111
|
+
if let value = model.videoAsset?.url, let url = URL(string: value) {
|
|
112
|
+
VideoPlayer(player: AVPlayer(url: url)).frame(height: 230).clipShape(RoundedRectangle(cornerRadius: 12))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
.onAppear { if selectedModel.isEmpty { selectedModel = model.videoModels.first?.id ?? "" } }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private struct CodeToolView: View {
|
|
120
|
+
@EnvironmentObject private var model: AppModel
|
|
121
|
+
@State private var source = "console.log('Enigma sandbox ready')"
|
|
122
|
+
|
|
123
|
+
var body: some View {
|
|
124
|
+
VStack(alignment: .leading, spacing: 18) {
|
|
125
|
+
intro("ENIGMA CODE", "Run below an OCI boundary.", "No network. Read-only root. No Linux capabilities. The session is burned after every run.")
|
|
126
|
+
TextEditor(text: $source)
|
|
127
|
+
.font(.system(.body, design: .monospaced))
|
|
128
|
+
.frame(minHeight: 240)
|
|
129
|
+
.padding(10)
|
|
130
|
+
.scrollContentBackground(.hidden)
|
|
131
|
+
.background(Color(red: 0.07, green: 0.065, blue: 0.06))
|
|
132
|
+
.foregroundStyle(Color(red: 0.93, green: 0.91, blue: 0.86))
|
|
133
|
+
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
134
|
+
Button("Create, run, and burn") { Task { await model.executeCode(source) } }
|
|
135
|
+
.buttonStyle(PrimaryButtonStyle())
|
|
136
|
+
.disabled(source.isEmpty || model.isBusy)
|
|
137
|
+
Text(model.codeOutput.isEmpty ? "No sandbox output." : model.codeOutput)
|
|
138
|
+
.font(.system(.footnote, design: .monospaced))
|
|
139
|
+
.textSelection(.enabled)
|
|
140
|
+
.frame(maxWidth: .infinity, minHeight: 120, alignment: .topLeading)
|
|
141
|
+
.enigmaSurface()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@ViewBuilder private func intro(_ eyebrow: String, _ title: String, _ description: String) -> some View {
|
|
147
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
148
|
+
SectionEyebrow(text: eyebrow)
|
|
149
|
+
Text(title).font(EnigmaTheme.title)
|
|
150
|
+
Text(description).font(.subheadline).foregroundStyle(EnigmaTheme.secondary)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
@main
|
|
4
|
+
struct EnigmaApp: App {
|
|
5
|
+
@StateObject private var model = AppModel()
|
|
6
|
+
|
|
7
|
+
var body: some Scene {
|
|
8
|
+
WindowGroup {
|
|
9
|
+
RootView()
|
|
10
|
+
.environmentObject(model)
|
|
11
|
+
.preferredColorScheme(.light)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
struct RootView: View {
|
|
17
|
+
@EnvironmentObject private var model: AppModel
|
|
18
|
+
|
|
19
|
+
var body: some View {
|
|
20
|
+
Group {
|
|
21
|
+
if model.isConnected {
|
|
22
|
+
TabView {
|
|
23
|
+
NavigationStack { ChatView() }
|
|
24
|
+
.tabItem { Label("Chat", systemImage: "bubble.left") }
|
|
25
|
+
NavigationStack { CouncilView() }
|
|
26
|
+
.tabItem { Label("Council", systemImage: "point.3.connected.trianglepath.dotted") }
|
|
27
|
+
NavigationStack { CreateView() }
|
|
28
|
+
.tabItem { Label("Create", systemImage: "sparkles.rectangle.stack") }
|
|
29
|
+
NavigationStack { PrivateVaultView() }
|
|
30
|
+
.tabItem { Label("Private", systemImage: "lock.square") }
|
|
31
|
+
}
|
|
32
|
+
.tint(EnigmaTheme.ember)
|
|
33
|
+
} else {
|
|
34
|
+
OnboardingView()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
.enigmaCanvas()
|
|
38
|
+
.overlay(alignment: .top) {
|
|
39
|
+
if model.isBusy {
|
|
40
|
+
ProgressView().tint(EnigmaTheme.ember).padding(.top, 8)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
.alert("Enigma", isPresented: Binding(
|
|
44
|
+
get: { model.errorMessage != nil },
|
|
45
|
+
set: { if !$0 { model.errorMessage = nil } }
|
|
46
|
+
)) {
|
|
47
|
+
Button("Dismiss", role: .cancel) { model.errorMessage = nil }
|
|
48
|
+
} message: {
|
|
49
|
+
Text(model.errorMessage ?? "")
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleDisplayName</key>
|
|
6
|
+
<string>Enigma</string>
|
|
7
|
+
<key>CFBundleIdentifier</key>
|
|
8
|
+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
9
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
10
|
+
<string>6.0</string>
|
|
11
|
+
<key>CFBundleName</key>
|
|
12
|
+
<string>$(PRODUCT_NAME)</string>
|
|
13
|
+
<key>CFBundlePackageType</key>
|
|
14
|
+
<string>APPL</string>
|
|
15
|
+
<key>CFBundleShortVersionString</key>
|
|
16
|
+
<string>1.0</string>
|
|
17
|
+
<key>CFBundleVersion</key>
|
|
18
|
+
<string>1</string>
|
|
19
|
+
<key>CFBundleURLTypes</key>
|
|
20
|
+
<array>
|
|
21
|
+
<dict>
|
|
22
|
+
<key>CFBundleTypeRole</key>
|
|
23
|
+
<string>Editor</string>
|
|
24
|
+
<key>CFBundleURLName</key>
|
|
25
|
+
<string>com.enigmamemory.ios.oauth</string>
|
|
26
|
+
<key>CFBundleURLSchemes</key>
|
|
27
|
+
<array><string>enigma-ios</string></array>
|
|
28
|
+
</dict>
|
|
29
|
+
</array>
|
|
30
|
+
<key>LSRequiresIPhoneOS</key>
|
|
31
|
+
<true/>
|
|
32
|
+
<key>NSAppTransportSecurity</key>
|
|
33
|
+
<dict>
|
|
34
|
+
<key>NSAllowsLocalNetworking</key>
|
|
35
|
+
<true/>
|
|
36
|
+
</dict>
|
|
37
|
+
<key>UIApplicationSceneManifest</key>
|
|
38
|
+
<dict>
|
|
39
|
+
<key>UIApplicationSupportsMultipleScenes</key>
|
|
40
|
+
<false/>
|
|
41
|
+
</dict>
|
|
42
|
+
<key>UILaunchScreen</key>
|
|
43
|
+
<dict>
|
|
44
|
+
<key>UIColorName</key>
|
|
45
|
+
<string>LaunchBackground</string>
|
|
46
|
+
</dict>
|
|
47
|
+
<key>UISupportedInterfaceOrientations</key>
|
|
48
|
+
<array>
|
|
49
|
+
<string>UIInterfaceOrientationPortrait</string>
|
|
50
|
+
</array>
|
|
51
|
+
</dict>
|
|
52
|
+
</plist>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import EnigmaCore
|
|
2
|
+
import Foundation
|
|
3
|
+
import NaturalLanguage
|
|
4
|
+
|
|
5
|
+
struct NaturalLanguagePrivacyTagger: SemanticPrivacyTagging {
|
|
6
|
+
private let fallback = HeuristicSemanticPrivacyTagger()
|
|
7
|
+
|
|
8
|
+
func findings(in text: String) -> [SemanticPrivacyFinding] {
|
|
9
|
+
var output = fallback.findings(in: text)
|
|
10
|
+
let tagger = NLTagger(tagSchemes: [.nameType])
|
|
11
|
+
tagger.string = text
|
|
12
|
+
let options: NLTagger.Options = [.omitWhitespace, .omitPunctuation, .joinNames]
|
|
13
|
+
tagger.enumerateTags(in: text.startIndex..<text.endIndex, unit: .word, scheme: .nameType, options: options) { tag, range in
|
|
14
|
+
let category: PrivacyCategory?
|
|
15
|
+
switch tag {
|
|
16
|
+
case .personalName: category = .person
|
|
17
|
+
case .placeName: category = .location
|
|
18
|
+
case .organizationName: category = .organization
|
|
19
|
+
default: category = nil
|
|
20
|
+
}
|
|
21
|
+
if let category { output.append(.init(range: range, category: category)) }
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
return output
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import AuthenticationServices
|
|
2
|
+
import EnigmaCore
|
|
3
|
+
import Foundation
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
private final class WebAuthenticationAnchor: NSObject, ASWebAuthenticationPresentationContextProviding {
|
|
7
|
+
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
|
8
|
+
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
|
9
|
+
return scenes.flatMap(\.windows).first(where: \.isKeyWindow) ?? ASPresentationAnchor()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@MainActor
|
|
15
|
+
private final class WebAuthenticationRunner {
|
|
16
|
+
private let anchor = WebAuthenticationAnchor()
|
|
17
|
+
private var session: ASWebAuthenticationSession?
|
|
18
|
+
|
|
19
|
+
func run(url: URL) async throws -> URL {
|
|
20
|
+
try await withCheckedThrowingContinuation { continuation in
|
|
21
|
+
session = ASWebAuthenticationSession(url: url, callbackURLScheme: "enigma-ios") { [weak self] url, error in
|
|
22
|
+
self?.session = nil
|
|
23
|
+
if let error { continuation.resume(throwing: error) }
|
|
24
|
+
else if let url { continuation.resume(returning: url) }
|
|
25
|
+
else { continuation.resume(throwing: OAuthClientError.invalidCallback) }
|
|
26
|
+
}
|
|
27
|
+
session?.presentationContextProvider = anchor
|
|
28
|
+
session?.prefersEphemeralWebBrowserSession = true
|
|
29
|
+
if session?.start() != true {
|
|
30
|
+
session = nil
|
|
31
|
+
continuation.resume(throwing: OAuthClientError.invalidCallback)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
enum OAuthClientError: LocalizedError {
|
|
37
|
+
case invalidIssuer
|
|
38
|
+
case invalidResource
|
|
39
|
+
case invalidMetadata
|
|
40
|
+
case invalidCallback
|
|
41
|
+
case stateMismatch
|
|
42
|
+
case unsupportedScopes([String])
|
|
43
|
+
case registrationUnavailable
|
|
44
|
+
case registrationFailed(String)
|
|
45
|
+
case authorizationDenied(String)
|
|
46
|
+
case tokenExchangeFailed(String)
|
|
47
|
+
|
|
48
|
+
var errorDescription: String? {
|
|
49
|
+
switch self {
|
|
50
|
+
case .invalidIssuer: return "The OAuth issuer must use HTTPS, or loopback HTTP for local development."
|
|
51
|
+
case .invalidResource: return "The protected resource must be a canonical HTTPS URL, or loopback HTTP URL for local development."
|
|
52
|
+
case .invalidMetadata: return "The authorization server metadata is invalid."
|
|
53
|
+
case .invalidCallback: return "The OAuth callback was invalid."
|
|
54
|
+
case .stateMismatch: return "OAuth state verification failed."
|
|
55
|
+
case let .unsupportedScopes(scopes): return "The authorization server does not support: \(scopes.joined(separator: ", "))."
|
|
56
|
+
case .registrationUnavailable: return "Enter a registered public client ID; this authorization server does not offer approved dynamic registration."
|
|
57
|
+
case let .registrationFailed(reason): return reason
|
|
58
|
+
case let .authorizationDenied(reason): return reason
|
|
59
|
+
case let .tokenExchangeFailed(reason): return reason
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private struct OAuthPublicClientRegistrationRequest: Encodable {
|
|
65
|
+
let clientName: String
|
|
66
|
+
let redirectURIs: [String]
|
|
67
|
+
let grantTypes: [String]
|
|
68
|
+
let responseTypes: [String]
|
|
69
|
+
let tokenEndpointAuthMethod: String
|
|
70
|
+
let scope: String
|
|
71
|
+
|
|
72
|
+
enum CodingKeys: String, CodingKey {
|
|
73
|
+
case clientName = "client_name"
|
|
74
|
+
case redirectURIs = "redirect_uris"
|
|
75
|
+
case grantTypes = "grant_types"
|
|
76
|
+
case responseTypes = "response_types"
|
|
77
|
+
case tokenEndpointAuthMethod = "token_endpoint_auth_method"
|
|
78
|
+
case scope
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private struct OAuthPublicClientRegistrationResponse: Decodable {
|
|
83
|
+
let clientID: String
|
|
84
|
+
let redirectURIs: [String]
|
|
85
|
+
let tokenEndpointAuthMethod: String
|
|
86
|
+
let scope: String
|
|
87
|
+
|
|
88
|
+
enum CodingKeys: String, CodingKey {
|
|
89
|
+
case clientID = "client_id"
|
|
90
|
+
case redirectURIs = "redirect_uris"
|
|
91
|
+
case tokenEndpointAuthMethod = "token_endpoint_auth_method"
|
|
92
|
+
case scope
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
struct OAuthClient {
|
|
97
|
+
static let redirectURI = "enigma-ios://oauth/callback"
|
|
98
|
+
static let platformScopes = EnigmaOAuthScopes.iOSPlatform
|
|
99
|
+
|
|
100
|
+
@MainActor
|
|
101
|
+
func authorize(issuer: URL, clientID: String?, resource: URL, scopes: [String] = platformScopes) async throws -> OAuthCredential {
|
|
102
|
+
guard let protectedResource = Self.canonicalResource(resource) else { throw OAuthClientError.invalidResource }
|
|
103
|
+
let metadata = try await Self.metadata(issuer: issuer)
|
|
104
|
+
let requestedScopes = Array(Set(scopes)).sorted()
|
|
105
|
+
if let supportedScopes = metadata.scopesSupported {
|
|
106
|
+
let missing = requestedScopes.filter { !supportedScopes.contains($0) }
|
|
107
|
+
if !missing.isEmpty { throw OAuthClientError.unsupportedScopes(missing) }
|
|
108
|
+
}
|
|
109
|
+
let suppliedClientID = clientID?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
110
|
+
let resolvedClientID: String
|
|
111
|
+
if suppliedClientID.isEmpty {
|
|
112
|
+
resolvedClientID = try await Self.registerPublicClient(metadata: metadata, scopes: requestedScopes)
|
|
113
|
+
} else {
|
|
114
|
+
resolvedClientID = suppliedClientID
|
|
115
|
+
}
|
|
116
|
+
let pkce = PKCE.create()
|
|
117
|
+
let state = UUID().uuidString
|
|
118
|
+
guard let endpoint = Self.secureEndpoint(metadata.authorizationEndpoint),
|
|
119
|
+
var components = URLComponents(url: endpoint, resolvingAgainstBaseURL: false) else {
|
|
120
|
+
throw OAuthClientError.invalidMetadata
|
|
121
|
+
}
|
|
122
|
+
components.queryItems = [
|
|
123
|
+
URLQueryItem(name: "response_type", value: "code"),
|
|
124
|
+
URLQueryItem(name: "client_id", value: resolvedClientID),
|
|
125
|
+
URLQueryItem(name: "redirect_uri", value: Self.redirectURI),
|
|
126
|
+
URLQueryItem(name: "scope", value: requestedScopes.joined(separator: " ")),
|
|
127
|
+
URLQueryItem(name: "state", value: state),
|
|
128
|
+
URLQueryItem(name: "code_challenge", value: pkce.challenge),
|
|
129
|
+
URLQueryItem(name: "code_challenge_method", value: "S256"),
|
|
130
|
+
URLQueryItem(name: "resource", value: protectedResource.absoluteString),
|
|
131
|
+
]
|
|
132
|
+
guard let authorizationURL = components.url else { throw OAuthClientError.invalidMetadata }
|
|
133
|
+
let callback = try await WebAuthenticationRunner().run(url: authorizationURL)
|
|
134
|
+
guard let callbackComponents = URLComponents(url: callback, resolvingAgainstBaseURL: false) else { throw OAuthClientError.invalidCallback }
|
|
135
|
+
let values = Dictionary(uniqueKeysWithValues: callbackComponents.queryItems?.map { ($0.name, $0.value ?? "") } ?? [])
|
|
136
|
+
if let error = values["error"] { throw OAuthClientError.authorizationDenied(values["error_description"] ?? error) }
|
|
137
|
+
guard values["state"] == state else { throw OAuthClientError.stateMismatch }
|
|
138
|
+
guard let code = values["code"], !code.isEmpty else { throw OAuthClientError.invalidCallback }
|
|
139
|
+
if let responseIssuer = values["iss"], responseIssuer != metadata.issuer { throw OAuthClientError.invalidCallback }
|
|
140
|
+
let tokens = try await Self.tokenRequest(
|
|
141
|
+
endpoint: metadata.tokenEndpoint,
|
|
142
|
+
fields: [
|
|
143
|
+
"grant_type": "authorization_code",
|
|
144
|
+
"client_id": resolvedClientID,
|
|
145
|
+
"code": code,
|
|
146
|
+
"redirect_uri": Self.redirectURI,
|
|
147
|
+
"code_verifier": pkce.verifier,
|
|
148
|
+
"resource": protectedResource.absoluteString,
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
return OAuthCredential(
|
|
152
|
+
accessToken: tokens.accessToken,
|
|
153
|
+
refreshToken: tokens.refreshToken,
|
|
154
|
+
expiresAt: Date().addingTimeInterval(TimeInterval(tokens.expiresIn)),
|
|
155
|
+
clientID: resolvedClientID,
|
|
156
|
+
issuer: issuer,
|
|
157
|
+
resource: protectedResource
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private static func registerPublicClient(metadata: OAuthAuthorizationServerMetadata, scopes: [String]) async throws -> String {
|
|
162
|
+
guard let value = metadata.registrationEndpoint, let endpoint = secureEndpoint(value) else {
|
|
163
|
+
throw OAuthClientError.registrationUnavailable
|
|
164
|
+
}
|
|
165
|
+
var request = URLRequest(url: endpoint, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30)
|
|
166
|
+
request.httpMethod = "POST"
|
|
167
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
168
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
169
|
+
request.httpBody = try JSONEncoder().encode(OAuthPublicClientRegistrationRequest(
|
|
170
|
+
clientName: "Enigma for iOS",
|
|
171
|
+
redirectURIs: [redirectURI],
|
|
172
|
+
grantTypes: ["authorization_code", "refresh_token"],
|
|
173
|
+
responseTypes: ["code"],
|
|
174
|
+
tokenEndpointAuthMethod: "none",
|
|
175
|
+
scope: scopes.joined(separator: " ")
|
|
176
|
+
))
|
|
177
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
178
|
+
guard let http = response as? HTTPURLResponse else { throw OAuthClientError.invalidMetadata }
|
|
179
|
+
guard (200..<300).contains(http.statusCode),
|
|
180
|
+
let registration = try? JSONDecoder().decode(OAuthPublicClientRegistrationResponse.self, from: data) else {
|
|
181
|
+
let reason = (try? JSONSerialization.jsonObject(with: data) as? [String: Any])?["error_description"] as? String
|
|
182
|
+
throw OAuthClientError.registrationFailed(reason ?? "OAuth public-client registration failed.")
|
|
183
|
+
}
|
|
184
|
+
let registeredScopes = Set(registration.scope.split(separator: " ").map(String.init))
|
|
185
|
+
guard !registration.clientID.isEmpty,
|
|
186
|
+
registration.redirectURIs.contains(redirectURI),
|
|
187
|
+
registration.tokenEndpointAuthMethod == "none",
|
|
188
|
+
Set(scopes).isSubset(of: registeredScopes) else {
|
|
189
|
+
throw OAuthClientError.registrationFailed("OAuth server returned an unsafe or incomplete public-client registration.")
|
|
190
|
+
}
|
|
191
|
+
return registration.clientID
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private static func secureEndpoint(_ value: String) -> URL? {
|
|
195
|
+
guard let url = URL(string: value),
|
|
196
|
+
let scheme = url.scheme?.lowercased(),
|
|
197
|
+
let host = url.host?.lowercased(),
|
|
198
|
+
url.user == nil,
|
|
199
|
+
url.password == nil,
|
|
200
|
+
url.fragment == nil else { return nil }
|
|
201
|
+
let loopback = ["localhost", "127.0.0.1", "::1"].contains(host)
|
|
202
|
+
return scheme == "https" || (scheme == "http" && loopback) ? url : nil
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private static func canonicalResource(_ value: URL) -> URL? {
|
|
206
|
+
guard let secured = secureEndpoint(value.absoluteString),
|
|
207
|
+
var components = URLComponents(url: secured, resolvingAgainstBaseURL: false),
|
|
208
|
+
components.query == nil else { return nil }
|
|
209
|
+
if components.path == "/" { components.path = "" }
|
|
210
|
+
return components.url
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static func refresh(_ credential: OAuthCredential) async throws -> OAuthCredential {
|
|
214
|
+
guard let refreshToken = credential.refreshToken else { throw EnigmaAPIError.unauthenticated }
|
|
215
|
+
guard let protectedResource = canonicalResource(credential.resource) else { throw OAuthClientError.invalidResource }
|
|
216
|
+
let metadata = try await metadata(issuer: credential.issuer)
|
|
217
|
+
let tokens = try await tokenRequest(
|
|
218
|
+
endpoint: metadata.tokenEndpoint,
|
|
219
|
+
fields: [
|
|
220
|
+
"grant_type": "refresh_token",
|
|
221
|
+
"client_id": credential.clientID,
|
|
222
|
+
"refresh_token": refreshToken,
|
|
223
|
+
"resource": protectedResource.absoluteString,
|
|
224
|
+
]
|
|
225
|
+
)
|
|
226
|
+
return OAuthCredential(
|
|
227
|
+
accessToken: tokens.accessToken,
|
|
228
|
+
refreshToken: tokens.refreshToken ?? refreshToken,
|
|
229
|
+
expiresAt: Date().addingTimeInterval(TimeInterval(tokens.expiresIn)),
|
|
230
|
+
clientID: credential.clientID,
|
|
231
|
+
issuer: credential.issuer,
|
|
232
|
+
resource: protectedResource
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
static func metadata(issuer: URL) async throws -> OAuthAuthorizationServerMetadata {
|
|
237
|
+
guard let securedIssuer = secureEndpoint(issuer.absoluteString),
|
|
238
|
+
var components = URLComponents(url: securedIssuer, resolvingAgainstBaseURL: false),
|
|
239
|
+
components.query == nil,
|
|
240
|
+
components.path.isEmpty || components.path == "/" else {
|
|
241
|
+
throw OAuthClientError.invalidIssuer
|
|
242
|
+
}
|
|
243
|
+
components.path = ""
|
|
244
|
+
guard let canonicalIssuer = components.url else { throw OAuthClientError.invalidIssuer }
|
|
245
|
+
let url = canonicalIssuer.appending(path: ".well-known/oauth-authorization-server")
|
|
246
|
+
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 20)
|
|
247
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
248
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
249
|
+
guard let http = response as? HTTPURLResponse, http.statusCode == 200,
|
|
250
|
+
let metadata = try? JSONDecoder().decode(OAuthAuthorizationServerMetadata.self, from: data),
|
|
251
|
+
metadata.issuer == canonicalIssuer.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/")) else {
|
|
252
|
+
throw OAuthClientError.invalidMetadata
|
|
253
|
+
}
|
|
254
|
+
return metadata
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private static func tokenRequest(endpoint: String, fields: [String: String]) async throws -> OAuthTokenResponse {
|
|
258
|
+
guard let url = secureEndpoint(endpoint) else { throw OAuthClientError.invalidMetadata }
|
|
259
|
+
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30)
|
|
260
|
+
request.httpMethod = "POST"
|
|
261
|
+
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
|
262
|
+
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
263
|
+
var form = URLComponents()
|
|
264
|
+
form.queryItems = fields.sorted { $0.key < $1.key }.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
265
|
+
request.httpBody = form.percentEncodedQuery?.data(using: .utf8)
|
|
266
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
267
|
+
guard let http = response as? HTTPURLResponse else { throw OAuthClientError.invalidMetadata }
|
|
268
|
+
guard (200..<300).contains(http.statusCode), let tokens = try? JSONDecoder().decode(OAuthTokenResponse.self, from: data) else {
|
|
269
|
+
let reason = (try? JSONSerialization.jsonObject(with: data) as? [String: Any])?["error_description"] as? String
|
|
270
|
+
throw OAuthClientError.tokenExchangeFailed(reason ?? "OAuth token exchange failed.")
|
|
271
|
+
}
|
|
272
|
+
guard tokens.tokenType.lowercased() == "bearer" else { throw OAuthClientError.tokenExchangeFailed("OAuth server returned an unsupported token type.") }
|
|
273
|
+
return tokens
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
actor CredentialBroker {
|
|
278
|
+
private let store = SecureStore()
|
|
279
|
+
private let account = "oauth-credential"
|
|
280
|
+
private var credential: OAuthCredential?
|
|
281
|
+
private var directToken: String?
|
|
282
|
+
|
|
283
|
+
init() {
|
|
284
|
+
if let data = try? store.data(account: account) {
|
|
285
|
+
credential = try? JSONDecoder().decode(OAuthCredential.self, from: data)
|
|
286
|
+
}
|
|
287
|
+
directToken = try? store.string(account: "direct-token")
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
func accessToken() async throws -> String? {
|
|
291
|
+
if let directToken { return directToken }
|
|
292
|
+
guard var credential else { return nil }
|
|
293
|
+
if credential.expiresAt.timeIntervalSinceNow < 60 {
|
|
294
|
+
credential = try await OAuthClient.refresh(credential)
|
|
295
|
+
self.credential = credential
|
|
296
|
+
try store.set(JSONEncoder().encode(credential), account: account)
|
|
297
|
+
}
|
|
298
|
+
return credential.accessToken
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
func save(_ credential: OAuthCredential) throws {
|
|
302
|
+
self.credential = credential
|
|
303
|
+
directToken = nil
|
|
304
|
+
try store.remove(account: "direct-token")
|
|
305
|
+
try store.set(JSONEncoder().encode(credential), account: account)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
func saveDirectToken(_ value: String) throws {
|
|
309
|
+
credential = nil
|
|
310
|
+
directToken = value
|
|
311
|
+
try store.remove(account: account)
|
|
312
|
+
try store.setString(value, account: "direct-token")
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
func clear() throws {
|
|
316
|
+
credential = nil
|
|
317
|
+
directToken = nil
|
|
318
|
+
try store.remove(account: account)
|
|
319
|
+
try store.remove(account: "direct-token")
|
|
320
|
+
}
|
|
321
|
+
}
|