diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 940e6a8d2..da99c54fb 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -73,8 +73,8 @@ jobs: APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }} - VITE_OPEN_SECRET_API_URL: https://enclave.trymaple.ai - VITE_MAPLE_BILLING_API_URL: https://billing.opensecret.cloud + VITE_OPEN_SECRET_API_URL: ${{ github.event_name == 'pull_request' && 'https://enclave.secretgpt.ai' || 'https://enclave.trymaple.ai' }} + VITE_MAPLE_BILLING_API_URL: ${{ github.event_name == 'pull_request' && 'https://billing-dev.opensecret.cloud' || 'https://billing.opensecret.cloud' }} VITE_CLIENT_ID: ba5a14b5-d915-47b1-b7b1-afda52bc5fc6 with: projectPath: './frontend' @@ -136,8 +136,8 @@ jobs: env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - VITE_OPEN_SECRET_API_URL: https://enclave.trymaple.ai - VITE_MAPLE_BILLING_API_URL: https://billing.opensecret.cloud + VITE_OPEN_SECRET_API_URL: ${{ github.event_name == 'pull_request' && 'https://enclave.secretgpt.ai' || 'https://enclave.trymaple.ai' }} + VITE_MAPLE_BILLING_API_URL: ${{ github.event_name == 'pull_request' && 'https://billing-dev.opensecret.cloud' || 'https://billing.opensecret.cloud' }} VITE_CLIENT_ID: ba5a14b5-d915-47b1-b7b1-afda52bc5fc6 - name: Upload Linux Builds diff --git a/frontend/bun.lock b/frontend/bun.lock index 4a8acd536..3ab602753 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -4,7 +4,7 @@ "": { "name": "maple", "dependencies": { - "@opensecret/react": "1.3.8", + "@opensecret/react": "1.4.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", @@ -14,6 +14,7 @@ "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-query": "^5.56.2", "@tanstack/react-router": "^1.50.1", @@ -216,7 +217,7 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opensecret/react": ["@opensecret/react@1.3.8", "", { "dependencies": { "@peculiar/x509": "^1.12.2", "@stablelib/base64": "^2.0.0", "@stablelib/chacha20poly1305": "^2.0.0", "@stablelib/random": "^2.0.0", "cbor2": "^1.7.0", "tweetnacl": "^1.0.3", "zod": "^3.23.8" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-FIkMvPaIWEKuDjffz2W0EPrwwoiWOTuBMMIXr7zvXBhDJ5HwCZIQBmm9bAMprxWYQrWAqdiy80iBAC7MKIGFiw=="], + "@opensecret/react": ["@opensecret/react@1.4.0", "", { "dependencies": { "@peculiar/x509": "^1.12.2", "@stablelib/base64": "^2.0.0", "@stablelib/chacha20poly1305": "^2.0.0", "@stablelib/random": "^2.0.0", "cbor2": "^1.7.0", "tweetnacl": "^1.0.3", "zod": "^3.23.8" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-21L4V1AWoKTzcMKwe4OrM+sj3BKl5ATMODdFwsh+wdRvmYrG/OXf9AXmO7cP9LfsN7Gb1nj3fTmnwU8Gbx//Kw=="], "@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "@peculiar/asn1-x509-attr": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-B+DoudF+TCrxoJSTjjcY8Mmu+lbv8e7pXGWrhNp2/EGJp9EEcpzjBCar7puU57sGifyzaRVM03oD5L7t7PghQg=="], @@ -284,7 +285,7 @@ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="], - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw=="], + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], "@radix-ui/react-select": ["@radix-ui/react-select@2.1.6", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg=="], @@ -294,12 +295,16 @@ "@radix-ui/react-switch": ["@radix-ui/react-switch@1.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ=="], + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="], "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="], "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], @@ -1218,6 +1223,42 @@ "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + "@radix-ui/react-menu/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw=="], + + "@radix-ui/react-roving-focus/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-tabs/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-tabs/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-tabs/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-tabs/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + + "@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-tabs/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], @@ -1254,6 +1295,24 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-tabs/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-tabs/@radix-ui/react-presence/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-tabs/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-tabs/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], @@ -1311,5 +1370,7 @@ "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], } } diff --git a/frontend/package.json b/frontend/package.json index 71d20fc50..b8f4ea829 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "mdast-util-gfm-autolink-literal": "2.0.0" }, "dependencies": { - "@opensecret/react": "1.3.8", + "@opensecret/react": "1.4.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", @@ -26,6 +26,7 @@ "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-query": "^5.56.2", "@tanstack/react-router": "^1.50.1", diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 88a51c848..53555d351 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -17,6 +17,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "ahash" version = "0.7.8" @@ -84,6 +94,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.95" @@ -123,6 +183,45 @@ dependencies = [ "zbus", ] +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -249,6 +348,28 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "async-task" version = "4.7.1" @@ -301,6 +422,72 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa 1.0.14", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -614,6 +801,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.39" @@ -622,11 +833,70 @@ checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.52.6", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "combine" version = "4.6.7" @@ -682,6 +952,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.0" @@ -705,9 +985,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.8.0", - "core-foundation", + "core-foundation 0.10.0", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -718,7 +998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.8.0", - "core-foundation", + "core-foundation 0.10.0", "libc", ] @@ -768,6 +1048,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -808,6 +1089,32 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "darling" version = "0.20.10" @@ -843,6 +1150,26 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.3.11" @@ -969,6 +1296,12 @@ dependencies = [ "const-random", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dpi" version = "0.1.1" @@ -1025,6 +1358,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.0" @@ -1109,6 +1451,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1133,6 +1486,12 @@ dependencies = [ "log", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "field-offset" version = "0.3.6" @@ -1171,6 +1530,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1178,7 +1546,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1192,6 +1560,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1223,6 +1597,21 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1230,6 +1619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1297,6 +1687,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1626,6 +2017,31 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.7.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "hashbrown" version = "0.12.3" @@ -1647,6 +2063,16 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "byteorder", + "num-traits", +] + [[package]] name = "heck" version = "0.4.1" @@ -1725,6 +2151,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.6.0" @@ -1734,9 +2166,11 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", + "httpdate", "itoa 1.0.14", "pin-project-lite", "smallvec", @@ -1762,6 +2196,22 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.10" @@ -1775,7 +2225,7 @@ dependencies = [ "http-body", "hyper", "pin-project-lite", - "socket2", + "socket2 0.5.8", "tokio", "tower-service", "tracing", @@ -1990,6 +2440,26 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.8.0", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2015,6 +2485,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "0.4.8" @@ -2160,9 +2636,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" @@ -2232,7 +2708,10 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" name = "maple" version = "1.1.11" dependencies = [ + "anyhow", + "axum", "log", + "maple-proxy", "once_cell", "serde", "serde_json", @@ -2241,6 +2720,7 @@ dependencies = [ "tauri-plugin", "tauri-plugin-deep-link", "tauri-plugin-dialog", + "tauri-plugin-fs", "tauri-plugin-log", "tauri-plugin-opener", "tauri-plugin-os", @@ -2249,6 +2729,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "maple-proxy" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0730b3b84047f8cfbe04ccf0f8aef4f8124c847926871f147f121c80add6041" +dependencies = [ + "anyhow", + "async-stream", + "axum", + "clap", + "dotenvy", + "futures", + "http", + "opensecret", + "serde", + "serde_json", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", +] + [[package]] name = "markup5ever" version = "0.11.0" @@ -2263,12 +2766,27 @@ dependencies = [ "tendril", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matches" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.4" @@ -2290,6 +2808,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "minisign-verify" version = "0.2.3" @@ -2338,6 +2862,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2393,12 +2934,51 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2649,44 +3229,143 @@ dependencies = [ ] [[package]] -name = "objc2-web-kit" -version = "0.3.0" +name = "objc2-web-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" +dependencies = [ + "bitflags 2.8.0", + "block2 0.6.0", + "objc2 0.6.0", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "opensecret" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907713724b6c28098a88c7bbc2e88d01e9b95de3b1e3aec59c8479562fae89ed" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.22.1", + "bytes", + "chacha20poly1305", + "chrono", + "eventsource-stream", + "futures", + "getrandom 0.2.15", + "hex", + "percent-encoding", + "pin-project", + "rand 0.8.5", + "reqwest", + "ring", + "serde", + "serde_cbor", + "serde_json", + "sha2", + "thiserror 2.0.11", + "tokio", + "tracing", + "uuid", + "x25519-dalek", + "x509-parser", + "yasna", +] + +[[package]] +name = "openssl" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ "bitflags 2.8.0", - "block2 0.6.0", - "objc2 0.6.0", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation 0.3.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", ] [[package]] -name = "object" -version = "0.36.7" +name = "openssl-macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "memchr", + "proc-macro2", + "quote", + "syn 2.0.98", ] [[package]] -name = "once_cell" -version = "1.20.3" +name = "openssl-probe" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] -name = "open" -version = "5.3.2" +name = "openssl-sys" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ - "dunce", - "is-wsl", + "cc", "libc", - "pathdiff", + "pkg-config", + "vcpkg", ] [[package]] @@ -2740,6 +3419,12 @@ dependencies = [ "thiserror 2.0.11", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "pango" version = "0.18.3" @@ -2940,6 +3625,26 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3010,6 +3715,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3139,7 +3855,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2", + "socket2 0.5.8", "thiserror 2.0.11", "tokio", "tracing", @@ -3174,7 +3890,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.8", "tracing", "windows-sys 0.59.0", ] @@ -3339,8 +4055,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -3351,9 +4076,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -3377,18 +4108,22 @@ checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -3400,7 +4135,9 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper", + "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -3530,6 +4267,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.38.44" @@ -3620,6 +4366,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "schemars" version = "0.8.21" @@ -3659,6 +4414,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.22.0" @@ -3708,6 +4486,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.217" @@ -3742,6 +4530,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa 1.0.14", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.19" @@ -3847,6 +4645,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3911,6 +4718,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "softbuffer" version = "0.4.6" @@ -3920,7 +4737,7 @@ dependencies = [ "bytemuck", "cfg_aliases", "core-graphics", - "foreign-types", + "foreign-types 0.5.0", "js-sys", "log", "objc2 0.5.2", @@ -4070,6 +4887,27 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -4090,7 +4928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63c8b1020610b9138dd7b1e06cf259ae91aa05c30f3bd0d6b42a03997b92dec1" dependencies = [ "bitflags 2.8.0", - "core-foundation", + "core-foundation 0.10.0", "core-graphics", "crossbeam-channel", "dispatch", @@ -4618,6 +5456,15 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.37" @@ -4687,19 +5534,44 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.43.0" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "slab", + "socket2 0.6.0", + "tokio-macros", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", ] [[package]] @@ -4789,11 +5661,32 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "hdrhistogram", + "indexmap 2.7.1", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.8.0", + "bytes", + "http", + "http-body", + "pin-project-lite", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -4814,6 +5707,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -4837,6 +5731,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -4949,6 +5873,16 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -5003,6 +5937,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.13.1" @@ -5013,12 +5953,24 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "value-bag" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" @@ -5900,6 +6852,35 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "xattr" version = "1.5.0" @@ -5920,6 +6901,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" + [[package]] name = "yoke" version = "0.7.5" @@ -6075,6 +7062,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] [[package]] name = "zerovec" diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 519cc11e1..5bb11d6f1 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -30,5 +30,9 @@ tauri-plugin-deep-link = "2" tauri-plugin-opener = "2" tauri-plugin-os = "2" tauri-plugin-sign-in-with-apple = "1.0.2" -tokio = { version = "1.0", features = ["time"] } +tokio = { version = "1.0", features = ["net", "sync", "rt-multi-thread", "macros", "time"] } once_cell = "1.18.0" +maple-proxy = "0.1.1" +tauri-plugin-fs = "2" +anyhow = "1.0" +axum = "0.8" diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index d8a7b18c7..d30e3e95c 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -8,6 +8,42 @@ "permissions": [ "core:default", "updater:default", + "fs:default", + { + "identifier": "fs:allow-read-file", + "allow": [ + { "path": "$APPCONFIG/**" }, + { "path": "$HOME/.config/maple/**" } + ] + }, + { + "identifier": "fs:allow-write-file", + "allow": [ + { "path": "$APPCONFIG/**" }, + { "path": "$HOME/.config/maple/**" } + ] + }, + { + "identifier": "fs:allow-create", + "allow": [ + { "path": "$APPCONFIG/**" }, + { "path": "$HOME/.config/maple/**" } + ] + }, + { + "identifier": "fs:allow-exists", + "allow": [ + { "path": "$APPCONFIG/**" }, + { "path": "$HOME/.config/maple/**" } + ] + }, + { + "identifier": "fs:allow-mkdir", + "allow": [ + { "path": "$APPCONFIG" }, + { "path": "$HOME/.config/maple" } + ] + }, { "identifier": "opener:allow-open-url", "allow": [ diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index a78e400b2..6de9bde36 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -1,7 +1,8 @@ -use tauri::Emitter; +use tauri::{Emitter, Manager}; use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_opener; -use tauri_plugin_sign_in_with_apple; + +mod proxy; // This handles incoming deep links fn handle_deep_link_event(url: &str, app: &tauri::AppHandle) { @@ -22,7 +23,31 @@ pub fn run() { .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_fs::init()) + .manage(proxy::ProxyState::new()) + .invoke_handler(tauri::generate_handler![ + proxy::start_proxy, + proxy::stop_proxy, + proxy::get_proxy_status, + proxy::load_proxy_config, + proxy::save_proxy_settings, + proxy::test_proxy_port, + ]) .setup(|app| { + // Initialize proxy auto-start + { + let app_handle_proxy = app.handle().clone(); + tauri::async_runtime::spawn(async move { + // Small delay to ensure app is fully initialized + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // Create a new State wrapper for the async context + if let Err(e) = proxy::init_proxy_on_startup_simple(app_handle_proxy).await { + log::error!("Failed to initialize proxy: {}", e); + } + }); + } + // Set up the deep link handler // Use a cloned handle with 'static lifetime let app_handle = app.handle().clone(); diff --git a/frontend/src-tauri/src/proxy.rs b/frontend/src-tauri/src/proxy.rs new file mode 100644 index 000000000..3fb24cbaa --- /dev/null +++ b/frontend/src-tauri/src/proxy.rs @@ -0,0 +1,302 @@ +use anyhow::{anyhow, Result}; +use maple_proxy::{create_app, Config}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::sync::Arc; +use tauri::{AppHandle, Emitter, Manager, State}; +use tokio::net::TcpListener; +use tokio::sync::Mutex; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProxyConfig { + pub host: String, + pub port: u16, + pub api_key: String, + pub enabled: bool, + #[serde(default = "default_cors")] + pub enable_cors: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub backend_url: Option, + #[serde(default)] + pub auto_start: bool, +} + +fn default_cors() -> bool { + true +} + +impl Default for ProxyConfig { + fn default() -> Self { + Self { + host: "127.0.0.1".to_string(), + port: 8080, + api_key: String::new(), + enabled: false, + enable_cors: true, + backend_url: None, + auto_start: false, + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct ProxyStatus { + pub running: bool, + pub config: ProxyConfig, + pub error: Option, +} + +pub struct ProxyState { + handle: Arc>>>, + config: Arc>, + running: Arc>, +} + +impl ProxyState { + pub fn new() -> Self { + Self { + handle: Arc::new(Mutex::new(None)), + config: Arc::new(Mutex::new(ProxyConfig::default())), + running: Arc::new(Mutex::new(false)), + } + } +} + +#[tauri::command] +pub async fn start_proxy( + state: State<'_, ProxyState>, + config: ProxyConfig, +) -> Result { + log::info!( + "Starting proxy on {}:{} (cors={}, auto_start={})", + config.host, + config.port, + config.enable_cors, + config.auto_start + ); + + // Check if proxy is already running + let mut running = state.running.lock().await; + if *running { + return Err("Proxy is already running".to_string()); + } + + // Update config + let mut stored_config = state.config.lock().await; + *stored_config = config.clone(); + drop(stored_config); // Release config lock early + + // Use backend URL from config or fall back to production + let backend_url = config + .backend_url + .clone() + .unwrap_or_else(|| "https://enclave.trymaple.ai".to_string()); + + // Create maple-proxy config + let proxy_config = Config::new(config.host.clone(), config.port, backend_url) + .with_api_key(config.api_key.clone()) + .with_debug(false) + .with_cors(config.enable_cors); + + // Try to bind to the address first to check if port is available + let addr = proxy_config + .socket_addr() + .map_err(|e| format!("Invalid address: {}", e))?; + + let listener = match TcpListener::bind(&addr).await { + Ok(listener) => listener, + Err(e) => { + return Err(format!( + "Failed to bind to {}:{} - {}", + config.host, config.port, e + )); + } + }; + + // Create the app + let app = create_app(proxy_config); + + // Spawn the proxy server + let handle = tokio::spawn(async move { + log::info!("Maple proxy server running on http://{}", addr); + if let Err(e) = axum::serve(listener, app).await { + log::error!("Proxy server error: {}", e); + } + }); + + // Store the handle + let mut handle_guard = state.handle.lock().await; + *handle_guard = Some(handle); + drop(handle_guard); // Release handle lock early + + *running = true; + + // Save config to disk + if let Err(e) = save_proxy_config(&config).await { + log::error!("Failed to save proxy config: {}", e); + } + + Ok(ProxyStatus { + running: true, + config, + error: None, + }) +} + +#[tauri::command] +pub async fn stop_proxy(state: State<'_, ProxyState>) -> Result { + log::info!("Stopping proxy"); + + let mut running = state.running.lock().await; + if !*running { + return Err("Proxy is not running".to_string()); + } + + // Abort the proxy task + let mut handle_guard = state.handle.lock().await; + if let Some(handle) = handle_guard.take() { + handle.abort(); + } + drop(handle_guard); // Release handle lock before taking config lock to avoid deadlock + + *running = false; + + let config = state.config.lock().await.clone(); + + // Config persists even when stopped (we don't auto-start anyway) + + Ok(ProxyStatus { + running: false, + config, + error: None, + }) +} + +#[tauri::command] +pub async fn get_proxy_status(state: State<'_, ProxyState>) -> Result { + let running = *state.running.lock().await; + let config = state.config.lock().await.clone(); + + Ok(ProxyStatus { + running, + config, + error: None, + }) +} + +#[tauri::command] +pub async fn load_proxy_config() -> Result { + load_saved_proxy_config() + .await + .map_err(|e| format!("Failed to load proxy config: {}", e)) +} + +#[tauri::command] +pub async fn save_proxy_settings(config: ProxyConfig) -> Result<(), String> { + save_proxy_config(&config) + .await + .map_err(|e| format!("Failed to save proxy config: {}", e)) +} + +#[tauri::command] +pub async fn test_proxy_port(host: String, port: u16) -> Result { + // Try to bind to the address to check if it's available + let addr = format!("{}:{}", host, port); + match TcpListener::bind(&addr).await { + Ok(_) => Ok(true), // Port is available + Err(e) => { + if e.kind() == std::io::ErrorKind::AddrInUse { + Ok(false) // Port is in use + } else { + Err(format!("Failed to test port: {}", e)) + } + } + } +} + +// Helper functions for config persistence +async fn get_config_path() -> Result { + // Use a hardcoded app name for the data directory + let app_name = "maple"; + let home_dir = std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) + .map_err(|_| anyhow!("Failed to get home directory"))?; + + // Note: We use ~/.config on all platforms for simplicity. + // This works well on Linux and macOS (our currently supported platforms). + // While macOS traditionally uses ~/Library/Application Support, many modern + // cross-platform tools use ~/.config on macOS as well. + // If Windows support is added in the future, consider using %APPDATA% instead. + let app_dir = PathBuf::from(home_dir).join(".config").join(app_name); + + // Ensure directory exists + tokio::fs::create_dir_all(&app_dir).await?; + + Ok(app_dir.join("proxy_config.json")) +} + +async fn save_proxy_config(config: &ProxyConfig) -> Result<()> { + let path = get_config_path().await?; + let json = serde_json::to_string_pretty(config)?; + + // Write the config file + tokio::fs::write(&path, json).await?; + + // Set restrictive permissions on Unix systems (owner read/write only) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let perms = std::fs::Permissions::from_mode(0o600); + tokio::fs::set_permissions(&path, perms).await?; + } + + Ok(()) +} + +async fn load_saved_proxy_config() -> Result { + let path = get_config_path().await?; + + if !path.exists() { + return Ok(ProxyConfig::default()); + } + + let json = tokio::fs::read_to_string(path).await?; + let config: ProxyConfig = serde_json::from_str(&json)?; + Ok(config) +} + +// Initialize proxy on app startup if auto_start is enabled +pub async fn init_proxy_on_startup_simple(app_handle: AppHandle) -> Result<()> { + // Load saved config + let config = load_saved_proxy_config().await?; + + // Check if auto-start is enabled and we have an API key + if config.auto_start && !config.api_key.is_empty() { + log::info!("Auto-starting proxy from saved config"); + + // Get the proxy state from the app handle + let proxy_state: tauri::State = app_handle.state(); + + // Try to start the proxy + match start_proxy(proxy_state, config.clone()).await { + Ok(_) => { + log::info!( + "Proxy auto-started successfully on {}:{}", + config.host, + config.port + ); + // Optionally emit an event to notify the frontend + let _ = app_handle.emit("proxy-autostarted", &config); + } + Err(e) => { + log::error!("Failed to auto-start proxy: {}", e); + // Emit an event to notify the frontend of the failure + let _ = app_handle.emit("proxy-autostart-failed", e); + } + } + } else { + log::info!("Proxy auto-start is disabled or no API key configured"); + } + + Ok(()) +} diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index 51e85a84e..fa176cf72 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -11,6 +11,8 @@ import { ErrorFallback } from "./components/ErrorFallback"; import { NotFoundFallback } from "./components/NotFoundFallback"; import { BillingServiceProvider } from "./components/BillingServiceProvider"; import { DeepLinkHandler } from "./components/DeepLinkHandler"; +import { NotificationProvider } from "./contexts/NotificationContext"; +import { ProxyEventListener } from "./components/ProxyEventListener"; // Create a new router instance const router = createRouter({ @@ -47,61 +49,64 @@ const queryClient = new QueryClient({ export default function App() { return ( - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + ); } diff --git a/frontend/src/billing/billingApi.ts b/frontend/src/billing/billingApi.ts index 457e2dc38..117d05b5e 100644 --- a/frontend/src/billing/billingApi.ts +++ b/frontend/src/billing/billingApi.ts @@ -1,3 +1,8 @@ +// API Credit Purchase Constants +export const MIN_PURCHASE_CREDITS = 10000; +export const MIN_PURCHASE_AMOUNT = 10; +export const MIN_PURCHASE_ERROR = `Minimum purchase is ${MIN_PURCHASE_CREDITS.toLocaleString()} credits ($${MIN_PURCHASE_AMOUNT})`; + export type BillingStatus = { is_subscribed: boolean; stripe_customer_id: string | null; @@ -602,3 +607,181 @@ export async function updateTeamName( return response.json(); } + +// API Credits Types +export type ApiCreditBalance = { + balance: number; + auto_topup_enabled: boolean; + auto_topup_threshold: number; + auto_topup_amount: number; +}; + +export type ApiCreditSettings = { + user_id: string; + auto_topup_enabled: boolean; + auto_topup_threshold: number; + auto_topup_amount: number; + stripe_customer_id: string | null; + stripe_payment_method_id: string | null; + created_at: string; + updated_at: string; +}; + +export type PurchaseCreditsRequest = { + credits: number; + success_url: string; + cancel_url: string; +}; + +export type PurchaseCreditsZapriteRequest = { + credits: number; + email: string; + success_url: string; +}; + +export type CheckoutResponse = { + checkout_url: string; +}; + +export type UpdateCreditSettingsRequest = { + auto_topup_enabled?: boolean; + auto_topup_threshold?: number; + auto_topup_amount?: number; +}; + +// API Credits Endpoints +export async function fetchApiCreditBalance(thirdPartyToken: string): Promise { + const response = await fetch( + `${import.meta.env.VITE_MAPLE_BILLING_API_URL}/v1/maple/api-credits/balance`, + { + headers: { + Authorization: `Bearer ${thirdPartyToken}`, + "Content-Type": "application/json" + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Fetch credit balance error:", errorText); + if (response.status === 401) { + throw new Error("Unauthorized"); + } + throw new Error(`Failed to fetch credit balance: ${errorText}`); + } + + return response.json(); +} + +export async function fetchApiCreditSettings(thirdPartyToken: string): Promise { + const response = await fetch( + `${import.meta.env.VITE_MAPLE_BILLING_API_URL}/v1/maple/api-credits/settings`, + { + headers: { + Authorization: `Bearer ${thirdPartyToken}`, + "Content-Type": "application/json" + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Fetch credit settings error:", errorText); + if (response.status === 401) { + throw new Error("Unauthorized"); + } + throw new Error(`Failed to fetch credit settings: ${errorText}`); + } + + return response.json(); +} + +export async function purchaseApiCredits( + thirdPartyToken: string, + data: PurchaseCreditsRequest +): Promise { + const response = await fetch( + `${import.meta.env.VITE_MAPLE_BILLING_API_URL}/v1/maple/api-credits/purchase`, + { + method: "POST", + headers: { + Authorization: `Bearer ${thirdPartyToken}`, + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Purchase credits error:", errorText); + if (response.status === 401) { + throw new Error("Unauthorized"); + } + if (response.status === 400) { + throw new Error(MIN_PURCHASE_ERROR); + } + throw new Error(`Failed to create checkout session: ${errorText}`); + } + + return response.json(); +} + +export async function purchaseApiCreditsZaprite( + thirdPartyToken: string, + data: PurchaseCreditsZapriteRequest +): Promise { + const response = await fetch( + `${import.meta.env.VITE_MAPLE_BILLING_API_URL}/v1/maple/api-credits/purchase-zaprite`, + { + method: "POST", + headers: { + Authorization: `Bearer ${thirdPartyToken}`, + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Purchase credits with Zaprite error:", errorText); + if (response.status === 401) { + throw new Error("Unauthorized"); + } + if (response.status === 400) { + throw new Error(MIN_PURCHASE_ERROR); + } + throw new Error(`Failed to create Zaprite checkout: ${errorText}`); + } + + return response.json(); +} + +export async function updateApiCreditSettings( + thirdPartyToken: string, + data: UpdateCreditSettingsRequest +): Promise { + const response = await fetch( + `${import.meta.env.VITE_MAPLE_BILLING_API_URL}/v1/maple/api-credits/settings`, + { + method: "PUT", + headers: { + Authorization: `Bearer ${thirdPartyToken}`, + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Update credit settings error:", errorText); + if (response.status === 401) { + throw new Error("Unauthorized"); + } + throw new Error(`Failed to update credit settings: ${errorText}`); + } + + return response.json(); +} diff --git a/frontend/src/billing/billingService.ts b/frontend/src/billing/billingService.ts index 60ffd37b9..caddb5e58 100644 --- a/frontend/src/billing/billingService.ts +++ b/frontend/src/billing/billingService.ts @@ -16,7 +16,18 @@ import { removeTeamMember, leaveTeam, revokeTeamInvite, - updateTeamName + updateTeamName, + fetchApiCreditBalance, + fetchApiCreditSettings, + purchaseApiCredits, + purchaseApiCreditsZaprite, + updateApiCreditSettings, + ApiCreditBalance, + ApiCreditSettings, + PurchaseCreditsRequest, + PurchaseCreditsZapriteRequest, + CheckoutResponse, + UpdateCreditSettingsRequest } from "./billingApi"; import type { TeamStatus, @@ -156,6 +167,27 @@ class BillingService { async updateTeamName(name: string): Promise { return this.executeWithToken((token) => updateTeamName(token, name)); } + + // API Credits methods + async getApiCreditBalance(): Promise { + return this.executeWithToken((token) => fetchApiCreditBalance(token)); + } + + async getApiCreditSettings(): Promise { + return this.executeWithToken((token) => fetchApiCreditSettings(token)); + } + + async purchaseApiCredits(data: PurchaseCreditsRequest): Promise { + return this.executeWithToken((token) => purchaseApiCredits(token, data)); + } + + async purchaseApiCreditsZaprite(data: PurchaseCreditsZapriteRequest): Promise { + return this.executeWithToken((token) => purchaseApiCreditsZaprite(token, data)); + } + + async updateApiCreditSettings(data: UpdateCreditSettingsRequest): Promise { + return this.executeWithToken((token) => updateApiCreditSettings(token, data)); + } } // Singleton instance diff --git a/frontend/src/components/AccountMenu.tsx b/frontend/src/components/AccountMenu.tsx index 5f8271e2e..d20bf5677 100644 --- a/frontend/src/components/AccountMenu.tsx +++ b/frontend/src/components/AccountMenu.tsx @@ -6,7 +6,8 @@ import { ArrowUpCircle, Mail, Users, - AlertCircle + AlertCircle, + Key } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -41,9 +42,10 @@ import { useLocalState } from "@/state/useLocalState"; import { useQueryClient, useQuery } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; import { getBillingService } from "@/billing/billingService"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import type { TeamStatus } from "@/types/team"; import { TeamManagementDialog } from "@/components/team/TeamManagementDialog"; +import { ApiKeyManagementDialog } from "@/components/apikeys/ApiKeyManagementDialog"; function ConfirmDeleteDialog() { const { clearHistory } = useLocalState(); @@ -82,6 +84,8 @@ export function AccountMenu() { const { billingStatus } = useLocalState(); const [isPortalLoading, setIsPortalLoading] = useState(false); const [isTeamDialogOpen, setIsTeamDialogOpen] = useState(false); + const [isApiKeyDialogOpen, setIsApiKeyDialogOpen] = useState(false); + const [isIOS, setIsIOS] = useState(false); const hasStripeAccount = billingStatus?.stripe_customer_id !== null; const productName = billingStatus?.product_name || ""; @@ -92,6 +96,29 @@ export function AccountMenu() { const showUpgrade = !isMax && !isTeamPlan; const showManage = (isPro || isMax || isStarter || isTeamPlan) && hasStripeAccount; + // Detect iOS platform + useEffect(() => { + const detectPlatform = async () => { + try { + const isTauri = await import("@tauri-apps/api/core") + .then((m) => m.isTauri()) + .catch(() => false); + + if (isTauri) { + const platform = await import("@tauri-apps/plugin-os") + .then((m) => m.type()) + .catch(() => null); + + setIsIOS(platform === "ios"); + } + } catch (error) { + console.error("Error detecting platform:", error); + } + }; + + detectPlatform(); + }, []); + // Fetch team status if user has team plan const { data: teamStatus } = useQuery({ queryKey: ["teamStatus"], @@ -246,6 +273,12 @@ export function AccountMenu() { )} + {!isIOS && ( + setIsApiKeyDialogOpen(true)}> + + API Management + + )} @@ -272,6 +305,7 @@ export function AccountMenu() { onOpenChange={setIsTeamDialogOpen} teamStatus={teamStatus} /> + diff --git a/frontend/src/components/GlobalNotification.tsx b/frontend/src/components/GlobalNotification.tsx new file mode 100644 index 000000000..ac07618da --- /dev/null +++ b/frontend/src/components/GlobalNotification.tsx @@ -0,0 +1,104 @@ +import { useEffect, useState, useCallback, useRef } from "react"; +import { CheckCircle, AlertCircle, X, Server } from "lucide-react"; +import { cn } from "@/utils/utils"; + +export interface Notification { + id: string; + type: "success" | "error" | "info"; + title: string; + message?: string; + icon?: React.ReactNode; + duration?: number; // ms, 0 = permanent +} + +interface GlobalNotificationProps { + notification: Notification | null; + onDismiss: () => void; +} + +export function GlobalNotification({ notification, onDismiss }: GlobalNotificationProps) { + const [isVisible, setIsVisible] = useState(false); + const [isLeaving, setIsLeaving] = useState(false); + const dismissTimeoutRef = useRef | null>(null); + + const handleDismiss = useCallback(() => { + setIsLeaving(true); + dismissTimeoutRef.current = setTimeout(() => { + setIsVisible(false); + onDismiss(); + }, 200); // Match animation duration + }, [onDismiss]); + + useEffect(() => { + if (notification) { + // Trigger enter animation + setIsVisible(true); + setIsLeaving(false); + + // Auto-dismiss after duration + if (notification.duration && notification.duration > 0) { + const timer = setTimeout(() => { + handleDismiss(); + }, notification.duration); + return () => clearTimeout(timer); + } + } else { + setIsVisible(false); + } + }, [notification, handleDismiss]); + + // Cleanup dismiss timeout on unmount + useEffect(() => { + return () => { + if (dismissTimeoutRef.current) { + clearTimeout(dismissTimeoutRef.current); + } + }; + }, []); + + if (!notification || !isVisible) return null; + + const getIcon = () => { + if (notification.icon) return notification.icon; + + switch (notification.type) { + case "success": + return ; + case "error": + return ; + case "info": + return ; + } + }; + + return ( +
+
+
{getIcon()}
+ +
+

