Merge branch 'main' of github.com:hoppscotch/hoppscotch
@@ -6,13 +6,18 @@ DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch
|
||||
JWT_SECRET="secret1233"
|
||||
TOKEN_SALT_COMPLEXITY=10
|
||||
MAGIC_LINK_TOKEN_VALIDITY= 3
|
||||
REFRESH_TOKEN_VALIDITY="604800000" # Default validity is 7 days (604800000 ms) in ms
|
||||
ACCESS_TOKEN_VALIDITY="86400000" # Default validity is 1 day (86400000 ms) in ms
|
||||
# Default validity is 7 days (604800000 ms) in ms
|
||||
REFRESH_TOKEN_VALIDITY="604800000"
|
||||
# Default validity is 1 day (86400000 ms) in ms
|
||||
ACCESS_TOKEN_VALIDITY="86400000"
|
||||
SESSION_SECRET='add some secret here'
|
||||
# Reccomended to be true, set to false if you are using http
|
||||
# Note: Some auth providers may not support http requests
|
||||
ALLOW_SECURE_COOKIES=true
|
||||
|
||||
# Sensitive Data Encryption Key while storing in Database (32 character)
|
||||
DATA_ENCRYPTION_KEY="data encryption key with 32 char"
|
||||
|
||||
# Hoppscotch App Domain Config
|
||||
REDIRECT_URL="http://localhost:3000"
|
||||
WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
||||
|
||||
249
.github/workflows/build-hoppscotch-agent.yml
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Tag of the version to build
|
||||
required: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, ubuntu-22.04, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- name: Checkout hoppscotch/hoppscotch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: hoppscotch/hoppscotch
|
||||
ref: main
|
||||
token: ${{ secrets.CHECKOUT_GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install Rust targets (Mac)
|
||||
if: matrix.platform == 'macos-latest'
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
|
||||
- name: Install additional tools (Linux)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
# Install Tauri CLI (binary)
|
||||
curl -LO "https://github.com/tauri-apps/tauri/releases/download/tauri-cli-v2.0.1/cargo-tauri-x86_64-unknown-linux-gnu.tgz"
|
||||
tar -xzf cargo-tauri-x86_64-unknown-linux-gnu.tgz
|
||||
chmod +x cargo-tauri
|
||||
sudo mv cargo-tauri /usr/local/bin/tauri
|
||||
|
||||
# Install Trunk (binary)
|
||||
curl -LO "https://github.com/thedodd/trunk/releases/download/v0.17.5/trunk-x86_64-unknown-linux-gnu.tar.gz"
|
||||
tar -xzf trunk-x86_64-unknown-linux-gnu.tar.gz
|
||||
chmod +x trunk
|
||||
sudo mv trunk /usr/local/bin/
|
||||
|
||||
- name: Install additional tools (Mac)
|
||||
if: matrix.platform == 'macos-latest'
|
||||
run: |
|
||||
# Install Tauri CLI (binary)
|
||||
mkdir __dist/
|
||||
cd __dist/
|
||||
curl -LO "https://github.com/tauri-apps/tauri/releases/download/tauri-cli-v2.0.1/cargo-tauri-aarch64-apple-darwin.zip"
|
||||
unzip cargo-tauri-aarch64-apple-darwin.zip
|
||||
chmod +x cargo-tauri
|
||||
sudo mv cargo-tauri /usr/local/bin/tauri
|
||||
|
||||
- name: Install system dependencies (Ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev
|
||||
|
||||
- name: Setting up Windows Environment and injecting before bundle command (Windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
shell: bash
|
||||
env:
|
||||
WINDOWS_SIGN_COMMAND: trusted-signing-cli -e ${{ secrets.AZURE_ENDPOINT }} -a ${{ secrets.AZURE_CODE_SIGNING_NAME }} -c ${{ secrets.AZURE_CERT_PROFILE_NAME }} %1
|
||||
run: |
|
||||
cd packages/hoppscotch-agent
|
||||
# Inject signing command into main conf.
|
||||
cat './src-tauri/tauri.conf.json' | jq '.bundle .windows += { "signCommand": env.WINDOWS_SIGN_COMMAND}' > './src-tauri/temp.json' && mv './src-tauri/temp.json' './src-tauri/tauri.conf.json'
|
||||
# Inject signing command into portable conf.
|
||||
cat './src-tauri/tauri.portable.conf.json' | jq '.bundle .windows += { "signCommand": env.WINDOWS_SIGN_COMMAND}' > './src-tauri/temp_portable.json' && mv './src-tauri/temp_portable.json' './src-tauri/tauri.portable.conf.json'
|
||||
cargo install trusted-signing-cli@0.3.0
|
||||
|
||||
- name: Set platform-specific variables
|
||||
run: |
|
||||
if [ "${{ matrix.platform }}" = "ubuntu-22.04" ]; then
|
||||
echo "target_arch=$(rustc -Vv | grep host | awk '{print $2}')" >> $GITHUB_ENV
|
||||
echo "target_ext=" >> $GITHUB_ENV
|
||||
echo "target_os_name=linux" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
echo "target_arch=x86_64-pc-windows-msvc" >> $GITHUB_ENV
|
||||
echo "target_ext=.exe" >> $GITHUB_ENV
|
||||
echo "target_os_name=win" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.platform }}" = "macos-latest" ]; then
|
||||
echo "target_os_name=mac" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Setup macOS code signing
|
||||
if: matrix.platform == 'macos-latest'
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
|
||||
security import certificate.p12 -k build.keychain -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
cd packages/hoppscotch-agent
|
||||
pnpm install --filter hoppscotch-agent
|
||||
|
||||
- name: Build Tauri app (Linux)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.AGENT_TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.AGENT_TAURI_SIGNING_PASSWORD }}
|
||||
run: |
|
||||
cd packages/hoppscotch-agent
|
||||
pnpm tauri build --verbose -b deb -b appimage -b updater
|
||||
|
||||
- name: Build Tauri app (Mac)
|
||||
if: matrix.platform == 'macos-latest'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.AGENT_TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.AGENT_TAURI_SIGNING_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
run: |
|
||||
cd packages/hoppscotch-agent
|
||||
pnpm tauri build --verbose --target x86_64-apple-darwin
|
||||
pnpm tauri build --verbose --target aarch64-apple-darwin
|
||||
|
||||
- name: Build Tauri app (Windows)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
shell: powershell
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.AGENT_TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.AGENT_TAURI_SIGNING_PASSWORD }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||
run: |
|
||||
cd packages/hoppscotch-agent
|
||||
# Build the portable version first and move it.
|
||||
# This way the next build will regenerate `hoppscotch-agent.exe`.
|
||||
pnpm tauri build --verbose --config src-tauri/tauri.portable.conf.json -- --no-default-features --features portable
|
||||
Rename-Item -Path "src-tauri/target/release/hoppscotch-agent.exe" -NewName "hoppscotch-agent-portable.exe"
|
||||
|
||||
# Build the installer version.
|
||||
pnpm tauri build --verbose -b msi -b updater
|
||||
|
||||
- name: Zip portable executable (Windows)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
shell: powershell
|
||||
run: |
|
||||
Compress-Archive -Path "packages/hoppscotch-agent/src-tauri/target/release/hoppscotch-agent-portable.exe" -DestinationPath "packages/hoppscotch-agent/src-tauri/target/release/Hoppscotch_Agent_win_x64_portable.zip"
|
||||
|
||||
- name: Prepare artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir artifacts
|
||||
mkdir artifacts/sigs
|
||||
if [ "${{ matrix.platform }}" = "ubuntu-22.04" ]; then
|
||||
mv packages/hoppscotch-agent/src-tauri/target/release/bundle/appimage/*.AppImage artifacts/Hoppscotch_Agent_linux_x64.AppImage
|
||||
mv packages/hoppscotch-agent/src-tauri/target/release/bundle/appimage/*.AppImage.sig artifacts/sigs/Hoppscotch_Agent_linux_x64.AppImage.sig
|
||||
mv packages/hoppscotch-agent/src-tauri/target/release/bundle/deb/*.deb artifacts/Hoppscotch_Agent_linux_x64.deb
|
||||
elif [ "${{ matrix.platform }}" = "macos-latest" ]; then
|
||||
mv packages/hoppscotch-agent/src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/*_x64.dmg artifacts/Hoppscotch_Agent_mac_x64.dmg
|
||||
mv packages/hoppscotch-agent/src-tauri/target/x86_64-apple-darwin/release/bundle/macos/*.app.tar.gz artifacts/Hoppscotch_Agent_mac_update_x64.tar.gz
|
||||
mv packages/hoppscotch-agent/src-tauri/target/x86_64-apple-darwin/release/bundle/macos/*.app.tar.gz.sig artifacts/sigs/Hoppscotch_Agent_mac_update_x64.tar.gz.sig
|
||||
mv packages/hoppscotch-agent/src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/*_aarch64.dmg artifacts/Hoppscotch_Agent_mac_aarch64.dmg
|
||||
mv packages/hoppscotch-agent/src-tauri/target/aarch64-apple-darwin/release/bundle/macos/*.app.tar.gz artifacts/Hoppscotch_Agent_mac_update_aarch64.tar.gz
|
||||
mv packages/hoppscotch-agent/src-tauri/target/aarch64-apple-darwin/release/bundle/macos/*.app.tar.gz.sig artifacts/sigs/Hoppscotch_Agent_mac_update_aarch64.tar.gz.sig
|
||||
elif [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
mv packages/hoppscotch-agent/src-tauri/target/release/bundle/msi/*_x64_en-US.msi artifacts/Hoppscotch_Agent_win_x64.msi
|
||||
mv packages/hoppscotch-agent/src-tauri/target/release/bundle/msi/*_x64_en-US.msi.sig artifacts/sigs/Hoppscotch_Agent_win_x64.msi.sig
|
||||
mv packages/hoppscotch-agent/src-tauri/target/release/Hoppscotch_Agent_win_x64_portable.zip artifacts/Hoppscotch_Agent_win_x64_portable.zip
|
||||
fi
|
||||
|
||||
- name: Generate checksums (Linux)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
cd artifacts
|
||||
mkdir shas
|
||||
for file in *; do
|
||||
if [ -f "$file" ]; then
|
||||
sha256sum "$file" > "shas/${file}.sha256"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Generate checksums (Mac)
|
||||
if: matrix.platform == 'macos-latest'
|
||||
run: |
|
||||
cd artifacts
|
||||
mkdir shas
|
||||
for file in *; do
|
||||
if [ -f "$file" ]; then
|
||||
shasum -a 256 "$file" > "shas/${file}.sha256"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Hoppscotch_Agent-${{ matrix.platform }}
|
||||
path: artifacts/*
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
2
.github/workflows/release-push-docker.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup environment
|
||||
run: cp .env.example .env
|
||||
|
||||
4
.github/workflows/tests.yml
vendored
@@ -2,9 +2,9 @@ name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, staging, "release/**"]
|
||||
branches: [main, next, patch]
|
||||
pull_request:
|
||||
branches: [main, staging, "release/**"]
|
||||
branches: [main, next, patch]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
4
.github/workflows/ui.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup environment
|
||||
run: mv .env.example .env
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
run_install: true
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
|
||||
1
.gitignore
vendored
@@ -19,6 +19,7 @@ pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
*.env
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install commitlint --edit ""
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run pre-commit
|
||||
npm run pre-commit
|
||||
@@ -1,3 +1,8 @@
|
||||
{
|
||||
admin off
|
||||
persist_config off
|
||||
}
|
||||
|
||||
:3000 {
|
||||
try_files {path} /
|
||||
root * /site/selfhost-web
|
||||
@@ -13,7 +18,3 @@
|
||||
:3170 {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
|
||||
:80 {
|
||||
respond 404
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
:3000 {
|
||||
respond 404
|
||||
{
|
||||
admin off
|
||||
persist_config off
|
||||
}
|
||||
|
||||
:3100 {
|
||||
respond 404
|
||||
}
|
||||
|
||||
:3170 {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
|
||||
:80 {
|
||||
:{$HOPP_AIO_ALTERNATE_PORT:80} {
|
||||
# Serve the `selfhost-web` SPA by default
|
||||
root * /site/selfhost-web
|
||||
file_server
|
||||
|
||||
@@ -51,7 +51,7 @@ fs.rmSync("build.env")
|
||||
|
||||
const caddyFileName = process.env.ENABLE_SUBPATH_BASED_ACCESS === 'true' ? 'aio-subpath-access.Caddyfile' : 'aio-multiport-setup.Caddyfile'
|
||||
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", `/etc/caddy/${caddyFileName}`, "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
|
||||
const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server")
|
||||
const backendProcess = runChildProcessWithPrefix("node", ["/dist/backend/dist/main.js"], "Backend Server")
|
||||
|
||||
caddyProcess.on("exit", (code) => {
|
||||
console.log(`Exiting process because Caddy Server exited with code ${code}`)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# THIS IS NOT TO BE USED FOR PERSONAL DEPLOYMENTS!
|
||||
# Internal Docker Compose Image used for internal testing deployments
|
||||
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
hoppscotch-db:
|
||||
image: postgres:15
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# To make it easier to self-host, we have a preset docker compose config that also
|
||||
# has a container with a Postgres instance running.
|
||||
# You can tweak around this file to match your instances
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
# This service runs the backend app in the port 3170
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
|
||||
curlCheck() {
|
||||
if ! curl -s --head "$1" | head -n 1 | grep -q "HTTP/1.[01] [23].."; then
|
||||
|
||||
26
package.json
@@ -7,7 +7,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"prepare": "husky install",
|
||||
"prepare": "husky",
|
||||
"dev": "pnpm -r do-dev",
|
||||
"gen-gql": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' pnpm -r generate-gql-sdl",
|
||||
"generate": "pnpm -r do-build-prod",
|
||||
@@ -23,18 +23,28 @@
|
||||
"./packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "16.3.0",
|
||||
"@commitlint/config-conventional": "16.2.4",
|
||||
"@hoppscotch/ui": "0.2.0",
|
||||
"@types/node": "17.0.27",
|
||||
"@commitlint/cli": "19.5.0",
|
||||
"@commitlint/config-conventional": "19.5.0",
|
||||
"@hoppscotch/ui": "0.2.2",
|
||||
"@types/node": "22.7.6",
|
||||
"cross-env": "7.0.3",
|
||||
"http-server": "14.1.1",
|
||||
"husky": "7.0.4",
|
||||
"lint-staged": "12.4.0"
|
||||
"husky": "9.1.6",
|
||||
"lint-staged": "15.2.10"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vue": "3.3.9"
|
||||
"cookie": "0.7.2",
|
||||
"vue": "3.5.12",
|
||||
"@nestjs-modules/mailer>mjml": "5.0.0-alpha.4",
|
||||
"subscriptions-transport-ws>ws": "7.5.10",
|
||||
"braces": "3.0.3",
|
||||
"send": "0.19.0",
|
||||
"pug": "3.0.3",
|
||||
"body-parser": "1.20.3",
|
||||
"path-to-regexp@3.2.0": "3.3.0",
|
||||
"micromatch@<4.0.8": "4.0.8",
|
||||
"dset@3.1.3": "3.1.4"
|
||||
},
|
||||
"packageExtensions": {
|
||||
"@hoppscotch/httpsnippet": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"author": "Hoppscotch (support@hoppscotch.io)",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prepare": "rollup -c"
|
||||
"prepare": "rollup -c && tsc --emitDeclarationOnly --declaration"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
@@ -25,8 +25,7 @@
|
||||
"@lezer/generator": "1.5.1",
|
||||
"mocha": "9.2.2",
|
||||
"rollup": "3.29.4",
|
||||
"rollup-plugin-dts": "6.0.2",
|
||||
"rollup-plugin-ts": "3.4.5",
|
||||
"@rollup/plugin-typescript": "12.1.1",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import typescript from "rollup-plugin-ts"
|
||||
import typescript from "@rollup/plugin-typescript"
|
||||
import { lezer } from "@lezer/generator/rollup"
|
||||
|
||||
export default {
|
||||
@@ -8,5 +8,10 @@ export default {
|
||||
{ file: "dist/index.cjs", format: "cjs" },
|
||||
{ dir: "./dist", format: "es" },
|
||||
],
|
||||
plugins: [lezer(), typescript()],
|
||||
plugins: [
|
||||
lezer(),
|
||||
typescript({
|
||||
tsconfig: "./tsconfig.json"
|
||||
})
|
||||
],
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"module": "es2020",
|
||||
"newLine": "lf",
|
||||
"declaration": true,
|
||||
"declarationDir": "./dist",
|
||||
"moduleResolution": "node",
|
||||
"allowJs": true
|
||||
},
|
||||
|
||||
3
packages/hoppscotch-agent/.envrc
Normal file
@@ -0,0 +1,3 @@
|
||||
source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0="
|
||||
|
||||
use devenv
|
||||
33
packages/hoppscotch-agent/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
# Devenv
|
||||
.devenv*
|
||||
devenv.local.nix
|
||||
|
||||
# direnv
|
||||
.direnv
|
||||
|
||||
# pre-commit
|
||||
.pre-commit-config.yaml
|
||||
16
packages/hoppscotch-agent/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Tauri + Vue + TypeScript
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
|
||||
|
||||
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
|
||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
||||
153
packages/hoppscotch-agent/devenv.lock
Normal file
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"nodes": {
|
||||
"devenv": {
|
||||
"locked": {
|
||||
"dir": "src/modules",
|
||||
"lastModified": 1729277673,
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "3c3ab087b53d3e4699a43018ac71b5e1091ed73d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"dir": "src/modules",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1728973961,
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "d6a9ff4d1e60c347a23bc96ccdb058d37a810541",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"pre-commit-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1729265718,
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ccc0c2126893dd20963580b6478d1a10a4512185",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1729181673,
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4eb33fe664af7b41a4c446f87d20c9a0a6321fa3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pre-commit-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": "nixpkgs-stable"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1729104314,
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"rev": "3c3e88f0f544d6bb54329832616af7eb971b6be6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"fenix": "fenix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"pre-commit-hooks": "pre-commit-hooks"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1729259624,
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "3ddfb0da474bdf243a655a5d785de9b59c7f1397",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
116
packages/hoppscotch-agent/devenv.nix
Normal file
@@ -0,0 +1,116 @@
|
||||
{ pkgs, lib, config, inputs, ... }:
|
||||
|
||||
let
|
||||
rosettaPkgs =
|
||||
if pkgs.stdenv.isDarwin && pkgs.stdenv.isAarch64
|
||||
then pkgs.pkgsx86_64Darwin
|
||||
else pkgs;
|
||||
|
||||
darwinPackages = with pkgs; [
|
||||
darwin.apple_sdk.frameworks.Security
|
||||
darwin.apple_sdk.frameworks.CoreServices
|
||||
darwin.apple_sdk.frameworks.CoreFoundation
|
||||
darwin.apple_sdk.frameworks.Foundation
|
||||
darwin.apple_sdk.frameworks.AppKit
|
||||
darwin.apple_sdk.frameworks.WebKit
|
||||
];
|
||||
|
||||
linuxPackages = with pkgs; [
|
||||
libsoup_3
|
||||
webkitgtk_4_1
|
||||
librsvg
|
||||
libappindicator
|
||||
libayatana-appindicator
|
||||
];
|
||||
|
||||
in {
|
||||
# https://devenv.sh/packages/
|
||||
packages = with pkgs; [
|
||||
git
|
||||
postgresql_16
|
||||
# FE and Node stuff
|
||||
nodejs_22
|
||||
nodePackages_latest.typescript-language-server
|
||||
nodePackages_latest.vls
|
||||
nodePackages_latest.prisma
|
||||
prisma-engines
|
||||
# Cargo
|
||||
cargo-edit
|
||||
] ++ lib.optionals pkgs.stdenv.isDarwin darwinPackages
|
||||
++ lib.optionals pkgs.stdenv.isLinux linuxPackages;
|
||||
|
||||
# https://devenv.sh/basics/
|
||||
env = {
|
||||
APP_GREET = "Hoppscotch";
|
||||
} // lib.optionalAttrs pkgs.stdenv.isLinux {
|
||||
# NOTE: Setting these `PRISMA_*` environment variable fixes
|
||||
# Error: Failed to fetch sha256 checksum at https://binaries.prisma.sh/all_commits/<hash>/linux-nixos/libquery_engine.so.node.gz.sha256 - 404 Not Found
|
||||
# See: https://github.com/prisma/prisma/discussions/3120
|
||||
PRISMA_QUERY_ENGINE_LIBRARY = "${pkgs.prisma-engines}/lib/libquery_engine.node";
|
||||
PRISMA_QUERY_ENGINE_BINARY = "${pkgs.prisma-engines}/bin/query-engine";
|
||||
PRISMA_SCHEMA_ENGINE_BINARY = "${pkgs.prisma-engines}/bin/schema-engine";
|
||||
|
||||
LD_LIBRARY_PATH = lib.makeLibraryPath [
|
||||
pkgs.libappindicator
|
||||
pkgs.libayatana-appindicator
|
||||
];
|
||||
} // lib.optionalAttrs pkgs.stdenv.isDarwin {
|
||||
# Place to put macOS-specific environment variables
|
||||
};
|
||||
|
||||
# https://devenv.sh/scripts/
|
||||
scripts = {
|
||||
hello.exec = "echo hello from $APP_GREET";
|
||||
e.exec = "emacs";
|
||||
};
|
||||
|
||||
enterShell = ''
|
||||
git --version
|
||||
${lib.optionalString pkgs.stdenv.isDarwin ''
|
||||
# Place to put macOS-specific shell initialization
|
||||
''}
|
||||
${lib.optionalString pkgs.stdenv.isLinux ''
|
||||
# Place to put Linux-specific shell initialization
|
||||
''}
|
||||
'';
|
||||
|
||||
# https://devenv.sh/tests/
|
||||
enterTest = ''
|
||||
echo "Running tests"
|
||||
'';
|
||||
|
||||
# https://devenv.sh/integrations/dotenv/
|
||||
dotenv.enable = true;
|
||||
|
||||
# https://devenv.sh/languages/
|
||||
languages = {
|
||||
typescript.enable = true;
|
||||
javascript = {
|
||||
enable = true;
|
||||
pnpm.enable = true;
|
||||
npm.enable = true;
|
||||
};
|
||||
rust = {
|
||||
enable = true;
|
||||
channel = "nightly";
|
||||
components = [
|
||||
"rustc"
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rustfmt"
|
||||
"rust-analyzer"
|
||||
"llvm-tools-preview"
|
||||
"rust-src"
|
||||
"rustc-codegen-cranelift-preview"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# https://devenv.sh/pre-commit-hooks/
|
||||
# pre-commit.hooks.shellcheck.enable = true;
|
||||
|
||||
# https://devenv.sh/processes/
|
||||
# processes.ping.exec = "ping example.com";
|
||||
|
||||
# See full reference at https://devenv.sh/reference/options/
|
||||
}
|
||||
23
packages/hoppscotch-agent/devenv.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
|
||||
inputs:
|
||||
# For NodeJS-22 and above
|
||||
nixpkgs:
|
||||
url: github:NixOS/nixpkgs/nixpkgs-unstable
|
||||
# nixpkgs:
|
||||
# url: github:cachix/devenv-nixpkgs/rolling
|
||||
fenix:
|
||||
url: github:nix-community/fenix
|
||||
inputs:
|
||||
nixpkgs:
|
||||
follows: nixpkgs
|
||||
|
||||
# If you're using non-OSS software, you can set allowUnfree to true.
|
||||
allowUnfree: true
|
||||
|
||||
# If you're willing to use a package that's vulnerable
|
||||
# permittedInsecurePackages:
|
||||
# - "openssl-1.1.1w"
|
||||
|
||||
# If you have more than one devenv you can merge them
|
||||
#imports:
|
||||
# - ./backend
|
||||
13
packages/hoppscotch-agent/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hoppscotch Agent</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
34
packages/hoppscotch-agent/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "hoppscotch-agent",
|
||||
"private": true,
|
||||
"version": "0.1.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hoppscotch/ui": "^0.2.1",
|
||||
"@tauri-apps/api": "^2.0.2",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"axios": "^1.7.7",
|
||||
"fp-ts": "^2.16.9",
|
||||
"vue": "3.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.8",
|
||||
"@tauri-apps/cli": "^2.0.3",
|
||||
"@types/node": "^22.7.5",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.6.3",
|
||||
"unplugin-icons": "^0.19.3",
|
||||
"vite": "^5.4.8",
|
||||
"vue-tsc": "^2.1.6"
|
||||
}
|
||||
}
|
||||
2952
packages/hoppscotch-agent/pnpm-lock.yaml
generated
Normal file
6
packages/hoppscotch-agent/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
6
packages/hoppscotch-agent/public/tauri.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
1
packages/hoppscotch-agent/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
25
packages/hoppscotch-agent/src-tauri/.cargo/config.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
# Enable static linking for C runtime library on Windows.
|
||||
#
|
||||
# Rust uses the msvc toolchain on Windows,
|
||||
# which by default dynamically links the C runtime (CRT) to the binary.
|
||||
#
|
||||
# This creates a runtime dependency on the Visual C++ Redistributable (`vcredist`),
|
||||
# meaning the target machine must have `vcredist` installed for the application to run.
|
||||
#
|
||||
# Since `portable` version doesn't have an installer,
|
||||
# we can't rely on it to install dependencies, so this config.
|
||||
#
|
||||
# Basically:
|
||||
# - The `+crt-static` flag instructs the Rust compiler to statically link the C runtime for Windows builds.\
|
||||
# - To avoids runtime errors related to missing `vcredist` installations.
|
||||
# - Results in a larger binary size because the runtime is bundled directly into the executable.
|
||||
#
|
||||
# For MSVC targets specifically, it will compile code with `/MT` or static linkage.
|
||||
# See: - RFC 1721: https://rust-lang.github.io/rfcs/1721-crt-static.html
|
||||
# - Rust Reference - Runtime: https://doc.rust-lang.org/reference/runtime.html
|
||||
# - MSVC Linking Options: https://docs.microsoft.com/en-us/cpp/build/reference/md-mt-ld-use-run-time-library
|
||||
# - Rust Issue #37406: https://github.com/rust-lang/rust/issues/37406
|
||||
# - Tauri Issue #3048: https://github.com/tauri-apps/tauri/issues/3048
|
||||
# - Rust Linkage: https://doc.rust-lang.org/reference/linkage.html
|
||||
[target.'cfg(windows)']
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
7
packages/hoppscotch-agent/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
6482
packages/hoppscotch-agent/src-tauri/Cargo.lock
generated
Normal file
56
packages/hoppscotch-agent/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,56 @@
|
||||
[package]
|
||||
name = "hoppscotch-agent"
|
||||
version = "0.1.3"
|
||||
description = "A cross-platform HTTP request agent for Hoppscotch for advanced request handling including custom headers, certificates, proxies, and local system integration."
|
||||
authors = ["AndrewBastin", "CuriousCorrelation"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "hoppscotch_agent_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.0.4", features = ["tray-icon", "image-png"] }
|
||||
tauri-plugin-shell = "2.0.1"
|
||||
tauri-plugin-autostart = { version = "2.0.1", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1.40.0", features = ["full"] }
|
||||
dashmap = { version = "6.1.0", features = ["serde"] }
|
||||
axum = { version = "0.7.7" }
|
||||
axum-extra = { version = "0.9.4", features = ["typed-header"] }
|
||||
tower-http = { version = "0.6.1", features = ["cors"] }
|
||||
tokio-util = "0.7.12"
|
||||
uuid = { version = "1.11.0", features = [ "v4", "fast-rng" ] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
rand = "0.8.5"
|
||||
log = "0.4.22"
|
||||
env_logger = "0.11.5"
|
||||
hoppscotch-relay = { path = "../../hoppscotch-relay" }
|
||||
thiserror = "1.0.64"
|
||||
tauri-plugin-store = "2.1.0"
|
||||
x25519-dalek = { version = "2.0.1", features = ["getrandom"] }
|
||||
base16 = "0.2.1"
|
||||
aes-gcm = { version = "0.10.3", features = ["aes"] }
|
||||
tauri-plugin-updater = "2.0.2"
|
||||
tauri-plugin-dialog = "2.0.1"
|
||||
lazy_static = "1.5.0"
|
||||
tauri-plugin-single-instance = "2.0.1"
|
||||
tauri-plugin-http = { version = "2.0.1", features = ["gzip"] }
|
||||
native-dialog = "0.7.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
tempfile = { version = "3.13.0" }
|
||||
winreg = { version = "0.52.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
mockito = "1.5.0"
|
||||
|
||||
[features]
|
||||
default = ["tauri-plugin-autostart"]
|
||||
portable = []
|
||||
5
packages/hoppscotch-agent/src-tauri/build.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
fn main() {
|
||||
tauri_build::build();
|
||||
println!("cargo::rerun-if-env-changed=UPDATER_PUB_KEY");
|
||||
println!("cargo::rerun-if-env-changed=UPDATER_URL");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main", "test"],
|
||||
"permissions": [
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{
|
||||
"url": "https://*.tauri.app"
|
||||
},
|
||||
{
|
||||
"url": "https://*.microsoft.*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"core:default",
|
||||
"shell:allow-open",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-set-always-on-top"
|
||||
]
|
||||
}
|
||||
BIN
packages/hoppscotch-agent/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 23 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/icon.icns
Normal file
BIN
packages/hoppscotch-agent/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 948 B |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 545 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 19 KiB |
BIN
packages/hoppscotch-agent/src-tauri/icons/tray_icon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
220
packages/hoppscotch-agent/src-tauri/src/controller.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::{Path, State},
|
||||
http::HeaderMap,
|
||||
Json,
|
||||
};
|
||||
use axum_extra::{
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
TypedHeader,
|
||||
};
|
||||
use hoppscotch_relay::{RequestWithMetadata, ResponseWithMetadata};
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
use crate::{
|
||||
error::{AgentError, AgentResult},
|
||||
model::{AuthKeyResponse, ConfirmedRegistrationRequest, HandshakeResponse},
|
||||
state::{AppState, Registration},
|
||||
util::EncryptedJson,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use rand::Rng;
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn generate_otp() -> String {
|
||||
let otp: u32 = rand::thread_rng().gen_range(0..1_000_000);
|
||||
|
||||
format!("{:06}", otp)
|
||||
}
|
||||
|
||||
pub async fn handshake(
|
||||
State((_, app_handle)): State<(Arc<AppState>, AppHandle)>,
|
||||
) -> AgentResult<Json<HandshakeResponse>> {
|
||||
Ok(Json(HandshakeResponse {
|
||||
status: "success".to_string(),
|
||||
__hoppscotch__agent__: true,
|
||||
agent_version: app_handle.package_info().version.to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn receive_registration(
|
||||
State((state, app_handle)): State<(Arc<AppState>, AppHandle)>,
|
||||
) -> AgentResult<Json<serde_json::Value>> {
|
||||
let otp = generate_otp();
|
||||
|
||||
let mut active_registration_code = state.active_registration_code.write().await;
|
||||
|
||||
if !active_registration_code.is_none() {
|
||||
return Ok(Json(
|
||||
json!({ "message": "There is already an existing registration happening" }),
|
||||
));
|
||||
}
|
||||
|
||||
*active_registration_code = Some(otp.clone());
|
||||
|
||||
app_handle
|
||||
.emit("registration_received", otp)
|
||||
.map_err(|_| AgentError::InternalServerError)?;
|
||||
|
||||
Ok(Json(
|
||||
json!({ "message": "Registration received and stored" }),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn verify_registration(
|
||||
State((state, app_handle)): State<(Arc<AppState>, AppHandle)>,
|
||||
Json(confirmed_registration): Json<ConfirmedRegistrationRequest>,
|
||||
) -> AgentResult<Json<AuthKeyResponse>> {
|
||||
state
|
||||
.validate_registration(&confirmed_registration.registration)
|
||||
.await
|
||||
.then_some(())
|
||||
.ok_or(AgentError::InvalidRegistration)?;
|
||||
|
||||
let auth_key = Uuid::new_v4().to_string();
|
||||
let created_at = Utc::now();
|
||||
|
||||
let auth_key_copy = auth_key.clone();
|
||||
|
||||
let agent_secret_key = EphemeralSecret::random();
|
||||
let agent_public_key = PublicKey::from(&agent_secret_key);
|
||||
|
||||
let their_public_key = {
|
||||
let public_key_slice: &[u8; 32] =
|
||||
&base16::decode(&confirmed_registration.client_public_key_b16)
|
||||
.map_err(|_| AgentError::InvalidClientPublicKey)?[0..32]
|
||||
.try_into()
|
||||
.map_err(|_| AgentError::InvalidClientPublicKey)?;
|
||||
|
||||
PublicKey::from(public_key_slice.to_owned())
|
||||
};
|
||||
|
||||
let shared_secret = agent_secret_key.diffie_hellman(&their_public_key);
|
||||
|
||||
let _ = state.update_registrations(app_handle.clone(), |regs| {
|
||||
regs.insert(
|
||||
auth_key_copy,
|
||||
Registration {
|
||||
registered_at: created_at,
|
||||
shared_secret_b16: base16::encode_lower(shared_secret.as_bytes()),
|
||||
},
|
||||
);
|
||||
})?;
|
||||
|
||||
let auth_payload = json!({
|
||||
"auth_key": auth_key,
|
||||
"created_at": created_at
|
||||
});
|
||||
|
||||
app_handle
|
||||
.emit("authenticated", &auth_payload)
|
||||
.map_err(|_| AgentError::InternalServerError)?;
|
||||
|
||||
Ok(Json(AuthKeyResponse {
|
||||
auth_key,
|
||||
created_at,
|
||||
agent_public_key_b16: base16::encode_lower(agent_public_key.as_bytes()),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn run_request<T>(
|
||||
State((state, _app_handle)): State<(Arc<AppState>, T)>,
|
||||
TypedHeader(auth_header): TypedHeader<Authorization<Bearer>>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> AgentResult<EncryptedJson<ResponseWithMetadata>> {
|
||||
let nonce = headers
|
||||
.get("X-Hopp-Nonce")
|
||||
.ok_or(AgentError::Unauthorized)?
|
||||
.to_str()
|
||||
.map_err(|_| AgentError::Unauthorized)?;
|
||||
|
||||
let req: RequestWithMetadata = state
|
||||
.validate_access_and_get_data(auth_header.token(), nonce, &body)
|
||||
.ok_or(AgentError::Unauthorized)?;
|
||||
|
||||
let req_id = req.req_id;
|
||||
|
||||
let reg_info = state
|
||||
.get_registration_info(auth_header.token())
|
||||
.ok_or(AgentError::Unauthorized)?;
|
||||
|
||||
let cancel_token = tokio_util::sync::CancellationToken::new();
|
||||
state.add_cancellation_token(req.req_id, cancel_token.clone());
|
||||
|
||||
let cancel_token_clone = cancel_token.clone();
|
||||
// Execute the HTTP request in a blocking thread pool and handles cancellation.
|
||||
//
|
||||
// It:
|
||||
// 1. Uses `spawn_blocking` to run the sync `run_request_task`
|
||||
// without blocking the main Tokio runtime.
|
||||
// 2. Uses `select!` to concurrently wait for either
|
||||
// a. the task to complete,
|
||||
// b. or a cancellation signal.
|
||||
//
|
||||
// Why spawn_blocking?
|
||||
// - `run_request_task` uses synchronous curl operations which would block
|
||||
// the async runtime if not run in a separate thread.
|
||||
// - `spawn_blocking` moves this operation to a thread pool designed for
|
||||
// blocking tasks, so other async operations to continue unblocked.
|
||||
let result = tokio::select! {
|
||||
res = tokio::task::spawn_blocking(move || hoppscotch_relay::run_request_task(&req, cancel_token_clone)) => {
|
||||
match res {
|
||||
Ok(task_result) => Ok(task_result?),
|
||||
Err(_) => Err(AgentError::InternalServerError),
|
||||
}
|
||||
},
|
||||
_ = cancel_token.cancelled() => {
|
||||
Err(AgentError::RequestCancelled)
|
||||
}
|
||||
};
|
||||
|
||||
state.remove_cancellation_token(req_id);
|
||||
|
||||
result.map(|val| EncryptedJson {
|
||||
key_b16: reg_info.shared_secret_b16,
|
||||
data: val,
|
||||
})
|
||||
}
|
||||
|
||||
/// Provides a way for registered clients to check if their
|
||||
/// registration still holds, this route is supposed to return
|
||||
/// an encrypted `true` value if the given auth_key is good.
|
||||
/// Since its encrypted with the shared secret established during
|
||||
/// registration, the client also needs the shared secret to verify
|
||||
/// if the read fails, or the auth_key didn't validate and this route returns
|
||||
/// undefined, we can count on the registration not being valid anymore.
|
||||
pub async fn registered_handshake(
|
||||
State((state, _)): State<(Arc<AppState>, AppHandle)>,
|
||||
TypedHeader(auth_header): TypedHeader<Authorization<Bearer>>,
|
||||
) -> AgentResult<EncryptedJson<serde_json::Value>> {
|
||||
let reg_info = state.get_registration_info(auth_header.token());
|
||||
|
||||
match reg_info {
|
||||
Some(reg) => Ok(EncryptedJson {
|
||||
key_b16: reg.shared_secret_b16,
|
||||
data: json!(true),
|
||||
}),
|
||||
None => Err(AgentError::Unauthorized),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn cancel_request<T>(
|
||||
State((state, _app_handle)): State<(Arc<AppState>, T)>,
|
||||
TypedHeader(auth_header): TypedHeader<Authorization<Bearer>>,
|
||||
Path(req_id): Path<usize>,
|
||||
) -> AgentResult<Json<serde_json::Value>> {
|
||||
if !state.validate_access(auth_header.token()) {
|
||||
return Err(AgentError::Unauthorized);
|
||||
}
|
||||
|
||||
if let Some((_, token)) = state.remove_cancellation_token(req_id) {
|
||||
token.cancel();
|
||||
Ok(Json(json!({"message": "Request cancelled successfully"})))
|
||||
} else {
|
||||
Err(AgentError::RequestNotFound)
|
||||
}
|
||||
}
|
||||
58
packages/hoppscotch-agent/src-tauri/src/dialog.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use native_dialog::{MessageDialog, MessageType};
|
||||
|
||||
pub fn panic(msg: &str) {
|
||||
const FATAL_ERROR: &str = "Fatal error";
|
||||
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Error)
|
||||
.set_title(FATAL_ERROR)
|
||||
.set_text(msg)
|
||||
.show_alert()
|
||||
.unwrap_or_default();
|
||||
|
||||
log::error!("{}: {}", FATAL_ERROR, msg);
|
||||
|
||||
panic!("{}: {}", FATAL_ERROR, msg);
|
||||
}
|
||||
|
||||
pub fn info(msg: &str) {
|
||||
log::info!("{}", msg);
|
||||
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Info)
|
||||
.set_title("Info")
|
||||
.set_text(msg)
|
||||
.show_alert()
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
pub fn warn(msg: &str) {
|
||||
log::warn!("{}", msg);
|
||||
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Warning)
|
||||
.set_title("Warning")
|
||||
.set_text(msg)
|
||||
.show_alert()
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
pub fn error(msg: &str) {
|
||||
log::error!("{}", msg);
|
||||
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Error)
|
||||
.set_title("Error")
|
||||
.set_text(msg)
|
||||
.show_alert()
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
pub fn confirm(title: &str, msg: &str, icon: MessageType) -> bool {
|
||||
MessageDialog::new()
|
||||
.set_type(icon)
|
||||
.set_title(title)
|
||||
.set_text(msg)
|
||||
.show_confirm()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
81
packages/hoppscotch-agent/src-tauri/src/error.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AgentError {
|
||||
#[error("Invalid Registration")]
|
||||
InvalidRegistration,
|
||||
#[error("Invalid Client Public Key")]
|
||||
InvalidClientPublicKey,
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
#[error("Request not found or already completed")]
|
||||
RequestNotFound,
|
||||
#[error("Internal server error")]
|
||||
InternalServerError,
|
||||
#[error("Invalid request: {0}")]
|
||||
BadRequest(String),
|
||||
#[error("Client certificate error")]
|
||||
ClientCertError,
|
||||
#[error("Root certificate error")]
|
||||
RootCertError,
|
||||
#[error("Invalid method")]
|
||||
InvalidMethod,
|
||||
#[error("Invalid URL")]
|
||||
InvalidUrl,
|
||||
#[error("Invalid headers")]
|
||||
InvalidHeaders,
|
||||
#[error("Request run error: {0}")]
|
||||
RequestRunError(String),
|
||||
#[error("Request cancelled")]
|
||||
RequestCancelled,
|
||||
#[error("Failed to clear registrations")]
|
||||
RegistrationClearError,
|
||||
#[error("Failed to insert registrations")]
|
||||
RegistrationInsertError,
|
||||
#[error("Failed to save registrations to store")]
|
||||
RegistrationSaveError,
|
||||
#[error("Serde error: {0}")]
|
||||
Serde(#[from] serde_json::Error),
|
||||
#[error("Store error: {0}")]
|
||||
TauriPluginStore(#[from] tauri_plugin_store::Error),
|
||||
#[error("Relay error: {0}")]
|
||||
Relay(#[from] hoppscotch_relay::RelayError),
|
||||
}
|
||||
|
||||
impl IntoResponse for AgentError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, error_message) = match self {
|
||||
AgentError::InvalidRegistration => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
AgentError::InvalidClientPublicKey => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
AgentError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()),
|
||||
AgentError::RequestNotFound => (StatusCode::NOT_FOUND, self.to_string()),
|
||||
AgentError::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
|
||||
AgentError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
|
||||
AgentError::ClientCertError => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
AgentError::RootCertError => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
AgentError::InvalidMethod => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
AgentError::InvalidUrl => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
AgentError::InvalidHeaders => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
AgentError::RequestRunError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
|
||||
AgentError::RequestCancelled => (StatusCode::BAD_REQUEST, self.to_string()),
|
||||
_ => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Internal Server Error".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
let body = Json(json!({
|
||||
"error": error_message,
|
||||
}));
|
||||
|
||||
(status, body).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub type AgentResult<T> = std::result::Result<T, AgentError>;
|
||||
2
packages/hoppscotch-agent/src-tauri/src/global.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub const AGENT_STORE: &str = "app_data.bin";
|
||||
pub const REGISTRATIONS: &str = "registrations";
|
||||
178
packages/hoppscotch-agent/src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
pub mod controller;
|
||||
pub mod dialog;
|
||||
pub mod error;
|
||||
pub mod global;
|
||||
pub mod model;
|
||||
pub mod route;
|
||||
pub mod server;
|
||||
pub mod state;
|
||||
pub mod tray;
|
||||
pub mod updater;
|
||||
pub mod util;
|
||||
pub mod webview;
|
||||
|
||||
use log::{error, info};
|
||||
use std::sync::Arc;
|
||||
use tauri::{Emitter, Listener, Manager, WebviewWindowBuilder};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use model::Payload;
|
||||
use state::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_otp(state: tauri::State<'_, Arc<AppState>>) -> Result<Option<String>, ()> {
|
||||
Ok(state.active_registration_code.read().await.clone())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
env_logger::init();
|
||||
|
||||
// The installer takes care of installing `WebView`,
|
||||
// this check is only required for portable variant.
|
||||
#[cfg(all(feature = "portable", windows))]
|
||||
webview::init_webview();
|
||||
|
||||
let cancellation_token = CancellationToken::new();
|
||||
let server_cancellation_token = cancellation_token.clone();
|
||||
|
||||
tauri::Builder::default()
|
||||
// NOTE: Currently, plugins run in the order they were added in to the builder,
|
||||
// so `tauri_plugin_single_instance` needs to be registered first.
|
||||
// See: https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/single-instance
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
|
||||
info!("{}, {args:?}, {cwd}", app.package_info().name);
|
||||
|
||||
app.emit("single-instance", Payload::new(args, cwd))
|
||||
.unwrap();
|
||||
|
||||
// Application is already running, bring it to foreground.
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
} else {
|
||||
error!("Failed to get `main` window");
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.setup(move |app| {
|
||||
let app_handle = app.app_handle();
|
||||
|
||||
#[cfg(all(desktop, not(feature = "portable")))]
|
||||
{
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
|
||||
let _ = app.handle().plugin(tauri_plugin_autostart::init(
|
||||
MacosLauncher::LaunchAgent,
|
||||
None,
|
||||
));
|
||||
|
||||
let autostart_manager = app.autolaunch();
|
||||
|
||||
println!(
|
||||
"autostart enabled: {}",
|
||||
autostart_manager.is_enabled().unwrap()
|
||||
);
|
||||
|
||||
if !autostart_manager.is_enabled().unwrap() {
|
||||
let _ = autostart_manager.enable();
|
||||
println!(
|
||||
"autostart updated: {}",
|
||||
autostart_manager.is_enabled().unwrap()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
let _ = app
|
||||
.handle()
|
||||
.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
|
||||
let _ = app.handle().plugin(tauri_plugin_dialog::init());
|
||||
|
||||
let updater = app.updater_builder().build().unwrap();
|
||||
|
||||
let app_handle_ref = app_handle.clone();
|
||||
|
||||
tauri::async_runtime::spawn_blocking(|| {
|
||||
tauri::async_runtime::block_on(async {
|
||||
updater::check_and_install_updates(app_handle_ref, updater).await;
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
let app_state = Arc::new(AppState::new(app_handle.clone())?);
|
||||
|
||||
app.manage(app_state.clone());
|
||||
|
||||
let server_cancellation_token = server_cancellation_token.clone();
|
||||
|
||||
let server_app_handle = app_handle.clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
server::run_server(app_state, server_cancellation_token, server_app_handle).await;
|
||||
});
|
||||
|
||||
#[cfg(all(desktop))]
|
||||
{
|
||||
let handle = app.handle();
|
||||
tray::create_tray(handle)?;
|
||||
}
|
||||
|
||||
// Blocks the app from populating the macOS dock
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
app_handle
|
||||
.set_activation_policy(tauri::ActivationPolicy::Accessory)
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
let app_handle_ref = app_handle.clone();
|
||||
|
||||
app_handle.listen("registration_received", move |_| {
|
||||
WebviewWindowBuilder::from_config(
|
||||
&app_handle_ref,
|
||||
&app_handle_ref.config().app.windows[0],
|
||||
)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.show()
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.manage(cancellation_token)
|
||||
.on_window_event(|window, event| {
|
||||
match &event {
|
||||
tauri::WindowEvent::CloseRequested { .. } => {
|
||||
let app_state = window.state::<Arc<AppState>>();
|
||||
|
||||
let mut current_code = app_state.active_registration_code.blocking_write();
|
||||
|
||||
if current_code.is_some() {
|
||||
*current_code = None;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![get_otp])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
.run(|app_handle, event| match event {
|
||||
tauri::RunEvent::ExitRequested { api, code, .. } => {
|
||||
if code.is_none() || matches!(code, Some(0)) {
|
||||
api.prevent_exit()
|
||||
} else if code.is_some() {
|
||||
let state = app_handle.state::<CancellationToken>();
|
||||
state.cancel();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
6
packages/hoppscotch-agent/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
hoppscotch_agent_lib::run()
|
||||
}
|
||||
47
packages/hoppscotch-agent/src-tauri/src/model.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Single instance payload.
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct Payload {
|
||||
args: Vec<String>,
|
||||
cwd: String,
|
||||
}
|
||||
|
||||
impl Payload {
|
||||
pub fn new(args: Vec<String>, cwd: String) -> Self {
|
||||
Self { args, cwd }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct HandshakeResponse {
|
||||
#[allow(non_snake_case)]
|
||||
pub __hoppscotch__agent__: bool,
|
||||
|
||||
pub status: String,
|
||||
pub agent_version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ConfirmedRegistrationRequest {
|
||||
pub registration: String,
|
||||
|
||||
/// base16 (lowercase) encoded public key shared by the client
|
||||
/// to the agent so that the agent can establish a shared secret
|
||||
/// which will be used to encrypt traffic between agent
|
||||
/// and client after registration
|
||||
pub client_public_key_b16: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AuthKeyResponse {
|
||||
pub auth_key: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
/// base16 (lowercase) encoded public key shared by the
|
||||
/// agent so that the client can establish a shared secret
|
||||
/// which will be used to encrypt traffic between agent
|
||||
/// and client after registration
|
||||
pub agent_public_key_b16: String,
|
||||
}
|
||||
28
packages/hoppscotch-agent/src-tauri/src/route.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use axum::{
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::{controller, state::AppState};
|
||||
|
||||
pub fn route(state: Arc<AppState>, app_handle: AppHandle) -> Router {
|
||||
Router::new()
|
||||
.route("/handshake", get(controller::handshake))
|
||||
.route(
|
||||
"/receive-registration",
|
||||
post(controller::receive_registration),
|
||||
)
|
||||
.route(
|
||||
"/verify-registration",
|
||||
post(controller::verify_registration),
|
||||
)
|
||||
.route(
|
||||
"/registered-handshake",
|
||||
get(controller::registered_handshake),
|
||||
)
|
||||
.route("/request", post(controller::run_request))
|
||||
.route("/cancel-request/:req_id", post(controller::cancel_request))
|
||||
.with_state((state, app_handle))
|
||||
}
|
||||
34
packages/hoppscotch-agent/src-tauri/src/server.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use axum::Router;
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tower_http::cors::CorsLayer;
|
||||
|
||||
use crate::route;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub async fn run_server(
|
||||
state: Arc<AppState>,
|
||||
cancellation_token: CancellationToken,
|
||||
app_handle: tauri::AppHandle,
|
||||
) {
|
||||
let cors = CorsLayer::permissive();
|
||||
|
||||
let app = Router::new()
|
||||
.merge(route::route(state, app_handle))
|
||||
.layer(cors);
|
||||
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 9119));
|
||||
|
||||
println!("Server running on http://{}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.with_graceful_shutdown(async move {
|
||||
cancellation_token.cancelled().await;
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("Server shut down");
|
||||
}
|
||||
161
packages/hoppscotch-agent/src-tauri/src/state.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit};
|
||||
use axum::body::Bytes;
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
error::{AgentError, AgentResult},
|
||||
global::{AGENT_STORE, REGISTRATIONS},
|
||||
};
|
||||
|
||||
/// Describes one registered app instance
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Registration {
|
||||
pub registered_at: DateTime<Utc>,
|
||||
|
||||
/// base16 (lowercase) encoded shared secret that the client
|
||||
/// and agent established during registration that is used
|
||||
/// to encrypt traffic between them
|
||||
pub shared_secret_b16: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AppState {
|
||||
/// The active registration code that is being registered.
|
||||
pub active_registration_code: RwLock<Option<String>>,
|
||||
|
||||
/// Cancellation Tokens for the running requests
|
||||
pub cancellation_tokens: DashMap<usize, CancellationToken>,
|
||||
|
||||
/// Registrations against the agent, the key is the auth
|
||||
/// token associated to the registration
|
||||
registrations: DashMap<String, Registration>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(app_handle: tauri::AppHandle) -> AgentResult<Self> {
|
||||
let store = app_handle.store(AGENT_STORE)?;
|
||||
|
||||
// Try loading and parsing registrations from the store, if that failed,
|
||||
// load the default list
|
||||
let registrations = store
|
||||
.get(REGISTRATIONS)
|
||||
.and_then(|val| serde_json::from_value(val.clone()).ok())
|
||||
.unwrap_or_else(|| DashMap::new());
|
||||
|
||||
// Try to save the latest registrations list
|
||||
let _ = store.set(REGISTRATIONS, serde_json::to_value(®istrations)?);
|
||||
let _ = store.save();
|
||||
|
||||
Ok(Self {
|
||||
active_registration_code: RwLock::new(None),
|
||||
cancellation_tokens: DashMap::new(),
|
||||
registrations,
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets you a readonly reference to the registrations list
|
||||
/// NOTE: Although DashMap API allows you to update the list from an immutable
|
||||
/// reference, you shouldn't do it for registrations as `update_registrations`
|
||||
/// performs save operation that needs to be done and should be used instead
|
||||
pub fn get_registrations(&self) -> &DashMap<String, Registration> {
|
||||
&self.registrations
|
||||
}
|
||||
|
||||
/// Provides you an opportunity to update the registrations list
|
||||
/// and also persists the data to the disk.
|
||||
/// This function bypasses `store.reload()` to avoid issues from stale or inconsistent
|
||||
/// data on disk. By relying solely on the in-memory `self.registrations`,
|
||||
/// we make sure that updates are applied based on the most recent changes in memory.
|
||||
pub fn update_registrations(
|
||||
&self,
|
||||
app_handle: tauri::AppHandle,
|
||||
update_func: impl FnOnce(&DashMap<String, Registration>),
|
||||
) -> Result<(), AgentError> {
|
||||
update_func(&self.registrations);
|
||||
|
||||
let store = app_handle.store(AGENT_STORE)?;
|
||||
|
||||
if store.has(REGISTRATIONS) {
|
||||
// We've confirmed `REGISTRATIONS` exists in the store
|
||||
store
|
||||
.delete(REGISTRATIONS)
|
||||
.then_some(())
|
||||
.ok_or(AgentError::RegistrationClearError)?;
|
||||
} else {
|
||||
log::debug!("`REGISTRATIONS` key not found in store; continuing with update.");
|
||||
}
|
||||
|
||||
// Since we've established `self.registrations` as the source of truth,
|
||||
// we avoid reloading the store from disk and instead choose to override it.
|
||||
|
||||
store.set(
|
||||
REGISTRATIONS,
|
||||
serde_json::to_value(self.registrations.clone())?,
|
||||
);
|
||||
|
||||
// Explicitly save the changes
|
||||
store.save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear all the registrations
|
||||
pub fn clear_registrations(&self, app_handle: tauri::AppHandle) -> Result<(), AgentError> {
|
||||
Ok(self.update_registrations(app_handle, |registrations| registrations.clear())?)
|
||||
}
|
||||
|
||||
pub async fn validate_registration(&self, registration: &str) -> bool {
|
||||
self.active_registration_code.read().await.as_deref() == Some(registration)
|
||||
}
|
||||
|
||||
pub fn remove_cancellation_token(&self, req_id: usize) -> Option<(usize, CancellationToken)> {
|
||||
self.cancellation_tokens.remove(&req_id)
|
||||
}
|
||||
|
||||
pub fn add_cancellation_token(&self, req_id: usize, cancellation_tokens: CancellationToken) {
|
||||
self.cancellation_tokens.insert(req_id, cancellation_tokens);
|
||||
}
|
||||
|
||||
pub fn validate_access(&self, auth_key: &str) -> bool {
|
||||
self.registrations.get(auth_key).is_some()
|
||||
}
|
||||
|
||||
pub fn validate_access_and_get_data<T>(
|
||||
&self,
|
||||
auth_key: &str,
|
||||
nonce: &str,
|
||||
data: &Bytes,
|
||||
) -> Option<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
if let Some(registration) = self.registrations.get(auth_key) {
|
||||
let key: [u8; 32] = base16::decode(®istration.shared_secret_b16).ok()?[0..32]
|
||||
.try_into()
|
||||
.ok()?;
|
||||
|
||||
let nonce: [u8; 12] = base16::decode(nonce).ok()?[0..12].try_into().ok()?;
|
||||
|
||||
let cipher = Aes256Gcm::new(&key.into());
|
||||
|
||||
let data = data.iter().cloned().collect::<Vec<u8>>();
|
||||
|
||||
let plain_data = cipher.decrypt(&nonce.into(), data.as_slice()).ok()?;
|
||||
|
||||
serde_json::from_reader(plain_data.as_slice()).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_registration_info(&self, auth_key: &str) -> Option<Registration> {
|
||||
self.registrations
|
||||
.get(auth_key)
|
||||
.map(|reference| reference.value().clone())
|
||||
}
|
||||
}
|
||||
91
packages/hoppscotch-agent/src-tauri/src/tray.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use crate::state::AppState;
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Arc;
|
||||
use tauri::{
|
||||
image::Image,
|
||||
menu::{MenuBuilder, MenuItem},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
AppHandle, Manager,
|
||||
};
|
||||
|
||||
const TRAY_ICON_DATA: &'static [u8] = include_bytes!("../icons/tray_icon.png");
|
||||
|
||||
lazy_static! {
|
||||
static ref TRAY_ICON: Image<'static> = Image::from_bytes(TRAY_ICON_DATA).unwrap();
|
||||
}
|
||||
|
||||
pub fn create_tray(app: &AppHandle) -> tauri::Result<()> {
|
||||
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
|
||||
let clear_registrations = MenuItem::with_id(
|
||||
app,
|
||||
"clear_registrations",
|
||||
"Clear Registrations",
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let pkg_info = app.package_info();
|
||||
let app_name = pkg_info.name.clone();
|
||||
let app_version = pkg_info.version.clone();
|
||||
|
||||
let app_name_item = MenuItem::with_id(app, "app_name", app_name, false, None::<&str>)?;
|
||||
let app_version_item = MenuItem::with_id(
|
||||
app,
|
||||
"app_version",
|
||||
format!("Version: {}", app_version),
|
||||
false,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let menu = MenuBuilder::new(app)
|
||||
.item(&app_name_item)
|
||||
.item(&app_version_item)
|
||||
.separator()
|
||||
.item(&clear_registrations)
|
||||
.item(&quit_i)
|
||||
.build()?;
|
||||
|
||||
let _ = TrayIconBuilder::with_id("hopp-tray")
|
||||
.tooltip("Hoppscotch Agent")
|
||||
.icon(if cfg!(target_os = "macos") {
|
||||
TRAY_ICON.clone()
|
||||
} else {
|
||||
app.default_window_icon().unwrap().clone()
|
||||
})
|
||||
.icon_as_template(cfg!(target_os = "macos"))
|
||||
.menu(&menu)
|
||||
.menu_on_left_click(true)
|
||||
.on_menu_event(move |app, event| match event.id.as_ref() {
|
||||
"quit" => {
|
||||
log::info!("Exiting the agent...");
|
||||
app.exit(-1);
|
||||
}
|
||||
"clear_registrations" => {
|
||||
let app_state = app.state::<Arc<AppState>>();
|
||||
|
||||
app_state
|
||||
.clear_registrations(app.clone())
|
||||
.expect("Invariant violation: Failed to clear registrations");
|
||||
}
|
||||
_ => {
|
||||
log::warn!("Unhandled menu event: {:?}", event.id);
|
||||
}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
let app = tray.app_handle();
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(app);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
67
packages/hoppscotch-agent/src-tauri/src/updater.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use tauri_plugin_dialog::MessageDialogButtons;
|
||||
use tauri_plugin_dialog::MessageDialogKind;
|
||||
|
||||
#[cfg(feature = "portable")]
|
||||
use {crate::dialog, crate::util, native_dialog::MessageType};
|
||||
|
||||
pub async fn check_and_install_updates(
|
||||
app: tauri::AppHandle,
|
||||
updater: tauri_plugin_updater::Updater,
|
||||
) {
|
||||
let update = updater.check().await;
|
||||
|
||||
if let Ok(Some(update)) = update {
|
||||
#[cfg(not(feature = "portable"))]
|
||||
{
|
||||
let do_update = app
|
||||
.dialog()
|
||||
.message(format!(
|
||||
"Update to {} is available!{}",
|
||||
update.version,
|
||||
update
|
||||
.body
|
||||
.clone()
|
||||
.map(|body| format!("\n\nRelease Notes: {}", body))
|
||||
.unwrap_or("".into())
|
||||
))
|
||||
.title("Update Available")
|
||||
.kind(MessageDialogKind::Info)
|
||||
.buttons(MessageDialogButtons::OkCancelCustom(
|
||||
"Update".to_string(),
|
||||
"Cancel".to_string(),
|
||||
))
|
||||
.blocking_show();
|
||||
|
||||
if do_update {
|
||||
let _ = update.download_and_install(|_, _| {}, || {}).await;
|
||||
|
||||
tauri::process::restart(&app.env());
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "portable")]
|
||||
{
|
||||
let download_url = "https://hoppscotch.com/download";
|
||||
let message = format!(
|
||||
"An update (version {}) is available for the Hoppscotch Agent.\n\nPlease download the latest portable version from our website.",
|
||||
update.version
|
||||
);
|
||||
|
||||
dialog::info(&message);
|
||||
|
||||
if dialog::confirm(
|
||||
"Open Download Page",
|
||||
"Would you like to open the download page in your browser?",
|
||||
MessageType::Info,
|
||||
) {
|
||||
if let None = util::open_link(download_url) {
|
||||
dialog::error(&format!(
|
||||
"Failed to open download page. Please visit {}",
|
||||
download_url
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
packages/hoppscotch-agent/src-tauri/src/util.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use aes_gcm::{aead::Aead, AeadCore, Aes256Gcm, KeyInit};
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use rand::rngs::OsRng;
|
||||
use serde::Serialize;
|
||||
|
||||
pub fn open_link(link: &str) -> Option<()> {
|
||||
let null = Stdio::null();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Command::new("rundll32")
|
||||
.args(["url.dll,FileProtocolHandler", link])
|
||||
.stdout(null)
|
||||
.spawn()
|
||||
.ok()
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Command::new("open")
|
||||
.arg(link)
|
||||
.stdout(null)
|
||||
.spawn()
|
||||
.ok()
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Command::new("xdg-open")
|
||||
.arg(link)
|
||||
.stdout(null)
|
||||
.spawn()
|
||||
.ok()
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EncryptedJson<T: Serialize> {
|
||||
pub key_b16: String,
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
impl<T> IntoResponse for EncryptedJson<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
let serialized_response = serde_json::to_vec(&self.data)
|
||||
.expect("Failed serializing response to vec for encryption");
|
||||
|
||||
let key: [u8; 32] = base16::decode(&self.key_b16).unwrap()[0..32]
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
let cipher = Aes256Gcm::new(&key.into());
|
||||
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||
|
||||
let nonce_b16 = base16::encode_lower(&nonce);
|
||||
|
||||
let encrypted_response = cipher
|
||||
.encrypt(&nonce, serialized_response.as_slice())
|
||||
.expect("Failed encrypting response");
|
||||
|
||||
let mut response = Response::new(Body::from(encrypted_response));
|
||||
let response_headers = response.headers_mut();
|
||||
|
||||
response_headers.insert("Content-Type", "application/octet-stream".parse().unwrap());
|
||||
response_headers.insert("X-Hopp-Nonce", nonce_b16.parse().unwrap());
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||