{notification.title}

+ {notification.message && ( +

{notification.message}

+ )} +
+ + +
+
+ ); +} diff --git a/frontend/src/components/ProxyEventListener.tsx b/frontend/src/components/ProxyEventListener.tsx new file mode 100644 index 000000000..b245891aa --- /dev/null +++ b/frontend/src/components/ProxyEventListener.tsx @@ -0,0 +1,52 @@ +import { useEffect } from "react"; +import { listen } from "@tauri-apps/api/event"; +import { useNotification } from "@/contexts/NotificationContext"; +import { Server } from "lucide-react"; + +export function ProxyEventListener() { + const { showNotification } = useNotification(); + + useEffect(() => { + let unlistenAutoStarted: (() => void) | null = null; + let unlistenAutoStartFailed: (() => void) | null = null; + + const setupListeners = async () => { + try { + // Listen for proxy auto-start success + unlistenAutoStarted = await listen("proxy-autostarted", (event) => { + const config = event.payload as { host: string; port: number }; + showNotification({ + type: "success", + title: "Proxy Started", + message: `Local proxy is running on ${config.host}:${config.port}`, + icon: , + duration: 5000 + }); + }); + + // Listen for proxy auto-start failure + unlistenAutoStartFailed = await listen("proxy-autostart-failed", (event) => { + const error = event.payload as string; + showNotification({ + type: "error", + title: "Proxy Auto-Start Failed", + message: error || "Failed to start the proxy automatically", + duration: 7000 + }); + }); + } catch (error) { + console.error("Failed to setup proxy event listeners:", error); + } + }; + + setupListeners(); + + // Cleanup listeners on unmount + return () => { + if (unlistenAutoStarted) unlistenAutoStarted(); + if (unlistenAutoStartFailed) unlistenAutoStartFailed(); + }; + }, [showNotification]); + + return null; // This component doesn't render anything +} diff --git a/frontend/src/components/apikeys/ApiCreditsSection.tsx b/frontend/src/components/apikeys/ApiCreditsSection.tsx new file mode 100644 index 000000000..7285b262c --- /dev/null +++ b/frontend/src/components/apikeys/ApiCreditsSection.tsx @@ -0,0 +1,356 @@ +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Input } from "@/components/ui/input"; +import { Loader2, CreditCard, Bitcoin, Coins, CheckCircle, Edit } from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; +import { getBillingService } from "@/billing/billingService"; +import { useOpenSecret } from "@opensecret/react"; +import { + MIN_PURCHASE_CREDITS, + MIN_PURCHASE_AMOUNT, + type ApiCreditBalance +} from "@/billing/billingApi"; + +interface CreditPackage { + credits: number; + price: number; + label: string; +} + +const CREDIT_PACKAGES: CreditPackage[] = [ + { credits: 20000, price: 20, label: "20,000 credits" }, + { credits: 50000, price: 50, label: "50,000 credits" }, + { credits: 100000, price: 100, label: "100,000 credits" }, + { credits: 500000, price: 500, label: "500,000 credits" } +]; + +interface ApiCreditsSectionProps { + showSuccessMessage?: boolean; +} + +export function ApiCreditsSection({ showSuccessMessage = false }: ApiCreditsSectionProps) { + const [selectedPackage, setSelectedPackage] = useState(CREDIT_PACKAGES[0]); + const [isPurchasing, setIsPurchasing] = useState(false); + const [paymentMethod, setPaymentMethod] = useState<"stripe" | "zaprite" | null>(null); + const [showSuccess, setShowSuccess] = useState(showSuccessMessage); + const [showCustomAmount, setShowCustomAmount] = useState(false); + const [customAmount, setCustomAmount] = useState(""); + const [purchaseError, setPurchaseError] = useState(null); + const { auth } = useOpenSecret(); + + const userEmail = auth.user?.user.email; + + useEffect(() => { + if (showSuccessMessage) { + setShowSuccess(true); + // Hide success message after 5 seconds + const timer = setTimeout(() => setShowSuccess(false), 5000); + return () => clearTimeout(timer); + } + }, [showSuccessMessage]); + + // Fetch credit balance + const { + data: creditBalance, + isLoading: isLoadingBalance, + error: balanceError + } = useQuery({ + queryKey: ["apiCreditBalance"], + queryFn: async () => { + const billingService = getBillingService(); + return await billingService.getApiCreditBalance(); + }, + enabled: !!auth.user && !auth.loading + }); + + const formatCredits = (credits: number) => { + return new Intl.NumberFormat("en-US").format(credits); + }; + + const handlePurchase = async (method: "stripe" | "zaprite") => { + // Clear any previous errors + setPurchaseError(null); + + // Validate custom amount if in custom mode + let finalPackage = selectedPackage; + if (showCustomAmount) { + const amount = parseInt(customAmount); + if (isNaN(amount) || amount < 10 || amount > 1000) { + return; // Invalid amount, don't proceed + } + finalPackage = { + credits: amount * 1000, + price: amount, + label: `${formatCredits(amount * 1000)} credits` + }; + } + + setIsPurchasing(true); + setPaymentMethod(method); + + try { + const billingService = getBillingService(); + + // Determine success/cancel URLs based on platform + let successUrl: string; + let cancelUrl: string | undefined; + + try { + // Check if we're in Tauri environment + const isTauri = await import("@tauri-apps/api/core") + .then((m) => m.isTauri()) + .catch(() => false); + + const isTauriIOS = + isTauri && + (await import("@tauri-apps/plugin-os") + .then((m) => m.type()) + .then((type) => type === "ios") + .catch(() => false)); + + // For iOS, use Universal Links that match the AASA configuration + if (isTauriIOS) { + successUrl = `https://trymaple.ai/payment-success-credits?source=${method}`; + cancelUrl = + method === "stripe" ? `https://trymaple.ai/payment-canceled?source=stripe` : undefined; + } else { + // For web or desktop, use regular URLs with query params + const baseUrl = window.location.origin; + successUrl = `${baseUrl}/?credits_success=true`; + cancelUrl = method === "stripe" ? `${baseUrl}/` : undefined; + } + } catch (error) { + console.error("Error determining platform:", error); + // Fall back to regular URLs if platform detection fails + const baseUrl = window.location.origin; + successUrl = `${baseUrl}/?credits_success=true`; + cancelUrl = method === "stripe" ? `${baseUrl}/` : undefined; + } + + if (method === "stripe") { + const response = await billingService.purchaseApiCredits({ + credits: finalPackage.credits, + success_url: successUrl, + cancel_url: cancelUrl || successUrl + }); + + // Redirect to Stripe checkout + // Note: This feature is only exposed on desktop/web (not iOS), where window.location.href works correctly. + // For iOS, we would need special handling with invoke("plugin:opener|open_url"), but that's not needed here. + window.location.href = response.checkout_url; + } else { + // For Zaprite, we need the user's email + if (!userEmail) { + setPurchaseError("Email is required for Bitcoin payments"); + setIsPurchasing(false); + setPaymentMethod(null); + return; + } + const response = await billingService.purchaseApiCreditsZaprite({ + credits: finalPackage.credits, + email: userEmail, + success_url: successUrl + }); + + // Redirect to Zaprite checkout + // Note: This feature is only exposed on desktop/web (not iOS), where window.location.href works correctly. + // For iOS, we would need special handling with invoke("plugin:opener|open_url"), but that's not needed here. + window.location.href = response.checkout_url; + } + } catch (error) { + console.error("Failed to create checkout session:", error); + setPurchaseError("Failed to create checkout session. Please try again."); + } finally { + setIsPurchasing(false); + setPaymentMethod(null); + } + }; + + if (isLoadingBalance) { + return ( + +
+ +
+
+ ); + } + + if (balanceError) { + return ( + +

Failed to load credit balance

+
+ ); + } + + return ( +
+ {/* Success Message */} + {showSuccess && ( + + + + Payment successful! Your credits have been added to your account. + + + )} + + {/* Error Message */} + {purchaseError && ( + + {purchaseError} + + )} + + {/* Current Balance */} + +
+
+

API Credit Balance

+

+ + {formatCredits(creditBalance?.balance || 0)} +

+

+ $1 per 1,000 credits • Use for API requests +

+
+
+
+ + {/* Purchase Credits */} + +

Purchase Credits

+ + {/* Credit Package Selection */} +
+ {CREDIT_PACKAGES.map((pkg) => ( + + ))} +
+ + {/* Custom Amount Button */} +
+ + + {showCustomAmount && ( +
+ setCustomAmount(e.target.value)} + onKeyDown={(e) => { + // Prevent decimal point + if (e.key === ".") { + e.preventDefault(); + } + }} + min="10" + max="1000" + step="1" + className="text-center" + /> + {customAmount && ( +

+ {(() => { + const amount = parseInt(customAmount); + if (isNaN(amount) || amount < 10) { + return "Minimum $10 required"; + } else if (amount > 1000) { + return "Maximum $1,000 allowed"; + } else { + return `${formatCredits(amount * 1000)} credits for $${amount}`; + } + })()} +

+ )} +
+ )} +
+ + {/* Payment Methods */} +
+ + + +
+ +

+ Minimum purchase: {MIN_PURCHASE_CREDITS.toLocaleString()} credits (${MIN_PURCHASE_AMOUNT}) +

+
+
+ ); +} diff --git a/frontend/src/components/apikeys/ApiKeyDashboard.tsx b/frontend/src/components/apikeys/ApiKeyDashboard.tsx new file mode 100644 index 000000000..0b58b51cc --- /dev/null +++ b/frontend/src/components/apikeys/ApiKeyDashboard.tsx @@ -0,0 +1,287 @@ +import { useState, useEffect } from "react"; +import { DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { + Plus, + Loader2, + Sparkles, + Zap, + Shield, + Rocket, + Server, + Key, + CreditCard +} from "lucide-react"; +import { CreateApiKeyDialog } from "./CreateApiKeyDialog"; +import { ApiKeysList } from "./ApiKeysList"; +import { ApiCreditsSection } from "./ApiCreditsSection"; +import { ProxyConfigSection } from "./ProxyConfigSection"; +import { useOpenSecret } from "@opensecret/react"; +import { useQuery } from "@tanstack/react-query"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useLocalState } from "@/state/useLocalState"; +import { useNavigate } from "@tanstack/react-router"; +import { proxyService } from "@/services/proxyService"; + +interface ApiKey { + name: string; + created_at: string; +} + +interface ApiKeyDashboardProps { + showCreditSuccessMessage?: boolean; +} + +export function ApiKeyDashboard({ showCreditSuccessMessage = false }: ApiKeyDashboardProps) { + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [isDesktop, setIsDesktop] = useState(false); + const { listApiKeys, auth, createApiKey } = useOpenSecret(); + const { billingStatus } = useLocalState(); + const navigate = useNavigate(); + + useEffect(() => { + proxyService.isTauriDesktop().then(setIsDesktop); + }, []); + + // Check if user has API access (Pro, Team, or Max plans only - not Starter) + const isBillingLoading = billingStatus === null; + const productName = billingStatus?.product_name || ""; + const isPro = productName.toLowerCase().includes("pro"); + const isMax = productName.toLowerCase().includes("max"); + const isTeamPlan = productName.toLowerCase().includes("team"); + const hasApiAccess = isPro || isMax || isTeamPlan; + + // Fetch API keys + const { + data: apiKeys, + isLoading, + error, + refetch + } = useQuery({ + queryKey: ["apiKeys"], + queryFn: async () => { + const response = await listApiKeys(); + // Sort by creation date (newest first) + return response.keys.sort( + (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); + }, + enabled: !!auth.user && !auth.loading + }); + + const handleKeyCreated = () => { + refetch(); + setIsCreateDialogOpen(false); + }; + + const handleKeyDeleted = () => { + refetch(); + }; + + const handleProxyApiKeyRequest = async (name: string): Promise => { + // Create a new API key for the proxy directly + try { + const response = await createApiKey(name); + // Refetch to update the list + await refetch(); + return response.key; + } catch (error) { + console.error("Failed to create API key for proxy:", error); + throw error; + } + }; + + // Show loading state if billing status or API keys are loading + if (isBillingLoading || isLoading) { + return ( + <> + + API Management + Loading... + +
+ +
+ + ); + } + + if (error) { + return ( + <> + + API Management + + Failed to load API keys. Please try again. + + + + ); + } + + // Show upgrade prompt for users without API access (Free and Starter plans) + if (!hasApiAccess) { + return ( + <> + + + + Unlock API Access + + + Upgrade to a paid plan to access powerful API features + + + +
+ +
+
+
+ +
+
+

Programmatic Access

+

+ Integrate Maple directly into your applications and workflows +

+
+
+ +
+
+ +
+
+

Secure API Keys

+

+ Create and manage multiple API keys with granular control +

+
+
+ +
+
+ +
+
+

Pay-As-You-Go Credits

+

+ Purchase credits for API usage at just $1 per 1,000 credits +

+
+
+
+
+ +
+

+ Starting at just $20/month with + the Pro plan +

+ + + +

+ Unlock API access, increased limits, and premium features +

+
+
+ + ); + } + + return ( + <> + + API Access + + Manage API keys and configure access to Maple services.{" "} +
+ Read more + + + + + + + + + Credits + + + + API Keys + + {isDesktop && ( + + + Local Proxy + + )} + + + +
+ +
+
+ + +
+
+

API Keys

+ + {/* Create button */} + + + {/* API Keys list */} + {apiKeys && apiKeys.length > 0 && ( + + )} + + {/* Info text */} +
+

API keys allow you to integrate Maple into your applications and workflows.

+

+ Keep your API keys secure and never share them publicly. Treat them like + passwords. +

+
+
+
+
+ + {isDesktop && ( + +
+ +
+
+ )} +
+ + {/* Create dialog */} + + + ); +} diff --git a/frontend/src/components/apikeys/ApiKeyManagementDialog.tsx b/frontend/src/components/apikeys/ApiKeyManagementDialog.tsx new file mode 100644 index 000000000..84448a40d --- /dev/null +++ b/frontend/src/components/apikeys/ApiKeyManagementDialog.tsx @@ -0,0 +1,22 @@ +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { ApiKeyDashboard } from "./ApiKeyDashboard"; + +interface ApiKeyManagementDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + showCreditSuccessMessage?: boolean; +} + +export function ApiKeyManagementDialog({ + open, + onOpenChange, + showCreditSuccessMessage = false +}: ApiKeyManagementDialogProps) { + return ( + + + + + + ); +} diff --git a/frontend/src/components/apikeys/ApiKeysList.tsx b/frontend/src/components/apikeys/ApiKeysList.tsx new file mode 100644 index 000000000..a0546936b --- /dev/null +++ b/frontend/src/components/apikeys/ApiKeysList.tsx @@ -0,0 +1,118 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Trash2, Calendar, Loader2 } from "lucide-react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from "@/components/ui/alert-dialog"; +import { useOpenSecret } from "@opensecret/react"; + +interface ApiKey { + name: string; + created_at: string; +} + +interface ApiKeysListProps { + apiKeys: ApiKey[]; + onKeyDeleted: () => void; +} + +export function ApiKeysList({ apiKeys, onKeyDeleted }: ApiKeysListProps) { + const [deleteKeyName, setDeleteKeyName] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + const { deleteApiKey } = useOpenSecret(); + + const handleDelete = async () => { + if (!deleteKeyName) return; + + setIsDeleting(true); + try { + await deleteApiKey(deleteKeyName); + console.log(`API key "${deleteKeyName}" deleted successfully`); + onKeyDeleted(); + } catch (error) { + console.error("Failed to delete API key:", error); + } finally { + setIsDeleting(false); + setDeleteKeyName(null); + } + }; + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit" + }).format(date); + }; + + return ( + <> +
+
+ {apiKeys.map((key) => ( +
+
+

{key.name}

+

+ + Created {formatDate(key.created_at)} +

+
+ +
+ ))} +
+
+ + {/* Delete confirmation dialog */} + setDeleteKeyName(null)}> + + + Delete API Key + + Are you sure you want to delete the API key "{deleteKeyName}"? This action cannot be + undone and any applications using this key will stop working immediately. + + + + Cancel + + {isDeleting ? ( + <> + + Deleting... + + ) : ( + "Delete" + )} + + + + + + ); +} diff --git a/frontend/src/components/apikeys/CreateApiKeyDialog.tsx b/frontend/src/components/apikeys/CreateApiKeyDialog.tsx new file mode 100644 index 000000000..da2acc416 --- /dev/null +++ b/frontend/src/components/apikeys/CreateApiKeyDialog.tsx @@ -0,0 +1,217 @@ +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Copy, CheckCircle, Loader2, AlertCircle } from "lucide-react"; +import { useOpenSecret } from "@opensecret/react"; + +interface CreateApiKeyDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onKeyCreated: () => void; +} + +export function CreateApiKeyDialog({ open, onOpenChange, onKeyCreated }: CreateApiKeyDialogProps) { + const [keyName, setKeyName] = useState(""); + const [isCreating, setIsCreating] = useState(false); + const [createdKey, setCreatedKey] = useState(null); + const [copied, setCopied] = useState(false); + const [error, setError] = useState(null); + const { createApiKey } = useOpenSecret(); + + const handleCreate = async () => { + const trimmedName = keyName.trim(); + + // Validation + if (!trimmedName) { + setError("Please enter a name for your API key"); + return; + } + + if (trimmedName.length > 100) { + setError("API key name must be 100 characters or less"); + return; + } + + setIsCreating(true); + setError(null); + + try { + const response = await createApiKey(trimmedName); + setCreatedKey(response.key); + console.log("API key created successfully"); + } catch (error) { + console.error("Failed to create API key:", error); + + // Handle specific error cases with user-friendly messages + const err = error as { status?: number; message?: string }; + if (err?.status === 409 || err?.message?.toLowerCase().includes("conflict")) { + setError( + `An API key named "${trimmedName}" already exists. Please choose a different name.` + ); + } else if (err?.status === 400 || err?.message?.toLowerCase().includes("invalid")) { + setError( + "Invalid API key name. Please use only letters, numbers, spaces, hyphens, and underscores." + ); + } else if (err?.status === 401 || err?.message?.toLowerCase().includes("unauthorized")) { + setError("You are not authorized to create API keys. Please check your subscription plan."); + } else if (err?.status === 429 || err?.message?.toLowerCase().includes("limit")) { + setError( + "You've reached the maximum number of API keys. Please delete an existing key first." + ); + } else { + setError(err?.message || "Failed to create API key. Please try again."); + } + } finally { + setIsCreating(false); + } + }; + + const handleCopy = async () => { + if (!createdKey) return; + + try { + await navigator.clipboard.writeText(createdKey); + setCopied(true); + console.log("API key copied to clipboard"); + setTimeout(() => setCopied(false), 2000); + } catch (error) { + console.error("Failed to copy:", error); + } + }; + + const handleClose = () => { + if (createdKey) { + onKeyCreated(); + } + // Reset state + setKeyName(""); + setCreatedKey(null); + setCopied(false); + setError(null); + onOpenChange(false); + }; + + return ( + { + if (!newOpen) { + handleClose(); + } + }} + > + + + Create API Key + + {createdKey + ? "Your API key has been created. Copy it now - you won't be able to see it again." + : "Create a new API key for programmatic access to Maple."} + + + + {!createdKey ? ( + <> + {error && ( + + + {error} + + )} + +
+
+ + setKeyName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && !isCreating) { + handleCreate(); + } + }} + disabled={isCreating} + maxLength={100} + /> + + {keyName.trim().length}/100 characters + +
+
+ + + + + + + ) : ( + <> +
+ + + + Make sure to copy your API key now. You won't be able to see it again! + + + +
+ +
+ e.currentTarget.select()} + /> + +
+
+ +
+

Use this key to authenticate with the Maple API:

+ + Authorization: Bearer {"{your-api-key}"} + +
+
+ + + + + + )} +
+
+ ); +} diff --git a/frontend/src/components/apikeys/ProxyConfigSection.tsx b/frontend/src/components/apikeys/ProxyConfigSection.tsx new file mode 100644 index 000000000..7b91b61e0 --- /dev/null +++ b/frontend/src/components/apikeys/ProxyConfigSection.tsx @@ -0,0 +1,375 @@ +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Play, Square, Loader2, AlertCircle, CheckCircle, Server, Copy, Check } from "lucide-react"; +import { proxyService, ProxyConfig, ProxyStatus } from "@/services/proxyService"; + +interface ProxyConfigSectionProps { + apiKeys: Array<{ name: string; created_at: string }>; + onRequestNewApiKey: (name: string) => Promise; +} + +export function ProxyConfigSection({ apiKeys, onRequestNewApiKey }: ProxyConfigSectionProps) { + const [proxyStatus, setProxyStatus] = useState(null); + const [config, setConfig] = useState({ + host: "127.0.0.1", + port: 8080, + api_key: "", + enabled: false, + enable_cors: true, + auto_start: false + }); + const [isLoading, setIsLoading] = useState(false); + const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); + const [copied, setCopied] = useState(false); + const [isDesktop, setIsDesktop] = useState(false); + + // Check if we're in Tauri desktop environment + useEffect(() => { + proxyService.isTauriDesktop().then(setIsDesktop); + }, []); + + useEffect(() => { + if (!isDesktop) return; + + // Load saved config and status on mount + loadProxyState(); + }, [isDesktop]); + + const loadProxyState = async () => { + try { + const [savedConfig, status] = await Promise.all([ + proxyService.loadProxyConfig(), + proxyService.getProxyStatus() + ]); + + setConfig(savedConfig); + setProxyStatus(status); + } catch (error) { + console.error("Failed to load proxy state:", error); + } + }; + + const handleStartProxy = async () => { + setIsLoading(true); + + try { + // If no API key is set, auto-generate one + let apiKey = config.api_key; + if (!apiKey) { + const date = new Date().toISOString().split("T")[0].replace(/-/g, ""); + const keyName = `maple-desktop-${date}`; + + // Check if a key with this name already exists + const existingKey = apiKeys.find((k) => k.name === keyName); + if (existingKey) { + // Use the existing key (we don't have access to the actual key value) + setMessage({ + type: "error", + text: "Please select an existing API key or create a new one" + }); + setIsLoading(false); + return; + } + + // Request a new API key + try { + apiKey = await onRequestNewApiKey(keyName); + setConfig((prev) => ({ ...prev, api_key: apiKey })); + } catch { + setMessage({ + type: "error", + text: "Failed to create API key. Please create an API key manually first" + }); + setIsLoading(false); + return; + } + } + + // Validate port range (1-65535) + const port = Number(config.port); + if (!Number.isInteger(port) || port < 1 || port > 65535) { + setMessage({ + type: "error", + text: `Invalid port: ${config.port}. Port must be between 1 and 65535.` + }); + setIsLoading(false); + return; + } + + // Get the backend URL from environment + const backendUrl = import.meta.env.VITE_OPEN_SECRET_API_URL || "https://enclave.trymaple.ai"; + + const updatedConfig = { + ...config, + api_key: apiKey, + enabled: true, + backend_url: backendUrl, + auto_start: config.auto_start // Preserve auto_start setting + }; + const status = await proxyService.startProxy(updatedConfig); + setProxyStatus(status); + setConfig(updatedConfig); + + setMessage({ + type: "success", + text: `Proxy is now running on ${config.host}:${config.port}` + }); + } catch (error) { + setMessage({ type: "error", text: `Failed to start proxy: ${error}` }); + } finally { + setIsLoading(false); + } + }; + + const handleStopProxy = async () => { + setIsLoading(true); + + try { + const status = await proxyService.stopProxy(); + setProxyStatus(status); + setConfig((prev) => ({ ...prev, enabled: false })); + + setMessage({ type: "success", text: "The proxy server has been stopped" }); + } catch (error) { + setMessage({ type: "error", text: `Failed to stop proxy: ${error}` }); + } finally { + setIsLoading(false); + } + }; + + const handleConfigChange = (field: keyof ProxyConfig, value: string | number | boolean) => { + setConfig((prev) => ({ ...prev, [field]: value })); + }; + + const copyProxyUrl = () => { + const url = `http://${config.host}:${config.port}/v1`; + navigator.clipboard.writeText(url); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + if (!isDesktop) { + return null; // Don't show proxy config on non-desktop platforms + } + + const isRunning = proxyStatus?.running || false; + + return ( + <> + {/* Show message alerts */} + {message && ( + +
+ {message.type === "error" ? ( + + ) : ( + + )} + {message.text} +
+
+ )} + +
+

+ + Local OpenAI Proxy +

+
+ {isRunning ? "Running" : "Stopped"} +
+
+
+ + + {/* Configuration */} +
+
+ + handleConfigChange("host", e.target.value)} + placeholder="127.0.0.1" + disabled={isRunning} + className="h-8 text-sm" + /> +
+
+ +
+ handleConfigChange("port", parseInt(e.target.value) || 8080)} + placeholder="8080" + disabled={isRunning} + className="h-8 text-sm" + /> +
+
+
+ + {/* API Key Info */} + {config.api_key && ( +
+ +

API key configured

+
+ )} + + {/* Proxy URL */} + {isRunning && ( +
+ +
+ + +
+

+ Use this URL as your OpenAI base URL in any client +

+
+ )} + + {/* Control Buttons */} +
+ {!isRunning ? ( + + ) : ( + + )} +
+ + {/* CORS Toggle */} +
+ + handleConfigChange("enable_cors", e.target.checked)} + disabled={isRunning} + className="h-4 w-4" + /> +
+ + {/* Auto-start Toggle */} +
+
+ +

+ Proxy will start automatically on app launch if configured +

+
+ { + const newConfig = { ...config, auto_start: e.target.checked }; + setConfig(newConfig); + // Save immediately when toggling auto-start + try { + await proxyService.saveProxySettings(newConfig); + setMessage({ + type: "success", + text: e.target.checked ? "Auto-start enabled" : "Auto-start disabled" + }); + } catch { + setMessage({ type: "error", text: "Failed to save auto-start setting" }); + } + }} + disabled={!config.api_key} // Only enable if we have an API key + className="h-4 w-4" + /> +
+
+ + {/* Usage Examples */} + {isRunning && ( +
+ +
+ Python Example: +
+                {`from openai import OpenAI
+client = OpenAI(
+  base_url="http://${config.host}:${config.port}/v1",
+  api_key="anything"  # API key handled by proxy
+)
+
+response = client.chat.completions.create(
+  model="llama3-3-70b",
+  messages=[{"role": "user", "content": "Hello!"}],
+  stream=True
+)
+
+for chunk in response:
+    print(chunk.choices[0].delta.content or "", end="")`}
+              
+
+
+ + +
+ cURL Example: +
+                {`curl -N http://${config.host}:${config.port}/v1/chat/completions \\
+  -H "Content-Type: application/json" \\
+  -d '{
+    "model": "llama3-3-70b",
+    "messages": [{"role": "user", "content": "Hello!"}],
+    "stream": true
+  }'`}
+              
+
+
+
+ )} + + ); +} diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx new file mode 100644 index 000000000..91b60ef99 --- /dev/null +++ b/frontend/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@/utils/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/frontend/src/config/pricingConfig.tsx b/frontend/src/config/pricingConfig.tsx index 7b486e6e0..d01bc9176 100644 --- a/frontend/src/config/pricingConfig.tsx +++ b/frontend/src/config/pricingConfig.tsx @@ -64,7 +64,8 @@ export const PRICING_PLANS: PricingPlan[] = [ text: "Qwen3 Coder 480B", included: false, icon: - } + }, + { text: "API Access", included: false, icon: } ], ctaText: "Start Free" }, @@ -110,7 +111,8 @@ export const PRICING_PLANS: PricingPlan[] = [ included: false, icon: }, - { text: "Image Upload", included: false, icon: } + { text: "Image Upload", included: false, icon: }, + { text: "API Access", included: false, icon: } ], ctaText: "Start Chatting" }, @@ -155,6 +157,11 @@ export const PRICING_PLANS: PricingPlan[] = [ text: "Qwen3 Coder 480B", included: true, icon: + }, + { + text: "API Access", + included: true, + icon: } ], ctaText: "Start Chatting", @@ -211,6 +218,11 @@ export const PRICING_PLANS: PricingPlan[] = [ text: "Qwen3 Coder 480B", included: true, icon: + }, + { + text: "API Access", + included: true, + icon: } ], ctaText: "Start Chatting" @@ -276,6 +288,11 @@ export const PRICING_PLANS: PricingPlan[] = [ text: "Qwen3 Coder 480B", included: true, icon: + }, + { + text: "API Access", + included: true, + icon: } ], ctaText: "Start Chatting" diff --git a/frontend/src/contexts/NotificationContext.tsx b/frontend/src/contexts/NotificationContext.tsx new file mode 100644 index 000000000..5914ae8d5 --- /dev/null +++ b/frontend/src/contexts/NotificationContext.tsx @@ -0,0 +1,41 @@ +import React, { createContext, useContext, useState, useCallback } from "react"; +import { GlobalNotification, type Notification } from "@/components/GlobalNotification"; + +interface NotificationContextType { + showNotification: (notification: Omit) => void; + clearNotification: () => void; +} + +const NotificationContext = createContext(undefined); + +export function NotificationProvider({ children }: { children: React.ReactNode }) { + const [notification, setNotification] = useState(null); + + const showNotification = useCallback((notif: Omit) => { + const id = Date.now().toString(); + setNotification({ + ...notif, + id, + duration: notif.duration ?? 5000 // Default 5 seconds + }); + }, []); + + const clearNotification = useCallback(() => { + setNotification(null); + }, []); + + return ( + + {children} + + + ); +} + +export function useNotification() { + const context = useContext(NotificationContext); + if (!context) { + throw new Error("useNotification must be used within NotificationProvider"); + } + return context; +} diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index f902daebe..631ee0801 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -14,7 +14,8 @@ import { TopNav } from "@/components/TopNav"; import { Marketing } from "@/components/Marketing"; import { SimplifiedFooter } from "@/components/SimplifiedFooter"; import { TeamManagementDialog } from "@/components/team/TeamManagementDialog"; -import { useQuery } from "@tanstack/react-query"; +import { ApiKeyManagementDialog } from "@/components/apikeys/ApiKeyManagementDialog"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { getBillingService } from "@/billing/billingService"; import type { TeamStatus } from "@/types/team"; @@ -34,13 +35,16 @@ type IndexSearchOptions = { login?: string; next?: string; team_setup?: boolean; + credits_success?: boolean; }; function validateSearch(search: Record): IndexSearchOptions { return { login: search?.login === "true" ? "true" : undefined, next: search.next ? (search.next as string) : undefined, - team_setup: search?.team_setup === true || search?.team_setup === "true" ? true : undefined + team_setup: search?.team_setup === true || search?.team_setup === "true" ? true : undefined, + credits_success: + search?.credits_success === true || search?.credits_success === "true" ? true : undefined }; } @@ -52,8 +56,9 @@ export const Route = createFileRoute("/")({ function Index() { const navigate = useNavigate(); const localState = useLocalState(); + const queryClient = useQueryClient(); - const { login, next, team_setup } = Route.useSearch(); + const { login, next, team_setup, credits_success } = Route.useSearch(); const os = useOpenSecret(); @@ -68,6 +73,8 @@ function Index() { const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [teamDialogOpen, setTeamDialogOpen] = useState(false); + const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false); + const [showCreditSuccess, setShowCreditSuccess] = useState(false); // Fetch team status for the dialog const { data: teamStatus } = useQuery({ @@ -88,6 +95,21 @@ function Index() { } }, [team_setup, os.auth.user, teamStatus, navigate]); + // Handle credits_success - open API key dialog and refresh balance + useEffect(() => { + if (credits_success && os.auth.user) { + setApiKeyDialogOpen(true); + setShowCreditSuccess(true); + // Refresh the credit balance + queryClient.invalidateQueries({ queryKey: ["apiCreditBalance"] }); + // Clear the query param to prevent re-opening on refresh + navigate({ to: "/", replace: true }); + // Clear success message after 5 seconds + const timer = setTimeout(() => setShowCreditSuccess(false), 5000); + return () => clearTimeout(timer); + } + }, [credits_success, os.auth.user, navigate, queryClient]); + const toggleSidebar = useCallback(() => setIsSidebarOpen((prev) => !prev), []); async function handleSubmit( @@ -194,6 +216,15 @@ function Index() { teamStatus={teamStatus} /> )} + + {/* API Key Management Dialog */} + {os.auth.user && ( + + )}
); } diff --git a/frontend/src/services/proxyService.ts b/frontend/src/services/proxyService.ts new file mode 100644 index 000000000..c5777b9b2 --- /dev/null +++ b/frontend/src/services/proxyService.ts @@ -0,0 +1,109 @@ +import { invoke } from "@tauri-apps/api/core"; + +export interface ProxyConfig { + host: string; + port: number; + api_key: string; + enabled: boolean; + enable_cors?: boolean; + backend_url?: string; + auto_start?: boolean; +} + +export interface ProxyStatus { + running: boolean; + config: ProxyConfig; + error?: string; +} + +class ProxyService { + private validatePort(port: number): void { + if (!Number.isInteger(port) || port < 0 || port > 65535) { + throw new Error(`Port must be a valid u16 integer (0-65535), got: ${port}`); + } + } + + async startProxy(config: ProxyConfig): Promise { + try { + this.validatePort(config.port); + return await invoke("start_proxy", { config }); + } catch (error) { + console.error("Failed to start proxy:", error); + throw error; + } + } + + async stopProxy(): Promise { + try { + return await invoke("stop_proxy"); + } catch (error) { + console.error("Failed to stop proxy:", error); + throw error; + } + } + + async getProxyStatus(): Promise { + try { + return await invoke("get_proxy_status"); + } catch (error) { + console.error("Failed to get proxy status:", error); + throw error; + } + } + + async loadProxyConfig(): Promise { + try { + return await invoke("load_proxy_config"); + } catch (error) { + console.error("Failed to load proxy config:", error); + // Return default config if loading fails + return { + host: "127.0.0.1", + port: 8080, + api_key: "", + enabled: false + }; + } + } + + async saveProxySettings(config: ProxyConfig): Promise { + try { + this.validatePort(config.port); + await invoke("save_proxy_settings", { config }); + } catch (error) { + console.error("Failed to save proxy settings:", error); + throw error; + } + } + + async testProxyPort(host: string, port: number): Promise { + try { + this.validatePort(port); + return await invoke("test_proxy_port", { host, port }); + } catch (error) { + console.error("Failed to test proxy port:", error); + throw error; + } + } + + // Helper to check if we're in Tauri desktop environment + async isTauriDesktop(): Promise { + try { + const { isTauri } = await import("@tauri-apps/api/core"); + const inTauri = await isTauri(); + + if (!inTauri) return false; + + // Check if it's desktop (not mobile) + const { type } = await import("@tauri-apps/plugin-os"); + const platform = await type(); + + // Desktop platforms + return platform === "macos" || platform === "windows" || platform === "linux"; + } catch { + return false; + } + } +} + +export const proxyService = new ProxyService();