mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-05-27 09:59:21 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa3e6e6508 |
@@ -37,11 +37,3 @@ The skill generates markdown-formatted release notes following this structure:
|
|||||||
|
|
||||||
**IMPORTANT**: Always add a blank lines around the markdown code fence and output the markdown code block last
|
**IMPORTANT**: Always add a blank lines around the markdown code fence and output the markdown code block last
|
||||||
**IMPORTANT**: PRs by `@gschier` should not mention the @username
|
**IMPORTANT**: PRs by `@gschier` should not mention the @username
|
||||||
|
|
||||||
## After Generating Release Notes
|
|
||||||
|
|
||||||
After outputting the release notes, ask the user if they would like to create a draft GitHub release with these notes. If they confirm, create the release using:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gh release create <tag> --draft --prerelease --title "<tag>" --notes '<release notes>'
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: Generate Artifacts
|
name: Generate Artifacts
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: [v*]
|
tags: [ v* ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-artifacts:
|
build-artifacts:
|
||||||
@@ -13,37 +13,37 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- platform: "macos-latest" # for Arm-based Macs (M1 and above).
|
- platform: 'macos-latest' # for Arm-based Macs (M1 and above).
|
||||||
args: "--target aarch64-apple-darwin"
|
args: '--target aarch64-apple-darwin'
|
||||||
yaak_arch: "arm64"
|
yaak_arch: 'arm64'
|
||||||
os: "macos"
|
os: 'macos'
|
||||||
targets: "aarch64-apple-darwin"
|
targets: 'aarch64-apple-darwin'
|
||||||
- platform: "macos-latest" # for Intel-based Macs.
|
- platform: 'macos-latest' # for Intel-based Macs.
|
||||||
args: "--target x86_64-apple-darwin"
|
args: '--target x86_64-apple-darwin'
|
||||||
yaak_arch: "x64"
|
yaak_arch: 'x64'
|
||||||
os: "macos"
|
os: 'macos'
|
||||||
targets: "x86_64-apple-darwin"
|
targets: 'x86_64-apple-darwin'
|
||||||
- platform: "ubuntu-22.04"
|
- platform: 'ubuntu-22.04'
|
||||||
args: ""
|
args: ''
|
||||||
yaak_arch: "x64"
|
yaak_arch: 'x64'
|
||||||
os: "ubuntu"
|
os: 'ubuntu'
|
||||||
targets: ""
|
targets: ''
|
||||||
- platform: "ubuntu-22.04-arm"
|
- platform: 'ubuntu-22.04-arm'
|
||||||
args: ""
|
args: ''
|
||||||
yaak_arch: "arm64"
|
yaak_arch: 'arm64'
|
||||||
os: "ubuntu"
|
os: 'ubuntu'
|
||||||
targets: ""
|
targets: ''
|
||||||
- platform: "windows-latest"
|
- platform: 'windows-latest'
|
||||||
args: ""
|
args: ''
|
||||||
yaak_arch: "x64"
|
yaak_arch: 'x64'
|
||||||
os: "windows"
|
os: 'windows'
|
||||||
targets: ""
|
targets: ''
|
||||||
# Windows ARM64
|
# Windows ARM64
|
||||||
- platform: "windows-latest"
|
- platform: 'windows-latest'
|
||||||
args: "--target aarch64-pc-windows-msvc"
|
args: '--target aarch64-pc-windows-msvc'
|
||||||
yaak_arch: "arm64"
|
yaak_arch: 'arm64'
|
||||||
os: "windows"
|
os: 'windows'
|
||||||
targets: "aarch64-pc-windows-msvc"
|
targets: 'aarch64-pc-windows-msvc'
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
steps:
|
steps:
|
||||||
@@ -88,9 +88,6 @@ jobs:
|
|||||||
& $exe --version
|
& $exe --version
|
||||||
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run bootstrap
|
|
||||||
env:
|
|
||||||
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- name: Run JS Tests
|
- name: Run JS Tests
|
||||||
run: npm test
|
run: npm test
|
||||||
@@ -102,29 +99,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
YAAK_VERSION: ${{ github.ref_name }}
|
YAAK_VERSION: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Sign vendored binaries (macOS only)
|
|
||||||
if: matrix.os == 'macos'
|
|
||||||
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: |
|
|
||||||
# Create keychain
|
|
||||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
|
||||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
|
||||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
|
||||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
|
||||||
|
|
||||||
# Import certificate
|
|
||||||
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
|
|
||||||
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
|
||||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
|
||||||
|
|
||||||
# Sign vendored binaries with hardened runtime and their specific entitlements
|
|
||||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app/macos/entitlements.yaakprotoc.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app/vendored/protoc/yaakprotoc || true
|
|
||||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app/macos/entitlements.yaaknode.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app/vendored/node/yaaknode || true
|
|
||||||
|
|
||||||
- uses: tauri-apps/tauri-action@v0
|
- uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
||||||
@@ -147,9 +121,9 @@ jobs:
|
|||||||
AZURE_CLIENT_SECRET: ${{ matrix.os == 'windows' && secrets.AZURE_CLIENT_SECRET }}
|
AZURE_CLIENT_SECRET: ${{ matrix.os == 'windows' && secrets.AZURE_CLIENT_SECRET }}
|
||||||
AZURE_TENANT_ID: ${{ matrix.os == 'windows' && secrets.AZURE_TENANT_ID }}
|
AZURE_TENANT_ID: ${{ matrix.os == 'windows' && secrets.AZURE_TENANT_ID }}
|
||||||
with:
|
with:
|
||||||
tagName: "v__VERSION__"
|
tagName: 'v__VERSION__'
|
||||||
releaseName: "Release __VERSION__"
|
releaseName: 'Release __VERSION__'
|
||||||
releaseBody: "[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)"
|
releaseBody: '[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)'
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
args: "${{ matrix.args }} --config ./crates-tauri/yaak-app/tauri.release.conf.json"
|
args: '${{ matrix.args }} --config ./crates-tauri/yaak-app/tauri.release.conf.json'
|
||||||
|
|||||||
Generated
-4
@@ -8075,7 +8075,6 @@ name = "yaak-common"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8122,10 +8121,8 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
"url",
|
"url",
|
||||||
"yaak-common",
|
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
"yaak-sync",
|
"yaak-sync",
|
||||||
]
|
]
|
||||||
@@ -8152,7 +8149,6 @@ dependencies = [
|
|||||||
"tonic",
|
"tonic",
|
||||||
"tonic-reflection",
|
"tonic-reflection",
|
||||||
"uuid",
|
"uuid",
|
||||||
"yaak-common",
|
|
||||||
"yaak-tls",
|
"yaak-tls",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/JamesIves/github-sponsors-readme-action">
|
<a href="https://github.com/JamesIves/github-sponsors-readme-action">
|
||||||
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/crates-tauri/yaak-app/icons/icon.png">
|
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/src-tauri/icons/icon.png">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https://github.com/MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a> <a href="https://github.com/dharsanb"><img src="https://github.com/dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a> <a href="https://github.com/railwayapp"><img src="https://github.com/railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a> <a href="https://github.com/caseyamcl"><img src="https://github.com/caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a> <a href="https://github.com/bytebase"><img src="https://github.com/bytebase.png" width="80px" alt="User avatar: bytebase" /></a> <a href="https://github.com/"><img src="https://raw.githubusercontent.com/JamesIves/github-sponsors-readme-action/dev/.github/assets/placeholder.png" width="80px" alt="User avatar: " /></a> <!-- sponsors-premium -->
|
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https://github.com/MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a> <a href="https://github.com/dharsanb"><img src="https://github.com/dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a> <a href="https://github.com/railwayapp"><img src="https://github.com/railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a> <a href="https://github.com/caseyamcl"><img src="https://github.com/caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a> <a href="https://github.com/bytebase"><img src="https://github.com/bytebase.png" width="80px" alt="User avatar: bytebase" /></a> <a href="https://github.com/"><img src="https://raw.githubusercontent.com/JamesIves/github-sponsors-readme-action/dev/.github/assets/placeholder.png" width="80px" alt="User avatar: " /></a> <!-- sponsors-premium -->
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https://github.com/seanwash.png" width="50px" alt="User avatar: seanwash" /></a> <a href="https://github.com/jerath"><img src="https://github.com/jerath.png" width="50px" alt="User avatar: jerath" /></a> <a href="https://github.com/itsa-sh"><img src="https://github.com/itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a> <a href="https://github.com/dmmulroy"><img src="https://github.com/dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a> <a href="https://github.com/timcole"><img src="https://github.com/timcole.png" width="50px" alt="User avatar: timcole" /></a> <a href="https://github.com/VLZH"><img src="https://github.com/VLZH.png" width="50px" alt="User avatar: VLZH" /></a> <a href="https://github.com/terasaka2k"><img src="https://github.com/terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a> <a href="https://github.com/andriyor"><img src="https://github.com/andriyor.png" width="50px" alt="User avatar: andriyor" /></a> <a href="https://github.com/majudhu"><img src="https://github.com/majudhu.png" width="50px" alt="User avatar: majudhu" /></a> <a href="https://github.com/axelrindle"><img src="https://github.com/axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a> <a href="https://github.com/jirizverina"><img src="https://github.com/jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="50px" alt="User avatar: chip-well" /></a> <a href="https://github.com/GRAYAH"><img src="https://github.com/GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a> <a href="https://github.com/flashblaze"><img src="https://github.com/flashblaze.png" width="50px" alt="User avatar: flashblaze" /></a> <!-- sponsors-base -->
|
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https://github.com/seanwash.png" width="50px" alt="User avatar: seanwash" /></a> <a href="https://github.com/jerath"><img src="https://github.com/jerath.png" width="50px" alt="User avatar: jerath" /></a> <a href="https://github.com/itsa-sh"><img src="https://github.com/itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a> <a href="https://github.com/dmmulroy"><img src="https://github.com/dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a> <a href="https://github.com/timcole"><img src="https://github.com/timcole.png" width="50px" alt="User avatar: timcole" /></a> <a href="https://github.com/VLZH"><img src="https://github.com/VLZH.png" width="50px" alt="User avatar: VLZH" /></a> <a href="https://github.com/terasaka2k"><img src="https://github.com/terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a> <a href="https://github.com/andriyor"><img src="https://github.com/andriyor.png" width="50px" alt="User avatar: andriyor" /></a> <a href="https://github.com/majudhu"><img src="https://github.com/majudhu.png" width="50px" alt="User avatar: majudhu" /></a> <a href="https://github.com/axelrindle"><img src="https://github.com/axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a> <a href="https://github.com/jirizverina"><img src="https://github.com/jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="50px" alt="User avatar: chip-well" /></a> <a href="https://github.com/GRAYAH"><img src="https://github.com/GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a> <!-- sponsors-base -->
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|

|
||||||
@@ -64,7 +64,7 @@ visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment
|
|||||||
## Useful Resources
|
## Useful Resources
|
||||||
|
|
||||||
- [Feedback and Bug Reports](https://feedback.yaak.app)
|
- [Feedback and Bug Reports](https://feedback.yaak.app)
|
||||||
- [Documentation](https://yaak.app/docs)
|
- [Documentation](https://feedback.yaak.app/help)
|
||||||
- [Yaak vs Postman](https://yaak.app/alternatives/postman)
|
- [Yaak vs Postman](https://yaak.app/alternatives/postman)
|
||||||
- [Yaak vs Bruno](https://yaak.app/alternatives/bruno)
|
- [Yaak vs Bruno](https://yaak.app/alternatives/bruno)
|
||||||
- [Yaak vs Insomnia](https://yaak.app/alternatives/insomnia)
|
- [Yaak vs Insomnia](https://yaak.app/alternatives/insomnia)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use yaak_models::util::UpdateSource;
|
|||||||
use yaak_plugins::events::{PluginContext, RenderPurpose};
|
use yaak_plugins::events::{PluginContext, RenderPurpose};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||||
use yaak_templates::{RenderOptions, parse_and_render, render_json_value_raw};
|
use yaak_templates::{parse_and_render, render_json_value_raw, RenderOptions};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "yaakcli")]
|
#[command(name = "yaakcli")]
|
||||||
@@ -149,7 +149,14 @@ async fn render_http_request(
|
|||||||
// Apply path placeholders (e.g., /users/:id -> /users/123)
|
// Apply path placeholders (e.g., /users/:id -> /users/123)
|
||||||
let (url, url_parameters) = apply_path_placeholders(&url, &url_parameters);
|
let (url, url_parameters) = apply_path_placeholders(&url, &url_parameters);
|
||||||
|
|
||||||
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..r.to_owned() })
|
Ok(HttpRequest {
|
||||||
|
url,
|
||||||
|
url_parameters,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
authentication,
|
||||||
|
..r.to_owned()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -162,10 +169,16 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the same app_id for both data directory and keyring
|
// Use the same app_id for both data directory and keyring
|
||||||
let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" };
|
let app_id = if cfg!(debug_assertions) {
|
||||||
|
"app.yaak.desktop.dev"
|
||||||
|
} else {
|
||||||
|
"app.yaak.desktop"
|
||||||
|
};
|
||||||
|
|
||||||
let data_dir = cli.data_dir.unwrap_or_else(|| {
|
let data_dir = cli.data_dir.unwrap_or_else(|| {
|
||||||
dirs::data_dir().expect("Could not determine data directory").join(app_id)
|
dirs::data_dir()
|
||||||
|
.expect("Could not determine data directory")
|
||||||
|
.join(app_id)
|
||||||
});
|
});
|
||||||
|
|
||||||
let db_path = data_dir.join("db.sqlite");
|
let db_path = data_dir.join("db.sqlite");
|
||||||
@@ -178,7 +191,9 @@ async fn main() {
|
|||||||
|
|
||||||
// Initialize encryption manager for secure() template function
|
// Initialize encryption manager for secure() template function
|
||||||
// Use the same app_id as the Tauri app for keyring access
|
// Use the same app_id as the Tauri app for keyring access
|
||||||
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
let encryption_manager = Arc::new(
|
||||||
|
EncryptionManager::new(query_manager.clone(), app_id),
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize plugin manager for template functions
|
// Initialize plugin manager for template functions
|
||||||
let vendored_plugin_dir = data_dir.join("vendored-plugins");
|
let vendored_plugin_dir = data_dir.join("vendored-plugins");
|
||||||
@@ -188,8 +203,9 @@ async fn main() {
|
|||||||
let node_bin_path = PathBuf::from("node");
|
let node_bin_path = PathBuf::from("node");
|
||||||
|
|
||||||
// Find the plugin runtime - check YAAK_PLUGIN_RUNTIME env var, then fallback to development path
|
// Find the plugin runtime - check YAAK_PLUGIN_RUNTIME env var, then fallback to development path
|
||||||
let plugin_runtime_main =
|
let plugin_runtime_main = std::env::var("YAAK_PLUGIN_RUNTIME")
|
||||||
std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| {
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
// Development fallback: look relative to crate root
|
// Development fallback: look relative to crate root
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("../../crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs")
|
.join("../../crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs")
|
||||||
@@ -210,10 +226,14 @@ async fn main() {
|
|||||||
// Initialize plugins from database
|
// Initialize plugins from database
|
||||||
let plugins = db.list_plugins().unwrap_or_default();
|
let plugins = db.list_plugins().unwrap_or_default();
|
||||||
if !plugins.is_empty() {
|
if !plugins.is_empty() {
|
||||||
let errors =
|
let errors = plugin_manager
|
||||||
plugin_manager.initialize_all_plugins(plugins, &PluginContext::new_empty()).await;
|
.initialize_all_plugins(plugins, &PluginContext::new_empty())
|
||||||
|
.await;
|
||||||
for (plugin_dir, error_msg) in errors {
|
for (plugin_dir, error_msg) in errors {
|
||||||
eprintln!("Warning: Failed to initialize plugin '{}': {}", plugin_dir, error_msg);
|
eprintln!(
|
||||||
|
"Warning: Failed to initialize plugin '{}': {}",
|
||||||
|
plugin_dir, error_msg
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +249,9 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Requests { workspace_id } => {
|
Commands::Requests { workspace_id } => {
|
||||||
let requests = db.list_http_requests(&workspace_id).expect("Failed to list requests");
|
let requests = db
|
||||||
|
.list_http_requests(&workspace_id)
|
||||||
|
.expect("Failed to list requests");
|
||||||
if requests.is_empty() {
|
if requests.is_empty() {
|
||||||
println!("No requests found in workspace {}", workspace_id);
|
println!("No requests found in workspace {}", workspace_id);
|
||||||
} else {
|
} else {
|
||||||
@@ -239,7 +261,9 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Send { request_id } => {
|
Commands::Send { request_id } => {
|
||||||
let request = db.get_http_request(&request_id).expect("Failed to get request");
|
let request = db
|
||||||
|
.get_http_request(&request_id)
|
||||||
|
.expect("Failed to get request");
|
||||||
|
|
||||||
// Resolve environment chain for variable substitution
|
// Resolve environment chain for variable substitution
|
||||||
let environment_chain = db
|
let environment_chain = db
|
||||||
@@ -294,13 +318,18 @@ async fn main() {
|
|||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
// Drain events silently
|
// Drain events silently
|
||||||
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
tokio::spawn(async move {
|
||||||
|
while event_rx.recv().await.is_some() {}
|
||||||
|
});
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the request
|
// Send the request
|
||||||
let sender = ReqwestSender::new().expect("Failed to create HTTP client");
|
let sender = ReqwestSender::new().expect("Failed to create HTTP client");
|
||||||
let response = sender.send(sendable, event_tx).await.expect("Failed to send request");
|
let response = sender
|
||||||
|
.send(sendable, event_tx)
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
// Wait for event handler to finish
|
// Wait for event handler to finish
|
||||||
if let Some(handle) = verbose_handle {
|
if let Some(handle) = verbose_handle {
|
||||||
@@ -354,13 +383,18 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
tokio::spawn(async move {
|
||||||
|
while event_rx.recv().await.is_some() {}
|
||||||
|
});
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the request
|
// Send the request
|
||||||
let sender = ReqwestSender::new().expect("Failed to create HTTP client");
|
let sender = ReqwestSender::new().expect("Failed to create HTTP client");
|
||||||
let response = sender.send(sendable, event_tx).await.expect("Failed to send request");
|
let response = sender
|
||||||
|
.send(sendable, event_tx)
|
||||||
|
.await
|
||||||
|
.expect("Failed to send request");
|
||||||
|
|
||||||
if let Some(handle) = verbose_handle {
|
if let Some(handle) = verbose_handle {
|
||||||
let _ = handle.await;
|
let _ = handle.await;
|
||||||
@@ -387,7 +421,12 @@ async fn main() {
|
|||||||
let (body, _stats) = response.text().await.expect("Failed to read response body");
|
let (body, _stats) = response.text().await.expect("Failed to read response body");
|
||||||
println!("{}", body);
|
println!("{}", body);
|
||||||
}
|
}
|
||||||
Commands::Create { workspace_id, name, method, url } => {
|
Commands::Create {
|
||||||
|
workspace_id,
|
||||||
|
name,
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
} => {
|
||||||
let request = HttpRequest {
|
let request = HttpRequest {
|
||||||
workspace_id,
|
workspace_id,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<!-- Enable for NodeJS execution -->
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- Allow loading 1Password's dylib (signed with different Team ID) -->
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
<!-- Re-enable for sandboxing. Currently disabled because auto-updater doesn't work with sandboxing.-->
|
<!-- Re-enable for sandboxing. Currently disabled because auto-updater doesn't work with sandboxing.-->
|
||||||
<!-- <key>com.apple.security.app-sandbox</key> <true/>-->
|
<!-- <key>com.apple.security.app-sandbox</key> <true/>-->
|
||||||
<!-- <key>com.apple.security.files.user-selected.read-write</key> <true/>-->
|
<!-- <key>com.apple.security.files.user-selected.read-write</key> <true/>-->
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<!-- Enable for NodeJS/V8 JIT compiler -->
|
|
||||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
||||||
<true/>
|
|
||||||
|
|
||||||
<!-- Allow loading plugins signed with different Team IDs (e.g., 1Password) -->
|
|
||||||
<key>com.apple.security.cs.disable-library-validation</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
use crate::PluginContextExt;
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use crate::PluginContextExt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow, command};
|
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow, command};
|
||||||
|
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
use yaak_models::models::HttpRequestHeader;
|
|
||||||
use yaak_models::queries::workspaces::default_headers;
|
|
||||||
use yaak_plugins::events::GetThemesResponse;
|
use yaak_plugins::events::GetThemesResponse;
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::native_template_functions::{
|
use yaak_plugins::native_template_functions::{
|
||||||
@@ -22,6 +21,20 @@ impl<'a, R: Runtime, M: Manager<R>> EncryptionManagerExt<'a, R> for M {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub(crate) async fn cmd_show_workspace_key<R: Runtime>(
|
||||||
|
window: WebviewWindow<R>,
|
||||||
|
workspace_id: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let key = window.crypto().reveal_workspace_key(workspace_id)?;
|
||||||
|
window
|
||||||
|
.dialog()
|
||||||
|
.message(format!("Your workspace key is \n\n{}", key))
|
||||||
|
.kind(MessageDialogKind::Info)
|
||||||
|
.show(|_v| {});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub(crate) async fn cmd_decrypt_template<R: Runtime>(
|
pub(crate) async fn cmd_decrypt_template<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
@@ -41,12 +54,7 @@ pub(crate) async fn cmd_secure_template<R: Runtime>(
|
|||||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||||
let plugin_context = window.plugin_context();
|
let plugin_context = window.plugin_context();
|
||||||
Ok(encrypt_secure_template_function(
|
Ok(encrypt_secure_template_function(plugin_manager, encryption_manager, &plugin_context, template)?)
|
||||||
plugin_manager,
|
|
||||||
encryption_manager,
|
|
||||||
&plugin_context,
|
|
||||||
template,
|
|
||||||
)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -84,17 +92,3 @@ pub(crate) async fn cmd_set_workspace_key<R: Runtime>(
|
|||||||
window.crypto().set_human_key(workspace_id, key)?;
|
window.crypto().set_human_key(workspace_id, key)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub(crate) async fn cmd_disable_encryption<R: Runtime>(
|
|
||||||
window: WebviewWindow<R>,
|
|
||||||
workspace_id: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
window.crypto().disable_encryption(workspace_id)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub(crate) fn cmd_default_headers() -> Vec<HttpRequestHeader> {
|
|
||||||
default_headers()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,47 +6,33 @@ use crate::error::Result;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tauri::command;
|
use tauri::command;
|
||||||
use yaak_git::{
|
use yaak_git::{
|
||||||
BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult,
|
GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult,
|
||||||
PushResult, git_add, git_add_credential, git_add_remote, git_checkout_branch, git_clone,
|
git_add, git_add_credential, git_add_remote, git_checkout_branch, git_commit,
|
||||||
git_commit, git_create_branch, git_delete_branch, git_delete_remote_branch, git_fetch_all,
|
git_create_branch, git_delete_branch, git_fetch_all, git_init, git_log,
|
||||||
git_init, git_log, git_merge_branch, git_pull, git_pull_force_reset, git_pull_merge, git_push,
|
git_merge_branch, git_pull, git_push, git_remotes, git_rm_remote, git_status,
|
||||||
git_remotes, git_rename_branch, git_reset_changes, git_rm_remote, git_status, git_unstage,
|
git_unstage,
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOTE: All of these commands are async to prevent blocking work from locking up the UI
|
// NOTE: All of these commands are async to prevent blocking work from locking up the UI
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_git_checkout(dir: &Path, branch: &str, force: bool) -> Result<String> {
|
pub async fn cmd_git_checkout(dir: &Path, branch: &str, force: bool) -> Result<String> {
|
||||||
Ok(git_checkout_branch(dir, branch, force).await?)
|
Ok(git_checkout_branch(dir, branch, force)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_git_branch(dir: &Path, branch: &str, base: Option<&str>) -> Result<()> {
|
pub async fn cmd_git_branch(dir: &Path, branch: &str) -> Result<()> {
|
||||||
Ok(git_create_branch(dir, branch, base).await?)
|
Ok(git_create_branch(dir, branch)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_git_delete_branch(
|
pub async fn cmd_git_delete_branch(dir: &Path, branch: &str) -> Result<()> {
|
||||||
dir: &Path,
|
Ok(git_delete_branch(dir, branch)?)
|
||||||
branch: &str,
|
|
||||||
force: Option<bool>,
|
|
||||||
) -> Result<BranchDeleteResult> {
|
|
||||||
Ok(git_delete_branch(dir, branch, force.unwrap_or(false)).await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_git_delete_remote_branch(dir: &Path, branch: &str) -> Result<()> {
|
pub async fn cmd_git_merge_branch(dir: &Path, branch: &str, force: bool) -> Result<()> {
|
||||||
Ok(git_delete_remote_branch(dir, branch).await?)
|
Ok(git_merge_branch(dir, branch, force)?)
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn cmd_git_merge_branch(dir: &Path, branch: &str) -> Result<()> {
|
|
||||||
Ok(git_merge_branch(dir, branch).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn cmd_git_rename_branch(dir: &Path, old_name: &str, new_name: &str) -> Result<()> {
|
|
||||||
Ok(git_rename_branch(dir, old_name, new_name).await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -64,43 +50,24 @@ pub async fn cmd_git_initialize(dir: &Path) -> Result<()> {
|
|||||||
Ok(git_init(dir)?)
|
Ok(git_init(dir)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn cmd_git_clone(url: &str, dir: &Path) -> Result<CloneResult> {
|
|
||||||
Ok(git_clone(url, dir).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_git_commit(dir: &Path, message: &str) -> Result<()> {
|
pub async fn cmd_git_commit(dir: &Path, message: &str) -> Result<()> {
|
||||||
Ok(git_commit(dir, message).await?)
|
Ok(git_commit(dir, message)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_git_fetch_all(dir: &Path) -> Result<()> {
|
pub async fn cmd_git_fetch_all(dir: &Path) -> Result<()> {
|
||||||
Ok(git_fetch_all(dir).await?)
|
Ok(git_fetch_all(dir)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_git_push(dir: &Path) -> Result<PushResult> {
|
pub async fn cmd_git_push(dir: &Path) -> Result<PushResult> {
|
||||||
Ok(git_push(dir).await?)
|
Ok(git_push(dir)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_git_pull(dir: &Path) -> Result<PullResult> {
|
pub async fn cmd_git_pull(dir: &Path) -> Result<PullResult> {
|
||||||
Ok(git_pull(dir).await?)
|
Ok(git_pull(dir)?)
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn cmd_git_pull_force_reset(
|
|
||||||
dir: &Path,
|
|
||||||
remote: &str,
|
|
||||||
branch: &str,
|
|
||||||
) -> Result<PullResult> {
|
|
||||||
Ok(git_pull_force_reset(dir, remote, branch).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn cmd_git_pull_merge(dir: &Path, remote: &str, branch: &str) -> Result<PullResult> {
|
|
||||||
Ok(git_pull_merge(dir, remote, branch).await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -119,18 +86,14 @@ pub async fn cmd_git_unstage(dir: &Path, rela_paths: Vec<PathBuf>) -> Result<()>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn cmd_git_reset_changes(dir: &Path) -> Result<()> {
|
|
||||||
Ok(git_reset_changes(dir).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_git_add_credential(
|
pub async fn cmd_git_add_credential(
|
||||||
|
dir: &Path,
|
||||||
remote_url: &str,
|
remote_url: &str,
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
Ok(git_add_credential(remote_url, username, password).await?)
|
Ok(git_add_credential(dir, remote_url, username, password).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crate::PluginContextExt;
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models_ext::QueryManagerExt;
|
use crate::PluginContextExt;
|
||||||
use KeyAndValueRef::{Ascii, Binary};
|
use KeyAndValueRef::{Ascii, Binary};
|
||||||
use tauri::{Manager, Runtime, WebviewWindow};
|
use tauri::{Manager, Runtime, WebviewWindow};
|
||||||
use yaak_grpc::{KeyAndValueRef, MetadataMap};
|
use yaak_grpc::{KeyAndValueRef, MetadataMap};
|
||||||
use yaak_models::models::GrpcRequest;
|
use yaak_models::models::GrpcRequest;
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader};
|
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use tauri::{AppHandle, Runtime};
|
use tauri::{AppHandle, Runtime};
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
const NAMESPACE: &str = "analytics";
|
const NAMESPACE: &str = "analytics";
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
use crate::PluginContextExt;
|
|
||||||
use crate::error::Error::GenericError;
|
use crate::error::Error::GenericError;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models_ext::BlobManagerExt;
|
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use crate::render::render_http_request;
|
use crate::render::render_http_request;
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicI32, Ordering};
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tauri::{AppHandle, Manager, Runtime, WebviewWindow};
|
use tauri::{AppHandle, Manager, Runtime, WebviewWindow};
|
||||||
use tokio::fs::{File, create_dir_all};
|
use tokio::fs::{File, create_dir_all};
|
||||||
@@ -19,19 +15,22 @@ use yaak_http::client::{
|
|||||||
HttpConnectionOptions, HttpConnectionProxySetting, HttpConnectionProxySettingAuth,
|
HttpConnectionOptions, HttpConnectionProxySetting, HttpConnectionProxySettingAuth,
|
||||||
};
|
};
|
||||||
use yaak_http::cookies::CookieStore;
|
use yaak_http::cookies::CookieStore;
|
||||||
use yaak_http::manager::{CachedClient, HttpConnectionManager};
|
use yaak_http::manager::HttpConnectionManager;
|
||||||
use yaak_http::sender::ReqwestSender;
|
use yaak_http::sender::ReqwestSender;
|
||||||
use yaak_http::tee_reader::TeeReader;
|
use yaak_http::tee_reader::TeeReader;
|
||||||
use yaak_http::transaction::HttpTransaction;
|
use yaak_http::transaction::HttpTransaction;
|
||||||
use yaak_http::types::{
|
use yaak_http::types::{
|
||||||
SendableBody, SendableHttpRequest, SendableHttpRequestOptions, append_query_params,
|
SendableBody, SendableHttpRequest, SendableHttpRequestOptions, append_query_params,
|
||||||
};
|
};
|
||||||
|
use crate::models_ext::BlobManagerExt;
|
||||||
use yaak_models::blob_manager::BodyChunk;
|
use yaak_models::blob_manager::BodyChunk;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseHeader,
|
CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseHeader,
|
||||||
HttpResponseState, ProxySetting, ProxySettingAuth,
|
HttpResponseState, ProxySetting, ProxySettingAuth,
|
||||||
};
|
};
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
use crate::PluginContextExt;
|
||||||
use yaak_plugins::events::{
|
use yaak_plugins::events::{
|
||||||
CallHttpAuthenticationRequest, HttpHeader, PluginContext, RenderPurpose,
|
CallHttpAuthenticationRequest, HttpHeader, PluginContext, RenderPurpose,
|
||||||
};
|
};
|
||||||
@@ -174,22 +173,10 @@ async fn send_http_request_inner<R: Runtime>(
|
|||||||
let environment_id = environment.map(|e| e.id);
|
let environment_id = environment.map(|e| e.id);
|
||||||
let workspace = window.db().get_workspace(workspace_id)?;
|
let workspace = window.db().get_workspace(workspace_id)?;
|
||||||
let (resolved, auth_context_id) = resolve_http_request(window, unrendered_request)?;
|
let (resolved, auth_context_id) = resolve_http_request(window, unrendered_request)?;
|
||||||
let cb = PluginTemplateCallback::new(
|
let cb = PluginTemplateCallback::new(plugin_manager.clone(), encryption_manager.clone(), &plugin_context, RenderPurpose::Send);
|
||||||
plugin_manager.clone(),
|
|
||||||
encryption_manager.clone(),
|
|
||||||
&plugin_context,
|
|
||||||
RenderPurpose::Send,
|
|
||||||
);
|
|
||||||
let env_chain =
|
let env_chain =
|
||||||
window.db().resolve_environments(&workspace.id, folder_id, environment_id.as_deref())?;
|
window.db().resolve_environments(&workspace.id, folder_id, environment_id.as_deref())?;
|
||||||
let mut cancel_rx = cancelled_rx.clone();
|
let request = render_http_request(&resolved, env_chain, &cb, &RenderOptions::throw()).await?;
|
||||||
let render_options = RenderOptions::throw();
|
|
||||||
let request = tokio::select! {
|
|
||||||
result = render_http_request(&resolved, env_chain, &cb, &render_options) => result?,
|
|
||||||
_ = cancel_rx.changed() => {
|
|
||||||
return Err(GenericError("Request canceled".to_string()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build the sendable request using the new SendableHttpRequest type
|
// Build the sendable request using the new SendableHttpRequest type
|
||||||
let options = SendableHttpRequestOptions {
|
let options = SendableHttpRequestOptions {
|
||||||
@@ -241,36 +228,29 @@ async fn send_http_request_inner<R: Runtime>(
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cached_client = connection_manager
|
let client = connection_manager
|
||||||
.get_client(&HttpConnectionOptions {
|
.get_client(&HttpConnectionOptions {
|
||||||
id: plugin_context.id.clone(),
|
id: plugin_context.id.clone(),
|
||||||
validate_certificates: workspace.setting_validate_certificates,
|
validate_certificates: workspace.setting_validate_certificates,
|
||||||
proxy: proxy_setting,
|
proxy: proxy_setting,
|
||||||
client_certificate,
|
client_certificate,
|
||||||
dns_overrides: workspace.setting_dns_overrides.clone(),
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Apply authentication to the request, racing against cancellation since
|
// Apply authentication to the request
|
||||||
// auth plugins (e.g. OAuth2) can block indefinitely waiting for user action.
|
apply_authentication(
|
||||||
let mut cancel_rx = cancelled_rx.clone();
|
&window,
|
||||||
tokio::select! {
|
&mut sendable_request,
|
||||||
result = apply_authentication(
|
&request,
|
||||||
&window,
|
auth_context_id,
|
||||||
&mut sendable_request,
|
&plugin_manager,
|
||||||
&request,
|
plugin_context,
|
||||||
auth_context_id,
|
)
|
||||||
&plugin_manager,
|
.await?;
|
||||||
plugin_context,
|
|
||||||
) => result?,
|
|
||||||
_ = cancel_rx.changed() => {
|
|
||||||
return Err(GenericError("Request canceled".to_string()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let cookie_store = maybe_cookie_store.as_ref().map(|(cs, _)| cs.clone());
|
let cookie_store = maybe_cookie_store.as_ref().map(|(cs, _)| cs.clone());
|
||||||
let result = execute_transaction(
|
let result = execute_transaction(
|
||||||
cached_client,
|
client,
|
||||||
sendable_request,
|
sendable_request,
|
||||||
response_ctx,
|
response_ctx,
|
||||||
cancelled_rx.clone(),
|
cancelled_rx.clone(),
|
||||||
@@ -330,7 +310,7 @@ pub fn resolve_http_request<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_transaction<R: Runtime>(
|
async fn execute_transaction<R: Runtime>(
|
||||||
cached_client: CachedClient,
|
client: reqwest::Client,
|
||||||
mut sendable_request: SendableHttpRequest,
|
mut sendable_request: SendableHttpRequest,
|
||||||
response_ctx: &mut ResponseContext<R>,
|
response_ctx: &mut ResponseContext<R>,
|
||||||
mut cancelled_rx: Receiver<bool>,
|
mut cancelled_rx: Receiver<bool>,
|
||||||
@@ -341,10 +321,7 @@ async fn execute_transaction<R: Runtime>(
|
|||||||
let workspace_id = response_ctx.response().workspace_id.clone();
|
let workspace_id = response_ctx.response().workspace_id.clone();
|
||||||
let is_persisted = response_ctx.is_persisted();
|
let is_persisted = response_ctx.is_persisted();
|
||||||
|
|
||||||
// Keep a reference to the resolver for DNS timing events
|
let sender = ReqwestSender::with_client(client);
|
||||||
let resolver = cached_client.resolver.clone();
|
|
||||||
|
|
||||||
let sender = ReqwestSender::with_client(cached_client.client);
|
|
||||||
let transaction = match cookie_store {
|
let transaction = match cookie_store {
|
||||||
Some(cs) => HttpTransaction::with_cookie_store(sender, cs),
|
Some(cs) => HttpTransaction::with_cookie_store(sender, cs),
|
||||||
None => HttpTransaction::new(sender),
|
None => HttpTransaction::new(sender),
|
||||||
@@ -369,39 +346,21 @@ async fn execute_transaction<R: Runtime>(
|
|||||||
let (event_tx, mut event_rx) =
|
let (event_tx, mut event_rx) =
|
||||||
tokio::sync::mpsc::channel::<yaak_http::sender::HttpResponseEvent>(100);
|
tokio::sync::mpsc::channel::<yaak_http::sender::HttpResponseEvent>(100);
|
||||||
|
|
||||||
// Set the event sender on the DNS resolver so it can emit DNS timing events
|
|
||||||
resolver.set_event_sender(Some(event_tx.clone())).await;
|
|
||||||
|
|
||||||
// Shared state to capture DNS timing from the event processing task
|
|
||||||
let dns_elapsed = Arc::new(AtomicI32::new(0));
|
|
||||||
|
|
||||||
// Write events to DB in a task (only for persisted responses)
|
// Write events to DB in a task (only for persisted responses)
|
||||||
if is_persisted {
|
if is_persisted {
|
||||||
let response_id = response_id.clone();
|
let response_id = response_id.clone();
|
||||||
let app_handle = app_handle.clone();
|
let app_handle = app_handle.clone();
|
||||||
let update_source = response_ctx.update_source.clone();
|
let update_source = response_ctx.update_source.clone();
|
||||||
let workspace_id = workspace_id.clone();
|
let workspace_id = workspace_id.clone();
|
||||||
let dns_elapsed = dns_elapsed.clone();
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(event) = event_rx.recv().await {
|
while let Some(event) = event_rx.recv().await {
|
||||||
// Capture DNS timing when we see a DNS event
|
|
||||||
if let yaak_http::sender::HttpResponseEvent::DnsResolved { duration, .. } = &event {
|
|
||||||
dns_elapsed.store(*duration as i32, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
let db_event = HttpResponseEvent::new(&response_id, &workspace_id, event.into());
|
let db_event = HttpResponseEvent::new(&response_id, &workspace_id, event.into());
|
||||||
let _ = app_handle.db().upsert_http_response_event(&db_event, &update_source);
|
let _ = app_handle.db().upsert_http_response_event(&db_event, &update_source);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// For ephemeral responses, just drain the events but still capture DNS timing
|
// For ephemeral responses, just drain the events
|
||||||
let dns_elapsed = dns_elapsed.clone();
|
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
||||||
tokio::spawn(async move {
|
|
||||||
while let Some(event) = event_rx.recv().await {
|
|
||||||
if let yaak_http::sender::HttpResponseEvent::DnsResolved { duration, .. } = &event {
|
|
||||||
dns_elapsed.store(*duration as i32, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Capture request body as it's sent (only for persisted responses)
|
// Capture request body as it's sent (only for persisted responses)
|
||||||
@@ -569,14 +528,10 @@ async fn execute_transaction<R: Runtime>(
|
|||||||
// Final update with closed state and accurate byte count
|
// Final update with closed state and accurate byte count
|
||||||
response_ctx.update(|r| {
|
response_ctx.update(|r| {
|
||||||
r.elapsed = start.elapsed().as_millis() as i32;
|
r.elapsed = start.elapsed().as_millis() as i32;
|
||||||
r.elapsed_dns = dns_elapsed.load(Ordering::SeqCst);
|
|
||||||
r.content_length = Some(written_bytes as i32);
|
r.content_length = Some(written_bytes as i32);
|
||||||
r.state = HttpResponseState::Closed;
|
r.state = HttpResponseState::Closed;
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Clear the event sender from the resolver since this request is done
|
|
||||||
resolver.set_event_sender(None).await;
|
|
||||||
|
|
||||||
Ok((response_ctx.response().clone(), maybe_blob_write_handle))
|
Ok((response_ctx.response().clone(), maybe_blob_write_handle))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
use crate::PluginContextExt;
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models_ext::QueryManagerExt;
|
use crate::models_ext::QueryManagerExt;
|
||||||
|
use crate::PluginContextExt;
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
use tauri::{Manager, Runtime, WebviewWindow};
|
use tauri::{Manager, Runtime, WebviewWindow};
|
||||||
|
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
||||||
use yaak_core::WorkspaceContext;
|
use yaak_core::WorkspaceContext;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace,
|
Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace,
|
||||||
};
|
};
|
||||||
use yaak_models::util::{BatchUpsertResult, UpdateSource, maybe_gen_id, maybe_gen_id_opt};
|
use yaak_models::util::{BatchUpsertResult, UpdateSource, maybe_gen_id, maybe_gen_id_opt};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
|
||||||
|
|
||||||
pub(crate) async fn import_data<R: Runtime>(
|
pub(crate) async fn import_data<R: Runtime>(
|
||||||
window: &WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::http_request::{resolve_http_request, send_http_request};
|
|||||||
use crate::import::import_data;
|
use crate::import::import_data;
|
||||||
use crate::models_ext::{BlobManagerExt, QueryManagerExt};
|
use crate::models_ext::{BlobManagerExt, QueryManagerExt};
|
||||||
use crate::notifications::YaakNotifier;
|
use crate::notifications::YaakNotifier;
|
||||||
use crate::render::{render_grpc_request, render_json_value, render_template};
|
use crate::render::{render_grpc_request, render_template};
|
||||||
use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
|
use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
|
||||||
use crate::uri_scheme::handle_deep_link;
|
use crate::uri_scheme::handle_deep_link;
|
||||||
use error::Result as YaakResult;
|
use error::Result as YaakResult;
|
||||||
@@ -37,8 +37,8 @@ use yaak_grpc::{Code, ServiceDefinition, serialize_message};
|
|||||||
use yaak_mac_window::AppHandleMacWindowExt;
|
use yaak_mac_window::AppHandleMacWindowExt;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
|
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
|
||||||
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Plugin,
|
GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState,
|
||||||
Workspace, WorkspaceMeta,
|
Plugin, Workspace, WorkspaceMeta,
|
||||||
};
|
};
|
||||||
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
|
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
|
||||||
use yaak_plugins::events::{
|
use yaak_plugins::events::{
|
||||||
@@ -101,7 +101,6 @@ struct AppMetaData {
|
|||||||
app_data_dir: String,
|
app_data_dir: String,
|
||||||
app_log_dir: String,
|
app_log_dir: String,
|
||||||
vendored_plugin_dir: String,
|
vendored_plugin_dir: String,
|
||||||
default_project_dir: String,
|
|
||||||
feature_updater: bool,
|
feature_updater: bool,
|
||||||
feature_license: bool,
|
feature_license: bool,
|
||||||
}
|
}
|
||||||
@@ -112,7 +111,6 @@ async fn cmd_metadata(app_handle: AppHandle) -> YaakResult<AppMetaData> {
|
|||||||
let app_log_dir = app_handle.path().app_log_dir()?;
|
let app_log_dir = app_handle.path().app_log_dir()?;
|
||||||
let vendored_plugin_dir =
|
let vendored_plugin_dir =
|
||||||
app_handle.path().resolve("vendored/plugins", BaseDirectory::Resource)?;
|
app_handle.path().resolve("vendored/plugins", BaseDirectory::Resource)?;
|
||||||
let default_project_dir = app_handle.path().home_dir()?.join("YaakProjects");
|
|
||||||
Ok(AppMetaData {
|
Ok(AppMetaData {
|
||||||
is_dev: is_dev(),
|
is_dev: is_dev(),
|
||||||
version: app_handle.package_info().version.to_string(),
|
version: app_handle.package_info().version.to_string(),
|
||||||
@@ -120,7 +118,6 @@ async fn cmd_metadata(app_handle: AppHandle) -> YaakResult<AppMetaData> {
|
|||||||
app_data_dir: app_data_dir.to_string_lossy().to_string(),
|
app_data_dir: app_data_dir.to_string_lossy().to_string(),
|
||||||
app_log_dir: app_log_dir.to_string_lossy().to_string(),
|
app_log_dir: app_log_dir.to_string_lossy().to_string(),
|
||||||
vendored_plugin_dir: vendored_plugin_dir.to_string_lossy().to_string(),
|
vendored_plugin_dir: vendored_plugin_dir.to_string_lossy().to_string(),
|
||||||
default_project_dir: default_project_dir.to_string_lossy().to_string(),
|
|
||||||
feature_license: cfg!(feature = "license"),
|
feature_license: cfg!(feature = "license"),
|
||||||
feature_updater: cfg!(feature = "updater"),
|
feature_updater: cfg!(feature = "updater"),
|
||||||
})
|
})
|
||||||
@@ -192,6 +189,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
|||||||
request_id: &str,
|
request_id: &str,
|
||||||
environment_id: Option<&str>,
|
environment_id: Option<&str>,
|
||||||
proto_files: Vec<String>,
|
proto_files: Vec<String>,
|
||||||
|
skip_cache: Option<bool>,
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||||
@@ -226,21 +224,18 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
|||||||
let settings = window.db().get_settings();
|
let settings = window.db().get_settings();
|
||||||
let client_certificate =
|
let client_certificate =
|
||||||
find_client_certificate(req.url.as_str(), &settings.client_certificates);
|
find_client_certificate(req.url.as_str(), &settings.client_certificates);
|
||||||
let proto_files: Vec<PathBuf> =
|
|
||||||
proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect();
|
|
||||||
|
|
||||||
// Always invalidate cached pool when this command is called, to force re-reflection
|
Ok(grpc_handle
|
||||||
let mut handle = grpc_handle.lock().await;
|
.lock()
|
||||||
handle.invalidate_pool(&req.id, &uri, &proto_files);
|
.await
|
||||||
|
|
||||||
Ok(handle
|
|
||||||
.services(
|
.services(
|
||||||
&req.id,
|
&req.id,
|
||||||
&uri,
|
&uri,
|
||||||
&proto_files,
|
&proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(),
|
||||||
&metadata,
|
&metadata,
|
||||||
workspace.setting_validate_certificates,
|
workspace.setting_validate_certificates,
|
||||||
client_certificate,
|
client_certificate,
|
||||||
|
skip_cache.unwrap_or(false),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| GenericError(e.to_string()))?)
|
.map_err(|e| GenericError(e.to_string()))?)
|
||||||
@@ -365,8 +360,10 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
|
|
||||||
let cb = {
|
let cb = {
|
||||||
let cancelled_rx = cancelled_rx.clone();
|
let cancelled_rx = cancelled_rx.clone();
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
let environment_chain = environment_chain.clone();
|
let environment_chain = environment_chain.clone();
|
||||||
let window = window.clone();
|
let window = window.clone();
|
||||||
|
let base_msg = base_msg.clone();
|
||||||
let plugin_manager = plugin_manager.clone();
|
let plugin_manager = plugin_manager.clone();
|
||||||
let encryption_manager = encryption_manager.clone();
|
let encryption_manager = encryption_manager.clone();
|
||||||
|
|
||||||
@@ -388,12 +385,14 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
match serde_json::from_str::<IncomingMsg>(ev.payload()) {
|
match serde_json::from_str::<IncomingMsg>(ev.payload()) {
|
||||||
Ok(IncomingMsg::Message(msg)) => {
|
Ok(IncomingMsg::Message(msg)) => {
|
||||||
let window = window.clone();
|
let window = window.clone();
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
|
let base_msg = base_msg.clone();
|
||||||
let environment_chain = environment_chain.clone();
|
let environment_chain = environment_chain.clone();
|
||||||
let plugin_manager = plugin_manager.clone();
|
let plugin_manager = plugin_manager.clone();
|
||||||
let encryption_manager = encryption_manager.clone();
|
let encryption_manager = encryption_manager.clone();
|
||||||
let msg = block_in_place(|| {
|
let msg = block_in_place(|| {
|
||||||
tauri::async_runtime::block_on(async {
|
tauri::async_runtime::block_on(async {
|
||||||
let result = render_template(
|
render_template(
|
||||||
msg.as_str(),
|
msg.as_str(),
|
||||||
environment_chain,
|
environment_chain,
|
||||||
&PluginTemplateCallback::new(
|
&PluginTemplateCallback::new(
|
||||||
@@ -407,11 +406,24 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
),
|
),
|
||||||
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
|
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
result.expect("Failed to render template")
|
.expect("Failed to render template")
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
in_msg_tx.try_send(msg.clone()).unwrap();
|
in_msg_tx.try_send(msg.clone()).unwrap();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
app_handle
|
||||||
|
.db()
|
||||||
|
.upsert_grpc_event(
|
||||||
|
&GrpcEvent {
|
||||||
|
content: msg,
|
||||||
|
event_type: GrpcEventType::ClientMessage,
|
||||||
|
..base_msg.clone()
|
||||||
|
},
|
||||||
|
&UpdateSource::from_window_label(window.label()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Ok(IncomingMsg::Commit) => {
|
Ok(IncomingMsg::Commit) => {
|
||||||
maybe_in_msg_tx.take();
|
maybe_in_msg_tx.take();
|
||||||
@@ -458,48 +470,12 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// Create callback for streaming methods that handles both success and error
|
|
||||||
let on_message = {
|
|
||||||
let app_handle = app_handle.clone();
|
|
||||||
let base_event = base_event.clone();
|
|
||||||
let window_label = window.label().to_string();
|
|
||||||
move |result: std::result::Result<String, String>| match result {
|
|
||||||
Ok(msg) => {
|
|
||||||
let _ = app_handle.db().upsert_grpc_event(
|
|
||||||
&GrpcEvent {
|
|
||||||
content: msg,
|
|
||||||
event_type: GrpcEventType::ClientMessage,
|
|
||||||
..base_event.clone()
|
|
||||||
},
|
|
||||||
&UpdateSource::from_window_label(&window_label),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
let _ = app_handle.db().upsert_grpc_event(
|
|
||||||
&GrpcEvent {
|
|
||||||
content: format!("Failed to send message: {}", error),
|
|
||||||
event_type: GrpcEventType::Error,
|
|
||||||
..base_event.clone()
|
|
||||||
},
|
|
||||||
&UpdateSource::from_window_label(&window_label),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (maybe_stream, maybe_msg) =
|
let (maybe_stream, maybe_msg) =
|
||||||
match (method_desc.is_client_streaming(), method_desc.is_server_streaming()) {
|
match (method_desc.is_client_streaming(), method_desc.is_server_streaming()) {
|
||||||
(true, true) => (
|
(true, true) => (
|
||||||
Some(
|
Some(
|
||||||
connection
|
connection
|
||||||
.streaming(
|
.streaming(&service, &method, in_msg_stream, &metadata, client_cert)
|
||||||
&service,
|
|
||||||
&method,
|
|
||||||
in_msg_stream,
|
|
||||||
&metadata,
|
|
||||||
client_cert,
|
|
||||||
on_message.clone(),
|
|
||||||
)
|
|
||||||
.await,
|
.await,
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
@@ -514,7 +490,6 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
in_msg_stream,
|
in_msg_stream,
|
||||||
&metadata,
|
&metadata,
|
||||||
client_cert,
|
client_cert,
|
||||||
on_message.clone(),
|
|
||||||
)
|
)
|
||||||
.await,
|
.await,
|
||||||
),
|
),
|
||||||
@@ -1060,54 +1035,14 @@ async fn cmd_get_http_authentication_summaries<R: Runtime>(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_get_http_authentication_config<R: Runtime>(
|
async fn cmd_get_http_authentication_config<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
app_handle: AppHandle<R>,
|
|
||||||
plugin_manager: State<'_, PluginManager>,
|
plugin_manager: State<'_, PluginManager>,
|
||||||
encryption_manager: State<'_, EncryptionManager>,
|
|
||||||
auth_name: &str,
|
auth_name: &str,
|
||||||
values: HashMap<String, JsonPrimitive>,
|
values: HashMap<String, JsonPrimitive>,
|
||||||
model: AnyModel,
|
model: AnyModel,
|
||||||
environment_id: Option<&str>,
|
_environment_id: Option<&str>,
|
||||||
) -> YaakResult<GetHttpAuthenticationConfigResponse> {
|
) -> YaakResult<GetHttpAuthenticationConfigResponse> {
|
||||||
// Extract workspace_id and folder_id from the model to resolve the environment chain
|
|
||||||
let (workspace_id, folder_id) = match &model {
|
|
||||||
AnyModel::HttpRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
|
||||||
AnyModel::GrpcRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
|
||||||
AnyModel::WebsocketRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
|
||||||
AnyModel::Folder(f) => (f.workspace_id.clone(), f.folder_id.clone()),
|
|
||||||
AnyModel::Workspace(w) => (w.id.clone(), None),
|
|
||||||
_ => return Err(GenericError("Unsupported model type for authentication config".into())),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resolve environment chain and render the values for token lookup
|
|
||||||
let environment_chain = app_handle.db().resolve_environments(
|
|
||||||
&workspace_id,
|
|
||||||
folder_id.as_deref(),
|
|
||||||
environment_id,
|
|
||||||
)?;
|
|
||||||
let plugin_manager_arc = Arc::new((*plugin_manager).clone());
|
|
||||||
let encryption_manager_arc = Arc::new((*encryption_manager).clone());
|
|
||||||
let cb = PluginTemplateCallback::new(
|
|
||||||
plugin_manager_arc,
|
|
||||||
encryption_manager_arc,
|
|
||||||
&window.plugin_context(),
|
|
||||||
RenderPurpose::Preview,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Convert HashMap<String, JsonPrimitive> to serde_json::Value for rendering
|
|
||||||
let values_json: serde_json::Value = serde_json::to_value(&values)?;
|
|
||||||
let rendered_json =
|
|
||||||
render_json_value(values_json, environment_chain, &cb, &RenderOptions::throw()).await?;
|
|
||||||
|
|
||||||
// Convert back to HashMap<String, JsonPrimitive>
|
|
||||||
let rendered_values: HashMap<String, JsonPrimitive> = serde_json::from_value(rendered_json)?;
|
|
||||||
|
|
||||||
Ok(plugin_manager
|
Ok(plugin_manager
|
||||||
.get_http_authentication_config(
|
.get_http_authentication_config(&window.plugin_context(), auth_name, values, model.id())
|
||||||
&window.plugin_context(),
|
|
||||||
auth_name,
|
|
||||||
rendered_values,
|
|
||||||
model.id(),
|
|
||||||
)
|
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1154,54 +1089,19 @@ async fn cmd_call_grpc_request_action<R: Runtime>(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_call_http_authentication_action<R: Runtime>(
|
async fn cmd_call_http_authentication_action<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
app_handle: AppHandle<R>,
|
|
||||||
plugin_manager: State<'_, PluginManager>,
|
plugin_manager: State<'_, PluginManager>,
|
||||||
encryption_manager: State<'_, EncryptionManager>,
|
|
||||||
auth_name: &str,
|
auth_name: &str,
|
||||||
action_index: i32,
|
action_index: i32,
|
||||||
values: HashMap<String, JsonPrimitive>,
|
values: HashMap<String, JsonPrimitive>,
|
||||||
model: AnyModel,
|
model: AnyModel,
|
||||||
environment_id: Option<&str>,
|
_environment_id: Option<&str>,
|
||||||
) -> YaakResult<()> {
|
) -> YaakResult<()> {
|
||||||
// Extract workspace_id and folder_id from the model to resolve the environment chain
|
|
||||||
let (workspace_id, folder_id) = match &model {
|
|
||||||
AnyModel::HttpRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
|
||||||
AnyModel::GrpcRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
|
||||||
AnyModel::WebsocketRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()),
|
|
||||||
AnyModel::Folder(f) => (f.workspace_id.clone(), f.folder_id.clone()),
|
|
||||||
AnyModel::Workspace(w) => (w.id.clone(), None),
|
|
||||||
_ => return Err(GenericError("Unsupported model type for authentication action".into())),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resolve environment chain and render the values
|
|
||||||
let environment_chain = app_handle.db().resolve_environments(
|
|
||||||
&workspace_id,
|
|
||||||
folder_id.as_deref(),
|
|
||||||
environment_id,
|
|
||||||
)?;
|
|
||||||
let plugin_manager_arc = Arc::new((*plugin_manager).clone());
|
|
||||||
let encryption_manager_arc = Arc::new((*encryption_manager).clone());
|
|
||||||
let cb = PluginTemplateCallback::new(
|
|
||||||
plugin_manager_arc,
|
|
||||||
encryption_manager_arc,
|
|
||||||
&window.plugin_context(),
|
|
||||||
RenderPurpose::Send,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Convert HashMap<String, JsonPrimitive> to serde_json::Value for rendering
|
|
||||||
let values_json: serde_json::Value = serde_json::to_value(&values)?;
|
|
||||||
let rendered_json =
|
|
||||||
render_json_value(values_json, environment_chain, &cb, &RenderOptions::throw()).await?;
|
|
||||||
|
|
||||||
// Convert back to HashMap<String, JsonPrimitive>
|
|
||||||
let rendered_values: HashMap<String, JsonPrimitive> = serde_json::from_value(rendered_json)?;
|
|
||||||
|
|
||||||
Ok(plugin_manager
|
Ok(plugin_manager
|
||||||
.call_http_authentication_action(
|
.call_http_authentication_action(
|
||||||
&window.plugin_context(),
|
&window.plugin_context(),
|
||||||
auth_name,
|
auth_name,
|
||||||
action_index,
|
action_index,
|
||||||
rendered_values,
|
values,
|
||||||
&model.id(),
|
&model.id(),
|
||||||
)
|
)
|
||||||
.await?)
|
.await?)
|
||||||
@@ -1271,6 +1171,35 @@ async fn cmd_save_response<R: Runtime>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn cmd_send_folder<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
window: WebviewWindow<R>,
|
||||||
|
environment_id: Option<String>,
|
||||||
|
cookie_jar_id: Option<String>,
|
||||||
|
folder_id: &str,
|
||||||
|
) -> YaakResult<()> {
|
||||||
|
let requests = app_handle.db().list_http_requests_for_folder_recursive(folder_id)?;
|
||||||
|
for request in requests {
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
|
let window = window.clone();
|
||||||
|
let environment_id = environment_id.clone();
|
||||||
|
let cookie_jar_id = cookie_jar_id.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = cmd_send_http_request(
|
||||||
|
app_handle,
|
||||||
|
window,
|
||||||
|
environment_id.as_deref(),
|
||||||
|
cookie_jar_id.as_deref(),
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_send_http_request<R: Runtime>(
|
async fn cmd_send_http_request<R: Runtime>(
|
||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
@@ -1367,6 +1296,27 @@ async fn cmd_install_plugin<R: Runtime>(
|
|||||||
Ok(plugin)
|
Ok(plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn cmd_create_grpc_request<R: Runtime>(
|
||||||
|
workspace_id: &str,
|
||||||
|
name: &str,
|
||||||
|
sort_priority: f64,
|
||||||
|
folder_id: Option<&str>,
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
window: WebviewWindow<R>,
|
||||||
|
) -> YaakResult<GrpcRequest> {
|
||||||
|
Ok(app_handle.db().upsert_grpc_request(
|
||||||
|
&GrpcRequest {
|
||||||
|
workspace_id: workspace_id.to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
folder_id: folder_id.map(|s| s.to_string()),
|
||||||
|
sort_priority,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&UpdateSource::from_window_label(window.label()),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_reload_plugins<R: Runtime>(
|
async fn cmd_reload_plugins<R: Runtime>(
|
||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
@@ -1629,6 +1579,7 @@ pub fn run() {
|
|||||||
cmd_call_folder_action,
|
cmd_call_folder_action,
|
||||||
cmd_call_grpc_request_action,
|
cmd_call_grpc_request_action,
|
||||||
cmd_check_for_updates,
|
cmd_check_for_updates,
|
||||||
|
cmd_create_grpc_request,
|
||||||
cmd_curl_to_request,
|
cmd_curl_to_request,
|
||||||
cmd_delete_all_grpc_connections,
|
cmd_delete_all_grpc_connections,
|
||||||
cmd_delete_all_http_responses,
|
cmd_delete_all_http_responses,
|
||||||
@@ -1662,6 +1613,7 @@ pub fn run() {
|
|||||||
cmd_save_response,
|
cmd_save_response,
|
||||||
cmd_send_ephemeral_request,
|
cmd_send_ephemeral_request,
|
||||||
cmd_send_http_request,
|
cmd_send_http_request,
|
||||||
|
cmd_send_folder,
|
||||||
cmd_template_function_config,
|
cmd_template_function_config,
|
||||||
cmd_template_function_summaries,
|
cmd_template_function_summaries,
|
||||||
cmd_template_tokens_to_string,
|
cmd_template_tokens_to_string,
|
||||||
@@ -1669,13 +1621,12 @@ pub fn run() {
|
|||||||
//
|
//
|
||||||
// Migrated commands
|
// Migrated commands
|
||||||
crate::commands::cmd_decrypt_template,
|
crate::commands::cmd_decrypt_template,
|
||||||
crate::commands::cmd_default_headers,
|
|
||||||
crate::commands::cmd_disable_encryption,
|
|
||||||
crate::commands::cmd_enable_encryption,
|
crate::commands::cmd_enable_encryption,
|
||||||
crate::commands::cmd_get_themes,
|
crate::commands::cmd_get_themes,
|
||||||
crate::commands::cmd_reveal_workspace_key,
|
crate::commands::cmd_reveal_workspace_key,
|
||||||
crate::commands::cmd_secure_template,
|
crate::commands::cmd_secure_template,
|
||||||
crate::commands::cmd_set_workspace_key,
|
crate::commands::cmd_set_workspace_key,
|
||||||
|
crate::commands::cmd_show_workspace_key,
|
||||||
//
|
//
|
||||||
// Models commands
|
// Models commands
|
||||||
models_ext::models_delete,
|
models_ext::models_delete,
|
||||||
@@ -1698,36 +1649,30 @@ pub fn run() {
|
|||||||
git_ext::cmd_git_checkout,
|
git_ext::cmd_git_checkout,
|
||||||
git_ext::cmd_git_branch,
|
git_ext::cmd_git_branch,
|
||||||
git_ext::cmd_git_delete_branch,
|
git_ext::cmd_git_delete_branch,
|
||||||
git_ext::cmd_git_delete_remote_branch,
|
|
||||||
git_ext::cmd_git_merge_branch,
|
git_ext::cmd_git_merge_branch,
|
||||||
git_ext::cmd_git_rename_branch,
|
|
||||||
git_ext::cmd_git_status,
|
git_ext::cmd_git_status,
|
||||||
git_ext::cmd_git_log,
|
git_ext::cmd_git_log,
|
||||||
git_ext::cmd_git_initialize,
|
git_ext::cmd_git_initialize,
|
||||||
git_ext::cmd_git_clone,
|
|
||||||
git_ext::cmd_git_commit,
|
git_ext::cmd_git_commit,
|
||||||
git_ext::cmd_git_fetch_all,
|
git_ext::cmd_git_fetch_all,
|
||||||
git_ext::cmd_git_push,
|
git_ext::cmd_git_push,
|
||||||
git_ext::cmd_git_pull,
|
git_ext::cmd_git_pull,
|
||||||
git_ext::cmd_git_pull_force_reset,
|
|
||||||
git_ext::cmd_git_pull_merge,
|
|
||||||
git_ext::cmd_git_add,
|
git_ext::cmd_git_add,
|
||||||
git_ext::cmd_git_unstage,
|
git_ext::cmd_git_unstage,
|
||||||
git_ext::cmd_git_reset_changes,
|
|
||||||
git_ext::cmd_git_add_credential,
|
git_ext::cmd_git_add_credential,
|
||||||
git_ext::cmd_git_remotes,
|
git_ext::cmd_git_remotes,
|
||||||
git_ext::cmd_git_add_remote,
|
git_ext::cmd_git_add_remote,
|
||||||
git_ext::cmd_git_rm_remote,
|
git_ext::cmd_git_rm_remote,
|
||||||
//
|
//
|
||||||
// Plugin commands
|
|
||||||
plugins_ext::cmd_plugins_search,
|
|
||||||
plugins_ext::cmd_plugins_install,
|
|
||||||
plugins_ext::cmd_plugins_uninstall,
|
|
||||||
plugins_ext::cmd_plugins_updates,
|
|
||||||
plugins_ext::cmd_plugins_update_all,
|
|
||||||
//
|
|
||||||
// WebSocket commands
|
// WebSocket commands
|
||||||
|
ws_ext::cmd_ws_upsert_request,
|
||||||
|
ws_ext::cmd_ws_duplicate_request,
|
||||||
|
ws_ext::cmd_ws_delete_request,
|
||||||
|
ws_ext::cmd_ws_delete_connection,
|
||||||
ws_ext::cmd_ws_delete_connections,
|
ws_ext::cmd_ws_delete_connections,
|
||||||
|
ws_ext::cmd_ws_list_events,
|
||||||
|
ws_ext::cmd_ws_list_requests,
|
||||||
|
ws_ext::cmd_ws_list_connections,
|
||||||
ws_ext::cmd_ws_send,
|
ws_ext::cmd_ws_send,
|
||||||
ws_ext::cmd_ws_close,
|
ws_ext::cmd_ws_close,
|
||||||
ws_ext::cmd_ws_connect,
|
ws_ext::cmd_ws_connect,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::history::get_or_upsert_launch_info;
|
use crate::history::get_or_upsert_launch_info;
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
@@ -9,8 +8,9 @@ use std::time::Instant;
|
|||||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_common::platform::get_os_str;
|
use yaak_common::platform::get_os_str;
|
||||||
use yaak_models::util::UpdateSource;
|
|
||||||
use yaak_tauri_utils::api_client::yaak_api_client;
|
use yaak_tauri_utils::api_client::yaak_api_client;
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
// Check for updates every hour
|
// Check for updates every hour
|
||||||
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60;
|
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::http_request::send_http_request_with_context;
|
use crate::http_request::send_http_request_with_context;
|
||||||
use crate::models_ext::BlobManagerExt;
|
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use crate::render::{render_grpc_request, render_http_request, render_json_value};
|
use crate::render::{render_grpc_request, render_http_request, render_json_value};
|
||||||
use crate::window::{CreateWindowConfig, create_window};
|
use crate::window::{CreateWindowConfig, create_window};
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -16,8 +14,11 @@ use tauri::{AppHandle, Emitter, Manager, Runtime};
|
|||||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
|
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
||||||
|
use crate::models_ext::BlobManagerExt;
|
||||||
use yaak_models::models::{AnyModel, HttpResponse, Plugin};
|
use yaak_models::models::{AnyModel, HttpResponse, Plugin};
|
||||||
use yaak_models::queries::any_request::AnyRequest;
|
use yaak_models::queries::any_request::AnyRequest;
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
use yaak_plugins::error::Error::PluginErr;
|
use yaak_plugins::error::Error::PluginErr;
|
||||||
use yaak_plugins::events::{
|
use yaak_plugins::events::{
|
||||||
@@ -31,7 +32,6 @@ use yaak_plugins::events::{
|
|||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::plugin_handle::PluginHandle;
|
use yaak_plugins::plugin_handle::PluginHandle;
|
||||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||||
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
|
||||||
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
||||||
|
|
||||||
pub(crate) async fn handle_plugin_event<R: Runtime>(
|
pub(crate) async fn handle_plugin_event<R: Runtime>(
|
||||||
@@ -57,10 +57,6 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
|
let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
|
||||||
Ok(call_frontend(&window, event).await)
|
Ok(call_frontend(&window, event).await)
|
||||||
}
|
}
|
||||||
InternalEventPayload::PromptFormRequest(_) => {
|
|
||||||
let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
|
|
||||||
Ok(call_frontend(&window, event).await)
|
|
||||||
}
|
|
||||||
InternalEventPayload::FindHttpResponsesRequest(req) => {
|
InternalEventPayload::FindHttpResponsesRequest(req) => {
|
||||||
let http_responses = app_handle
|
let http_responses = app_handle
|
||||||
.db()
|
.db()
|
||||||
@@ -170,12 +166,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
)?;
|
)?;
|
||||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||||
let cb = PluginTemplateCallback::new(
|
let cb = PluginTemplateCallback::new(plugin_manager, encryption_manager, &plugin_context, req.purpose);
|
||||||
plugin_manager,
|
|
||||||
encryption_manager,
|
|
||||||
&plugin_context,
|
|
||||||
req.purpose,
|
|
||||||
);
|
|
||||||
let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
||||||
let grpc_request =
|
let grpc_request =
|
||||||
render_grpc_request(&req.grpc_request, environment_chain, &cb, &opt).await?;
|
render_grpc_request(&req.grpc_request, environment_chain, &cb, &opt).await?;
|
||||||
@@ -196,12 +187,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
)?;
|
)?;
|
||||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||||
let cb = PluginTemplateCallback::new(
|
let cb = PluginTemplateCallback::new(plugin_manager, encryption_manager, &plugin_context, req.purpose);
|
||||||
plugin_manager,
|
|
||||||
encryption_manager,
|
|
||||||
&plugin_context,
|
|
||||||
req.purpose,
|
|
||||||
);
|
|
||||||
let opt = &RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
let opt = &RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
||||||
let http_request =
|
let http_request =
|
||||||
render_http_request(&req.http_request, environment_chain, &cb, &opt).await?;
|
render_http_request(&req.http_request, environment_chain, &cb, &opt).await?;
|
||||||
@@ -232,12 +218,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
)?;
|
)?;
|
||||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||||
let cb = PluginTemplateCallback::new(
|
let cb = PluginTemplateCallback::new(plugin_manager, encryption_manager, &plugin_context, req.purpose);
|
||||||
plugin_manager,
|
|
||||||
encryption_manager,
|
|
||||||
&plugin_context,
|
|
||||||
req.purpose,
|
|
||||||
);
|
|
||||||
let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
||||||
let data = render_json_value(req.data, environment_chain, &cb, &opt).await?;
|
let data = render_json_value(req.data, environment_chain, &cb, &opt).await?;
|
||||||
Ok(Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data })))
|
Ok(Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data })))
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use tauri::path::BaseDirectory;
|
|||||||
use tauri::plugin::{Builder, TauriPlugin};
|
use tauri::plugin::{Builder, TauriPlugin};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
AppHandle, Emitter, Manager, RunEvent, Runtime, State, WebviewWindow, WindowEvent, command,
|
AppHandle, Emitter, Manager, RunEvent, Runtime, State, WebviewWindow, WindowEvent, command,
|
||||||
is_dev,
|
generate_handler, is_dev,
|
||||||
};
|
};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
@@ -132,7 +132,7 @@ impl PluginUpdater {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_plugins_search<R: Runtime>(
|
pub(crate) async fn cmd_plugins_search<R: Runtime>(
|
||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<PluginSearchResponse> {
|
) -> Result<PluginSearchResponse> {
|
||||||
@@ -141,7 +141,7 @@ pub async fn cmd_plugins_search<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_plugins_install<R: Runtime>(
|
pub(crate) async fn cmd_plugins_install<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
name: &str,
|
name: &str,
|
||||||
version: Option<String>,
|
version: Option<String>,
|
||||||
@@ -163,7 +163,7 @@ pub async fn cmd_plugins_install<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_plugins_uninstall<R: Runtime>(
|
pub(crate) async fn cmd_plugins_uninstall<R: Runtime>(
|
||||||
plugin_id: &str,
|
plugin_id: &str,
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
) -> Result<Plugin> {
|
) -> Result<Plugin> {
|
||||||
@@ -174,7 +174,7 @@ pub async fn cmd_plugins_uninstall<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_plugins_updates<R: Runtime>(
|
pub(crate) async fn cmd_plugins_updates<R: Runtime>(
|
||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
) -> Result<PluginUpdatesResponse> {
|
) -> Result<PluginUpdatesResponse> {
|
||||||
let http_client = yaak_api_client(&app_handle)?;
|
let http_client = yaak_api_client(&app_handle)?;
|
||||||
@@ -183,7 +183,7 @@ pub async fn cmd_plugins_updates<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_plugins_update_all<R: Runtime>(
|
pub(crate) async fn cmd_plugins_update_all<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
) -> Result<Vec<PluginNameVersion>> {
|
) -> Result<Vec<PluginNameVersion>> {
|
||||||
let http_client = yaak_api_client(window.app_handle())?;
|
let http_client = yaak_api_client(window.app_handle())?;
|
||||||
@@ -233,6 +233,13 @@ pub async fn cmd_plugins_update_all<R: Runtime>(
|
|||||||
|
|
||||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||||
Builder::new("yaak-plugins")
|
Builder::new("yaak-plugins")
|
||||||
|
.invoke_handler(generate_handler![
|
||||||
|
cmd_plugins_search,
|
||||||
|
cmd_plugins_install,
|
||||||
|
cmd_plugins_uninstall,
|
||||||
|
cmd_plugins_updates,
|
||||||
|
cmd_plugins_update_all
|
||||||
|
])
|
||||||
.setup(|app_handle, _| {
|
.setup(|app_handle, _| {
|
||||||
// Resolve paths for plugin manager
|
// Resolve paths for plugin manager
|
||||||
let vendored_plugin_dir = app_handle
|
let vendored_plugin_dir = app_handle
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ use std::path::PathBuf;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{Emitter, Listener, Manager, Runtime, WebviewWindow};
|
use tauri::{Emitter, Listener, Manager, Runtime, WebviewWindow};
|
||||||
@@ -12,6 +11,7 @@ use tauri_plugin_updater::{Update, UpdaterExt};
|
|||||||
use tokio::task::block_in_place;
|
use tokio::task::block_in_place;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
use yaak_models::util::generate_id;
|
use yaak_models::util::generate_id;
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
use crate::PluginContextExt;
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::import::import_data;
|
use crate::import::import_data;
|
||||||
use crate::models_ext::QueryManagerExt;
|
use crate::models_ext::QueryManagerExt;
|
||||||
|
use crate::PluginContextExt;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Emitter, Manager, Runtime, Url};
|
use tauri::{AppHandle, Emitter, Manager, Runtime, Url};
|
||||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
|
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
|
||||||
|
use yaak_tauri_utils::api_client::yaak_api_client;
|
||||||
use yaak_models::util::generate_id;
|
use yaak_models::util::generate_id;
|
||||||
use yaak_plugins::events::{Color, ShowToastRequest};
|
use yaak_plugins::events::{Color, ShowToastRequest};
|
||||||
use yaak_plugins::install::download_and_install;
|
use yaak_plugins::install::download_and_install;
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_tauri_utils::api_client::yaak_api_client;
|
|
||||||
|
|
||||||
pub(crate) async fn handle_deep_link<R: Runtime>(
|
pub(crate) async fn handle_deep_link<R: Runtime>(
|
||||||
app_handle: &AppHandle<R>,
|
app_handle: &AppHandle<R>,
|
||||||
@@ -55,8 +55,7 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
|
|||||||
&plugin_context,
|
&plugin_context,
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
)
|
).await?;
|
||||||
.await?;
|
|
||||||
app_handle.emit(
|
app_handle.emit(
|
||||||
"show_toast",
|
"show_toast",
|
||||||
ShowToastRequest {
|
ShowToastRequest {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use crate::window_menu::app_menu;
|
use crate::window_menu::app_menu;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use rand::random;
|
use rand::random;
|
||||||
@@ -9,6 +8,7 @@ use tauri::{
|
|||||||
};
|
};
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
|
|
||||||
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
|
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
|
||||||
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
|
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
|
||||||
@@ -162,16 +162,11 @@ pub(crate) fn create_window<R: Runtime>(
|
|||||||
"dev.reset_size" => webview_window
|
"dev.reset_size" => webview_window
|
||||||
.set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT))
|
.set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
"dev.reset_size_16x9" => {
|
"dev.reset_size_record" => {
|
||||||
let width = webview_window.outer_size().unwrap().width;
|
let width = webview_window.outer_size().unwrap().width;
|
||||||
let height = width * 9 / 16;
|
let height = width * 9 / 16;
|
||||||
webview_window.set_size(PhysicalSize::new(width, height)).unwrap()
|
webview_window.set_size(PhysicalSize::new(width, height)).unwrap()
|
||||||
}
|
}
|
||||||
"dev.reset_size_16x10" => {
|
|
||||||
let width = webview_window.outer_size().unwrap().width;
|
|
||||||
let height = width * 10 / 16;
|
|
||||||
webview_window.set_size(PhysicalSize::new(width, height)).unwrap()
|
|
||||||
}
|
|
||||||
"dev.refresh" => webview_window.eval("location.reload()").unwrap(),
|
"dev.refresh" => webview_window.eval("location.reload()").unwrap(),
|
||||||
"dev.generate_theme_css" => {
|
"dev.generate_theme_css" => {
|
||||||
w.emit("generate_theme_css", true).unwrap();
|
w.emit("generate_theme_css", true).unwrap();
|
||||||
|
|||||||
@@ -154,13 +154,8 @@ pub fn app_menu<R: Runtime>(app_handle: &AppHandle<R>) -> tauri::Result<Menu<R>>
|
|||||||
&MenuItemBuilder::with_id("dev.reset_size".to_string(), "Reset Size")
|
&MenuItemBuilder::with_id("dev.reset_size".to_string(), "Reset Size")
|
||||||
.build(app_handle)?,
|
.build(app_handle)?,
|
||||||
&MenuItemBuilder::with_id(
|
&MenuItemBuilder::with_id(
|
||||||
"dev.reset_size_16x9".to_string(),
|
"dev.reset_size_record".to_string(),
|
||||||
"Resize to 16x9",
|
"Reset Size 16x9",
|
||||||
)
|
|
||||||
.build(app_handle)?,
|
|
||||||
&MenuItemBuilder::with_id(
|
|
||||||
"dev.reset_size_16x10".to_string(),
|
|
||||||
"Resize to 16x10",
|
|
||||||
)
|
)
|
||||||
.build(app_handle)?,
|
.build(app_handle)?,
|
||||||
&MenuItemBuilder::with_id(
|
&MenuItemBuilder::with_id(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//! WebSocket Tauri command wrappers
|
//! WebSocket Tauri command wrappers
|
||||||
//! These wrap the core yaak-ws functionality for Tauri IPC.
|
//! These wrap the core yaak-ws functionality for Tauri IPC.
|
||||||
|
|
||||||
use crate::PluginContextExt;
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models_ext::QueryManagerExt;
|
use crate::models_ext::QueryManagerExt;
|
||||||
|
use crate::PluginContextExt;
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -28,6 +28,53 @@ use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
|||||||
use yaak_tls::find_client_certificate;
|
use yaak_tls::find_client_certificate;
|
||||||
use yaak_ws::{WebsocketManager, render_websocket_request};
|
use yaak_ws::{WebsocketManager, render_websocket_request};
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn cmd_ws_upsert_request<R: Runtime>(
|
||||||
|
request: WebsocketRequest,
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
window: WebviewWindow<R>,
|
||||||
|
) -> Result<WebsocketRequest> {
|
||||||
|
Ok(app_handle
|
||||||
|
.db()
|
||||||
|
.upsert_websocket_request(&request, &UpdateSource::from_window_label(window.label()))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn cmd_ws_duplicate_request<R: Runtime>(
|
||||||
|
request_id: &str,
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
window: WebviewWindow<R>,
|
||||||
|
) -> Result<WebsocketRequest> {
|
||||||
|
let db = app_handle.db();
|
||||||
|
let request = db.get_websocket_request(request_id)?;
|
||||||
|
Ok(db.duplicate_websocket_request(&request, &UpdateSource::from_window_label(window.label()))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn cmd_ws_delete_request<R: Runtime>(
|
||||||
|
request_id: &str,
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
window: WebviewWindow<R>,
|
||||||
|
) -> Result<WebsocketRequest> {
|
||||||
|
Ok(app_handle
|
||||||
|
.db()
|
||||||
|
.delete_websocket_request_by_id(request_id, &UpdateSource::from_window_label(window.label()))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn cmd_ws_delete_connection<R: Runtime>(
|
||||||
|
connection_id: &str,
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
window: WebviewWindow<R>,
|
||||||
|
) -> Result<WebsocketConnection> {
|
||||||
|
Ok(app_handle
|
||||||
|
.db()
|
||||||
|
.delete_websocket_connection_by_id(
|
||||||
|
connection_id,
|
||||||
|
&UpdateSource::from_window_label(window.label()),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_ws_delete_connections<R: Runtime>(
|
pub async fn cmd_ws_delete_connections<R: Runtime>(
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
@@ -40,6 +87,30 @@ pub async fn cmd_ws_delete_connections<R: Runtime>(
|
|||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn cmd_ws_list_events<R: Runtime>(
|
||||||
|
connection_id: &str,
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
) -> Result<Vec<WebsocketEvent>> {
|
||||||
|
Ok(app_handle.db().list_websocket_events(connection_id)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn cmd_ws_list_requests<R: Runtime>(
|
||||||
|
workspace_id: &str,
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
) -> Result<Vec<WebsocketRequest>> {
|
||||||
|
Ok(app_handle.db().list_websocket_requests(workspace_id)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn cmd_ws_list_connections<R: Runtime>(
|
||||||
|
workspace_id: &str,
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
) -> Result<Vec<WebsocketConnection>> {
|
||||||
|
Ok(app_handle.db().list_websocket_connections(workspace_id)?)
|
||||||
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_ws_send<R: Runtime>(
|
pub async fn cmd_ws_send<R: Runtime>(
|
||||||
connection_id: &str,
|
connection_id: &str,
|
||||||
@@ -225,10 +296,8 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
for header in plugin_result.set_headers.unwrap_or_default() {
|
for header in plugin_result.set_headers.unwrap_or_default() {
|
||||||
match (
|
match (http::HeaderName::from_str(&header.name), HeaderValue::from_str(&header.value))
|
||||||
http::HeaderName::from_str(&header.name),
|
{
|
||||||
HeaderValue::from_str(&header.value),
|
|
||||||
) {
|
|
||||||
(Ok(name), Ok(value)) => {
|
(Ok(name), Ok(value)) => {
|
||||||
headers.insert(name, value);
|
headers.insert(name, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,8 @@
|
|||||||
"vendored/protoc/include",
|
"vendored/protoc/include",
|
||||||
"vendored/plugins",
|
"vendored/plugins",
|
||||||
"vendored/plugin-runtime",
|
"vendored/plugin-runtime",
|
||||||
"vendored/node/yaaknode*",
|
"vendored/node/yaaknode",
|
||||||
"vendored/protoc/yaakprotoc*"
|
"vendored/protoc/yaakprotoc"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ use std::time::Duration;
|
|||||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
|
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_common::platform::get_os_str;
|
use yaak_common::platform::get_os_str;
|
||||||
|
use yaak_tauri_utils::api_client::yaak_api_client;
|
||||||
use yaak_models::db_context::DbContext;
|
use yaak_models::db_context::DbContext;
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
use yaak_tauri_utils::api_client::yaak_api_client;
|
|
||||||
|
|
||||||
/// Extension trait for accessing the QueryManager from Tauri Manager types.
|
/// Extension trait for accessing the QueryManager from Tauri Manager types.
|
||||||
/// This is needed temporarily until all crates are refactored to not use Tauri.
|
/// This is needed temporarily until all crates are refactored to not use Tauri.
|
||||||
|
|||||||
@@ -6,4 +6,3 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["process"] }
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
use std::ffi::OsStr;
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
|
||||||
|
|
||||||
/// Creates a new `tokio::process::Command` that won't spawn a console window on Windows.
|
|
||||||
pub fn new_xplatform_command<S: AsRef<OsStr>>(program: S) -> tokio::process::Command {
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
let mut cmd = tokio::process::Command::new(program);
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
|
||||||
}
|
|
||||||
cmd
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
pub mod command;
|
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub mod serde;
|
pub mod serde;
|
||||||
|
|||||||
@@ -11,7 +11,3 @@ export function revealWorkspaceKey(workspaceId: string) {
|
|||||||
export function setWorkspaceKey(args: { workspaceId: string; key: string }) {
|
export function setWorkspaceKey(args: { workspaceId: string; key: string }) {
|
||||||
return invoke<void>('cmd_set_workspace_key', args);
|
return invoke<void>('cmd_set_workspace_key', args);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function disableEncryption(workspaceId: string) {
|
|
||||||
return invoke<void>('cmd_disable_encryption', { workspaceId });
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -115,35 +115,6 @@ impl EncryptionManager {
|
|||||||
self.set_workspace_key(workspace_id, &wkey)
|
self.set_workspace_key(workspace_id, &wkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disable_encryption(&self, workspace_id: &str) -> Result<()> {
|
|
||||||
info!("Disabling encryption for {workspace_id}");
|
|
||||||
|
|
||||||
self.query_manager.with_tx::<(), Error>(|tx| {
|
|
||||||
let workspace = tx.get_workspace(workspace_id)?;
|
|
||||||
let workspace_meta = tx.get_or_create_workspace_meta(workspace_id)?;
|
|
||||||
|
|
||||||
// Clear encryption challenge on workspace
|
|
||||||
tx.upsert_workspace(
|
|
||||||
&Workspace { encryption_key_challenge: None, ..workspace },
|
|
||||||
&UpdateSource::Background,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Clear encryption key on workspace meta
|
|
||||||
tx.upsert_workspace_meta(
|
|
||||||
&WorkspaceMeta { encryption_key: None, ..workspace_meta },
|
|
||||||
&UpdateSource::Background,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Remove from cache
|
|
||||||
let mut cache = self.cached_workspace_keys.lock().unwrap();
|
|
||||||
cache.remove(workspace_id);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_workspace_key(&self, workspace_id: &str) -> Result<WorkspaceKey> {
|
fn get_workspace_key(&self, workspace_id: &str) -> Result<WorkspaceKey> {
|
||||||
{
|
{
|
||||||
let cache = self.cached_workspace_keys.lock().unwrap();
|
let cache = self.cached_workspace_keys.lock().unwrap();
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ serde = { workspace = true, features = ["derive"] }
|
|||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde_yaml = "0.9.34"
|
serde_yaml = "0.9.34"
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["io-util"] }
|
|
||||||
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
|
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
|
||||||
url = "2"
|
url = "2"
|
||||||
yaak-common = { workspace = true }
|
|
||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
yaak-sync = { workspace = true }
|
yaak-sync = { workspace = true }
|
||||||
|
|||||||
Generated
+2
-6
@@ -1,10 +1,6 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { SyncModel } from "./gen_models";
|
import type { SyncModel } from "./gen_models";
|
||||||
|
|
||||||
export type BranchDeleteResult = { "type": "success", message: string, } | { "type": "not_fully_merged" };
|
|
||||||
|
|
||||||
export type CloneResult = { "type": "success" } | { "type": "cancelled" } | { "type": "needs_credentials", url: string, error: string | null, };
|
|
||||||
|
|
||||||
export type GitAuthor = { name: string | null, email: string | null, };
|
export type GitAuthor = { name: string | null, email: string | null, };
|
||||||
|
|
||||||
export type GitCommit = { author: GitAuthor, when: string, message: string | null, };
|
export type GitCommit = { author: GitAuthor, when: string, message: string | null, };
|
||||||
@@ -15,8 +11,8 @@ export type GitStatus = "untracked" | "conflict" | "current" | "modified" | "rem
|
|||||||
|
|
||||||
export type GitStatusEntry = { relaPath: string, status: GitStatus, staged: boolean, prev: SyncModel | null, next: SyncModel | null, };
|
export type GitStatusEntry = { relaPath: string, status: GitStatus, staged: boolean, prev: SyncModel | null, next: SyncModel | null, };
|
||||||
|
|
||||||
export type GitStatusSummary = { path: string, headRef: string | null, headRefShorthand: string | null, entries: Array<GitStatusEntry>, origins: Array<string>, localBranches: Array<string>, remoteBranches: Array<string>, ahead: number, behind: number, };
|
export type GitStatusSummary = { path: string, headRef: string | null, headRefShorthand: string | null, entries: Array<GitStatusEntry>, origins: Array<string>, localBranches: Array<string>, remoteBranches: Array<string>, };
|
||||||
|
|
||||||
export type PullResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, } | { "type": "diverged", remote: string, branch: string, } | { "type": "uncommitted_changes" };
|
export type PullResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, };
|
||||||
|
|
||||||
export type PushResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, };
|
export type PushResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, };
|
||||||
|
|||||||
Generated
+1
-3
@@ -1,7 +1,5 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
|
||||||
|
|
||||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||||
|
|
||||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
@@ -20,4 +18,4 @@ export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environ
|
|||||||
|
|
||||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||||
|
|||||||
+20
-108
@@ -3,31 +3,20 @@ import { invoke } from '@tauri-apps/api/core';
|
|||||||
import { createFastMutation } from '@yaakapp/app/hooks/useFastMutation';
|
import { createFastMutation } from '@yaakapp/app/hooks/useFastMutation';
|
||||||
import { queryClient } from '@yaakapp/app/lib/queryClient';
|
import { queryClient } from '@yaakapp/app/lib/queryClient';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult } from './bindings/gen_git';
|
import { GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult } from './bindings/gen_git';
|
||||||
import { showToast } from '@yaakapp/app/lib/toast';
|
|
||||||
|
|
||||||
export * from './bindings/gen_git';
|
export * from './bindings/gen_git';
|
||||||
export * from './bindings/gen_models';
|
|
||||||
|
|
||||||
export interface GitCredentials {
|
export interface GitCredentials {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DivergedStrategy = 'force_reset' | 'merge' | 'cancel';
|
|
||||||
|
|
||||||
export type UncommittedChangesStrategy = 'reset' | 'cancel';
|
|
||||||
|
|
||||||
export interface GitCallbacks {
|
export interface GitCallbacks {
|
||||||
addRemote: () => Promise<GitRemote | null>;
|
addRemote: () => Promise<GitRemote | null>;
|
||||||
promptCredentials: (
|
promptCredentials: (
|
||||||
result: Extract<PushResult, { type: 'needs_credentials' }>,
|
result: Extract<PushResult, { type: 'needs_credentials' }>,
|
||||||
) => Promise<GitCredentials | null>;
|
) => Promise<GitCredentials | null>;
|
||||||
promptDiverged: (
|
|
||||||
result: Extract<PullResult, { type: 'diverged' }>,
|
|
||||||
) => Promise<DivergedStrategy>;
|
|
||||||
promptUncommittedChanges: () => Promise<UncommittedChangesStrategy>;
|
|
||||||
forceSync: () => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSuccess = () => queryClient.invalidateQueries({ queryKey: ['git'] });
|
const onSuccess = () => queryClient.invalidateQueries({ queryKey: ['git'] });
|
||||||
@@ -70,6 +59,7 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
|||||||
if (creds == null) throw new Error('Canceled');
|
if (creds == null) throw new Error('Canceled');
|
||||||
|
|
||||||
await invoke('cmd_git_add_credential', {
|
await invoke('cmd_git_add_credential', {
|
||||||
|
dir,
|
||||||
remoteUrl: result.url,
|
remoteUrl: result.url,
|
||||||
username: creds.username,
|
username: creds.username,
|
||||||
password: creds.password,
|
password: creds.password,
|
||||||
@@ -79,15 +69,6 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
|||||||
return invoke<PushResult>('cmd_git_push', { dir });
|
return invoke<PushResult>('cmd_git_push', { dir });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = (err: unknown) => {
|
|
||||||
showToast({
|
|
||||||
id: `${err}`,
|
|
||||||
message: `${err}`,
|
|
||||||
color: 'danger',
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init: createFastMutation<void, string, void>({
|
init: createFastMutation<void, string, void>({
|
||||||
mutationKey: ['git', 'init'],
|
mutationKey: ['git', 'init'],
|
||||||
@@ -109,31 +90,21 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
|||||||
mutationFn: (args) => invoke('cmd_git_rm_remote', { dir, ...args }),
|
mutationFn: (args) => invoke('cmd_git_rm_remote', { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
createBranch: createFastMutation<void, string, { branch: string; base?: string }>({
|
branch: createFastMutation<void, string, { branch: string }>({
|
||||||
mutationKey: ['git', 'branch', dir],
|
mutationKey: ['git', 'branch', dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_branch', { dir, ...args }),
|
mutationFn: (args) => invoke('cmd_git_branch', { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
mergeBranch: createFastMutation<void, string, { branch: string }>({
|
mergeBranch: createFastMutation<void, string, { branch: string; force: boolean }>({
|
||||||
mutationKey: ['git', 'merge', dir],
|
mutationKey: ['git', 'merge', dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_merge_branch', { dir, ...args }),
|
mutationFn: (args) => invoke('cmd_git_merge_branch', { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
deleteBranch: createFastMutation<BranchDeleteResult, string, { branch: string, force?: boolean }>({
|
deleteBranch: createFastMutation<void, string, { branch: string }>({
|
||||||
mutationKey: ['git', 'delete-branch', dir],
|
mutationKey: ['git', 'delete-branch', dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_delete_branch', { dir, ...args }),
|
mutationFn: (args) => invoke('cmd_git_delete_branch', { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
deleteRemoteBranch: createFastMutation<void, string, { branch: string }>({
|
|
||||||
mutationKey: ['git', 'delete-remote-branch', dir],
|
|
||||||
mutationFn: (args) => invoke('cmd_git_delete_remote_branch', { dir, ...args }),
|
|
||||||
onSuccess,
|
|
||||||
}),
|
|
||||||
renameBranch: createFastMutation<void, string, { oldName: string, newName: string }>({
|
|
||||||
mutationKey: ['git', 'rename-branch', dir],
|
|
||||||
mutationFn: (args) => invoke('cmd_git_rename_branch', { dir, ...args }),
|
|
||||||
onSuccess,
|
|
||||||
}),
|
|
||||||
checkout: createFastMutation<string, string, { branch: string; force: boolean }>({
|
checkout: createFastMutation<string, string, { branch: string; force: boolean }>({
|
||||||
mutationKey: ['git', 'checkout', dir],
|
mutationKey: ['git', 'checkout', dir],
|
||||||
mutationFn: (args) => invoke('cmd_git_checkout', { dir, ...args }),
|
mutationFn: (args) => invoke('cmd_git_checkout', { dir, ...args }),
|
||||||
@@ -152,9 +123,10 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
|||||||
},
|
},
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
fetchAll: createFastMutation<void, string, void>({
|
fetchAll: createFastMutation<string, string, void>({
|
||||||
mutationKey: ['git', 'fetch_all', dir],
|
mutationKey: ['git', 'checkout', dir],
|
||||||
mutationFn: () => invoke('cmd_git_fetch_all', { dir }),
|
mutationFn: () => invoke('cmd_git_fetch_all', { dir }),
|
||||||
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
push: createFastMutation<PushResult, string, void>({
|
push: createFastMutation<PushResult, string, void>({
|
||||||
mutationKey: ['git', 'push', dir],
|
mutationKey: ['git', 'push', dir],
|
||||||
@@ -165,51 +137,21 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
|||||||
mutationKey: ['git', 'pull', dir],
|
mutationKey: ['git', 'pull', dir],
|
||||||
async mutationFn() {
|
async mutationFn() {
|
||||||
const result = await invoke<PullResult>('cmd_git_pull', { dir });
|
const result = await invoke<PullResult>('cmd_git_pull', { dir });
|
||||||
|
if (result.type !== 'needs_credentials') return result;
|
||||||
|
|
||||||
if (result.type === 'needs_credentials') {
|
// Needs credentials, prompt for them
|
||||||
const creds = await callbacks.promptCredentials(result);
|
const creds = await callbacks.promptCredentials(result);
|
||||||
if (creds == null) throw new Error('Canceled');
|
if (creds == null) throw new Error('Canceled');
|
||||||
|
|
||||||
await invoke('cmd_git_add_credential', {
|
await invoke('cmd_git_add_credential', {
|
||||||
remoteUrl: result.url,
|
dir,
|
||||||
username: creds.username,
|
remoteUrl: result.url,
|
||||||
password: creds.password,
|
username: creds.username,
|
||||||
});
|
password: creds.password,
|
||||||
|
});
|
||||||
|
|
||||||
// Pull again after credentials
|
// Pull again
|
||||||
return invoke<PullResult>('cmd_git_pull', { dir });
|
return invoke<PullResult>('cmd_git_pull', { dir });
|
||||||
}
|
|
||||||
|
|
||||||
if (result.type === 'uncommitted_changes') {
|
|
||||||
callbacks.promptUncommittedChanges().then(async (strategy) => {
|
|
||||||
if (strategy === 'cancel') return;
|
|
||||||
|
|
||||||
await invoke('cmd_git_reset_changes', { dir });
|
|
||||||
return invoke<PullResult>('cmd_git_pull', { dir });
|
|
||||||
}).then(async () => { onSuccess(); await callbacks.forceSync(); }, handleError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.type === 'diverged') {
|
|
||||||
callbacks.promptDiverged(result).then((strategy) => {
|
|
||||||
if (strategy === 'cancel') return;
|
|
||||||
|
|
||||||
if (strategy === 'force_reset') {
|
|
||||||
return invoke<PullResult>('cmd_git_pull_force_reset', {
|
|
||||||
dir,
|
|
||||||
remote: result.remote,
|
|
||||||
branch: result.branch,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return invoke<PullResult>('cmd_git_pull_merge', {
|
|
||||||
dir,
|
|
||||||
remote: result.remote,
|
|
||||||
branch: result.branch,
|
|
||||||
});
|
|
||||||
}).then(async () => { onSuccess(); await callbacks.forceSync(); }, handleError);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
},
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
@@ -218,39 +160,9 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
|||||||
mutationFn: (args) => invoke('cmd_git_unstage', { dir, ...args }),
|
mutationFn: (args) => invoke('cmd_git_unstage', { dir, ...args }),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
resetChanges: createFastMutation<void, string, void>({
|
|
||||||
mutationKey: ['git', 'reset-changes', dir],
|
|
||||||
mutationFn: () => invoke('cmd_git_reset_changes', { dir }),
|
|
||||||
onSuccess,
|
|
||||||
}),
|
|
||||||
} as const;
|
} as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getRemotes(dir: string) {
|
async function getRemotes(dir: string) {
|
||||||
return invoke<GitRemote[]>('cmd_git_remotes', { dir });
|
return invoke<GitRemote[]>('cmd_git_remotes', { dir });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clone a git repository, prompting for credentials if needed.
|
|
||||||
*/
|
|
||||||
export async function gitClone(
|
|
||||||
url: string,
|
|
||||||
dir: string,
|
|
||||||
promptCredentials: (args: { url: string; error: string | null }) => Promise<GitCredentials | null>,
|
|
||||||
): Promise<CloneResult> {
|
|
||||||
const result = await invoke<CloneResult>('cmd_git_clone', { url, dir });
|
|
||||||
if (result.type !== 'needs_credentials') return result;
|
|
||||||
|
|
||||||
// Prompt for credentials
|
|
||||||
const creds = await promptCredentials({ url: result.url, error: result.error });
|
|
||||||
if (creds == null) return {type: 'cancelled'};
|
|
||||||
|
|
||||||
// Store credentials and retry
|
|
||||||
await invoke('cmd_git_add_credential', {
|
|
||||||
remoteUrl: result.url,
|
|
||||||
username: creds.username,
|
|
||||||
password: creds.password,
|
|
||||||
});
|
|
||||||
|
|
||||||
return invoke<CloneResult>('cmd_git_clone', { url, dir });
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,30 +1,38 @@
|
|||||||
use crate::error::Error::GitNotFound;
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Stdio;
|
use std::process::{Command, Stdio};
|
||||||
use tokio::process::Command;
|
|
||||||
use yaak_common::command::new_xplatform_command;
|
|
||||||
|
|
||||||
/// Create a git command that runs in the specified directory
|
use crate::error::Error::GitNotFound;
|
||||||
pub(crate) async fn new_binary_command(dir: &Path) -> Result<Command> {
|
#[cfg(target_os = "windows")]
|
||||||
let mut cmd = new_binary_command_global().await?;
|
use std::os::windows::process::CommandExt;
|
||||||
cmd.arg("-C").arg(dir);
|
|
||||||
Ok(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a git command without a specific directory (for global operations)
|
#[cfg(target_os = "windows")]
|
||||||
pub(crate) async fn new_binary_command_global() -> Result<Command> {
|
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||||
|
|
||||||
|
pub(crate) fn new_binary_command(dir: &Path) -> Result<Command> {
|
||||||
// 1. Probe that `git` exists and is runnable
|
// 1. Probe that `git` exists and is runnable
|
||||||
let mut probe = new_xplatform_command("git");
|
let mut probe = Command::new("git");
|
||||||
probe.arg("--version").stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
|
probe.arg("--version").stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
|
||||||
|
|
||||||
let status = probe.status().await.map_err(|_| GitNotFound)?;
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
probe.creation_flags(CREATE_NO_WINDOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = probe.status().map_err(|_| GitNotFound)?;
|
||||||
|
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
return Err(GitNotFound);
|
return Err(GitNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Build the reusable git command
|
// 2. Build the reusable git command
|
||||||
let cmd = new_xplatform_command("git");
|
let mut cmd = Command::new("git");
|
||||||
|
cmd.arg("-C").arg(dir);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-118
@@ -1,153 +1,99 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use ts_rs::TS;
|
|
||||||
|
|
||||||
use crate::binary::new_binary_command;
|
|
||||||
use crate::error::Error::GenericError;
|
use crate::error::Error::GenericError;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use crate::merge::do_merge;
|
||||||
|
use crate::repository::open_repo;
|
||||||
|
use crate::util::{bytes_to_string, get_branch_by_name, get_current_branch};
|
||||||
|
use git2::BranchType;
|
||||||
|
use git2::build::CheckoutBuilder;
|
||||||
|
use log::info;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
pub fn git_checkout_branch(dir: &Path, branch_name: &str, force: bool) -> Result<String> {
|
||||||
#[serde(rename_all = "snake_case", tag = "type")]
|
if branch_name.starts_with("origin/") {
|
||||||
#[ts(export, export_to = "gen_git.ts")]
|
return git_checkout_remote_branch(dir, branch_name, force);
|
||||||
pub enum BranchDeleteResult {
|
}
|
||||||
Success { message: String },
|
|
||||||
NotFullyMerged,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn git_checkout_branch(dir: &Path, branch_name: &str, force: bool) -> Result<String> {
|
let repo = open_repo(dir)?;
|
||||||
let branch_name = branch_name.trim_start_matches("origin/");
|
let branch = get_branch_by_name(&repo, branch_name)?;
|
||||||
|
let branch_ref = branch.into_reference();
|
||||||
|
let branch_tree = branch_ref.peel_to_tree()?;
|
||||||
|
|
||||||
let mut args = vec!["checkout"];
|
let mut options = CheckoutBuilder::default();
|
||||||
if force {
|
if force {
|
||||||
args.push("--force");
|
options.force();
|
||||||
}
|
}
|
||||||
args.push(branch_name);
|
|
||||||
|
|
||||||
let out = new_binary_command(dir)
|
repo.checkout_tree(branch_tree.as_object(), Some(&mut options))?;
|
||||||
.await?
|
repo.set_head(branch_ref.name().unwrap())?;
|
||||||
.args(&args)
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.map_err(|e| GenericError(format!("failed to run git checkout: {e}")))?;
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
||||||
let combined = format!("{}{}", stdout, stderr);
|
|
||||||
|
|
||||||
if !out.status.success() {
|
|
||||||
return Err(GenericError(format!("Failed to checkout: {}", combined.trim())));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(branch_name.to_string())
|
Ok(branch_name.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn git_create_branch(dir: &Path, name: &str, base: Option<&str>) -> Result<()> {
|
pub(crate) fn git_checkout_remote_branch(
|
||||||
let mut cmd = new_binary_command(dir).await?;
|
dir: &Path,
|
||||||
cmd.arg("branch").arg(name);
|
branch_name: &str,
|
||||||
if let Some(base_branch) = base {
|
force: bool,
|
||||||
cmd.arg(base_branch);
|
) -> Result<String> {
|
||||||
}
|
let branch_name = branch_name.trim_start_matches("origin/");
|
||||||
|
let repo = open_repo(dir)?;
|
||||||
|
|
||||||
let out =
|
let refname = format!("refs/remotes/origin/{}", branch_name);
|
||||||
cmd.output().await.map_err(|e| GenericError(format!("failed to run git branch: {e}")))?;
|
let remote_ref = repo.find_reference(&refname)?;
|
||||||
|
let commit = remote_ref.peel_to_commit()?;
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
let mut new_branch = repo.branch(branch_name, &commit, false)?;
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
let upstream_name = format!("origin/{}", branch_name);
|
||||||
let combined = format!("{}{}", stdout, stderr);
|
new_branch.set_upstream(Some(&upstream_name))?;
|
||||||
|
|
||||||
if !out.status.success() {
|
git_checkout_branch(dir, branch_name, force)
|
||||||
return Err(GenericError(format!("Failed to create branch: {}", combined.trim())));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn git_delete_branch(dir: &Path, name: &str, force: bool) -> Result<BranchDeleteResult> {
|
pub fn git_create_branch(dir: &Path, name: &str) -> Result<()> {
|
||||||
let mut cmd = new_binary_command(dir).await?;
|
let repo = open_repo(dir)?;
|
||||||
|
let head = match repo.head() {
|
||||||
let out =
|
Ok(h) => h,
|
||||||
if force { cmd.args(["branch", "-D", name]) } else { cmd.args(["branch", "-d", name]) }
|
Err(e) if e.code() == git2::ErrorCode::UnbornBranch => {
|
||||||
.output()
|
let msg = "Cannot create branch when there are no commits";
|
||||||
.await
|
return Err(GenericError(msg.into()));
|
||||||
.map_err(|e| GenericError(format!("failed to run git branch -d: {e}")))?;
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
||||||
let combined = format!("{}{}", stdout, stderr);
|
|
||||||
|
|
||||||
if !out.status.success() && stderr.to_lowercase().contains("not fully merged") {
|
|
||||||
return Ok(BranchDeleteResult::NotFullyMerged);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !out.status.success() {
|
|
||||||
return Err(GenericError(format!("Failed to delete branch: {}", combined.trim())));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(BranchDeleteResult::Success { message: combined })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn git_merge_branch(dir: &Path, name: &str) -> Result<()> {
|
|
||||||
let out = new_binary_command(dir)
|
|
||||||
.await?
|
|
||||||
.args(["merge", name])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.map_err(|e| GenericError(format!("failed to run git merge: {e}")))?;
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
||||||
let combined = format!("{}{}", stdout, stderr);
|
|
||||||
|
|
||||||
if !out.status.success() {
|
|
||||||
// Check for merge conflicts
|
|
||||||
if combined.to_lowercase().contains("conflict") {
|
|
||||||
return Err(GenericError(
|
|
||||||
"Merge conflicts detected. Please resolve them manually.".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
return Err(GenericError(format!("Failed to merge: {}", combined.trim())));
|
Err(e) => return Err(e.into()),
|
||||||
}
|
};
|
||||||
|
let head = head.peel_to_commit()?;
|
||||||
|
|
||||||
|
repo.branch(name, &head, false)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn git_delete_remote_branch(dir: &Path, name: &str) -> Result<()> {
|
pub fn git_delete_branch(dir: &Path, name: &str) -> Result<()> {
|
||||||
// Remote branch names come in as "origin/branch-name", extract the branch name
|
let repo = open_repo(dir)?;
|
||||||
let branch_name = name.trim_start_matches("origin/");
|
let mut branch = get_branch_by_name(&repo, name)?;
|
||||||
|
|
||||||
let out = new_binary_command(dir)
|
if branch.is_head() {
|
||||||
.await?
|
info!("Deleting head branch");
|
||||||
.args(["push", "origin", "--delete", branch_name])
|
let branches = repo.branches(Some(BranchType::Local))?;
|
||||||
.output()
|
let other_branch = branches.into_iter().filter_map(|b| b.ok()).find(|b| !b.0.is_head());
|
||||||
.await
|
let other_branch = match other_branch {
|
||||||
.map_err(|e| GenericError(format!("failed to run git push --delete: {e}")))?;
|
None => return Err(GenericError("Cannot delete only branch".into())),
|
||||||
|
Some(b) => bytes_to_string(b.0.name_bytes()?)?,
|
||||||
|
};
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
git_checkout_branch(dir, &other_branch, true)?;
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
||||||
let combined = format!("{}{}", stdout, stderr);
|
|
||||||
|
|
||||||
if !out.status.success() {
|
|
||||||
return Err(GenericError(format!("Failed to delete remote branch: {}", combined.trim())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
branch.delete()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn git_rename_branch(dir: &Path, old_name: &str, new_name: &str) -> Result<()> {
|
pub fn git_merge_branch(dir: &Path, name: &str, _force: bool) -> Result<()> {
|
||||||
let out = new_binary_command(dir)
|
let repo = open_repo(dir)?;
|
||||||
.await?
|
let local_branch = get_current_branch(&repo)?.unwrap();
|
||||||
.args(["branch", "-m", old_name, new_name])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.map_err(|e| GenericError(format!("failed to run git branch -m: {e}")))?;
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
let commit_to_merge = get_branch_by_name(&repo, name)?.into_reference();
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
let commit_to_merge = repo.reference_to_annotated_commit(&commit_to_merge)?;
|
||||||
let combined = format!("{}{}", stdout, stderr);
|
|
||||||
|
|
||||||
if !out.status.success() {
|
do_merge(&repo, &local_branch, &commit_to_merge)?;
|
||||||
return Err(GenericError(format!("Failed to rename branch: {}", combined.trim())));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
use crate::binary::new_binary_command;
|
|
||||||
use crate::error::Error::GenericError;
|
|
||||||
use crate::error::Result;
|
|
||||||
use log::info;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fs;
|
|
||||||
use std::path::Path;
|
|
||||||
use ts_rs::TS;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
|
||||||
#[serde(rename_all = "snake_case", tag = "type")]
|
|
||||||
#[ts(export, export_to = "gen_git.ts")]
|
|
||||||
pub enum CloneResult {
|
|
||||||
Success,
|
|
||||||
Cancelled,
|
|
||||||
NeedsCredentials { url: String, error: Option<String> },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn git_clone(url: &str, dir: &Path) -> Result<CloneResult> {
|
|
||||||
let parent = dir.parent().ok_or_else(|| GenericError("Invalid clone directory".to_string()))?;
|
|
||||||
fs::create_dir_all(parent)
|
|
||||||
.map_err(|e| GenericError(format!("Failed to create directory: {e}")))?;
|
|
||||||
let mut cmd = new_binary_command(parent).await?;
|
|
||||||
cmd.args(["clone", url]).arg(dir).env("GIT_TERMINAL_PROMPT", "0");
|
|
||||||
|
|
||||||
let out =
|
|
||||||
cmd.output().await.map_err(|e| GenericError(format!("failed to run git clone: {e}")))?;
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
||||||
let combined = format!("{}{}", stdout, stderr);
|
|
||||||
let combined_lower = combined.to_lowercase();
|
|
||||||
|
|
||||||
info!("Cloned status={}: {combined}", out.status);
|
|
||||||
|
|
||||||
if !out.status.success() {
|
|
||||||
// Check for credentials error
|
|
||||||
if combined_lower.contains("could not read") {
|
|
||||||
return Ok(CloneResult::NeedsCredentials { url: url.to_string(), error: None });
|
|
||||||
}
|
|
||||||
if combined_lower.contains("unable to access")
|
|
||||||
|| combined_lower.contains("authentication failed")
|
|
||||||
{
|
|
||||||
return Ok(CloneResult::NeedsCredentials {
|
|
||||||
url: url.to_string(),
|
|
||||||
error: Some(combined.to_string()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Err(GenericError(format!("Failed to clone: {}", combined.trim())));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(CloneResult::Success)
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,8 @@ use crate::error::Error::GenericError;
|
|||||||
use log::info;
|
use log::info;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub async fn git_commit(dir: &Path, message: &str) -> crate::error::Result<()> {
|
pub fn git_commit(dir: &Path, message: &str) -> crate::error::Result<()> {
|
||||||
let out =
|
let out = new_binary_command(dir)?.args(["commit", "--message", message]).output()?;
|
||||||
new_binary_command(dir).await?.args(["commit", "--message", message]).output().await?;
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
use crate::binary::new_binary_command_global;
|
use crate::binary::new_binary_command;
|
||||||
use crate::error::Error::GenericError;
|
use crate::error::Error::GenericError;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use tokio::io::AsyncWriteExt;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub async fn git_add_credential(remote_url: &str, username: &str, password: &str) -> Result<()> {
|
pub async fn git_add_credential(
|
||||||
|
dir: &Path,
|
||||||
|
remote_url: &str,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<()> {
|
||||||
let url = Url::parse(remote_url)
|
let url = Url::parse(remote_url)
|
||||||
.map_err(|e| GenericError(format!("Failed to parse remote url {remote_url}: {e:?}")))?;
|
.map_err(|e| GenericError(format!("Failed to parse remote url {remote_url}: {e:?}")))?;
|
||||||
let protocol = url.scheme();
|
let protocol = url.scheme();
|
||||||
let host = url.host_str().unwrap();
|
let host = url.host_str().unwrap();
|
||||||
let path = Some(url.path());
|
let path = Some(url.path());
|
||||||
|
|
||||||
let mut child = new_binary_command_global()
|
let mut child = new_binary_command(dir)?
|
||||||
.await?
|
|
||||||
.args(["credential", "approve"])
|
.args(["credential", "approve"])
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
@@ -21,21 +26,19 @@ pub async fn git_add_credential(remote_url: &str, username: &str, password: &str
|
|||||||
|
|
||||||
{
|
{
|
||||||
let stdin = child.stdin.as_mut().unwrap();
|
let stdin = child.stdin.as_mut().unwrap();
|
||||||
stdin.write_all(format!("protocol={}\n", protocol).as_bytes()).await?;
|
writeln!(stdin, "protocol={}", protocol)?;
|
||||||
stdin.write_all(format!("host={}\n", host).as_bytes()).await?;
|
writeln!(stdin, "host={}", host)?;
|
||||||
if let Some(path) = path {
|
if let Some(path) = path {
|
||||||
if !path.is_empty() {
|
if !path.is_empty() {
|
||||||
stdin
|
writeln!(stdin, "path={}", path.trim_start_matches('/'))?;
|
||||||
.write_all(format!("path={}\n", path.trim_start_matches('/')).as_bytes())
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stdin.write_all(format!("username={}\n", username).as_bytes()).await?;
|
writeln!(stdin, "username={}", username)?;
|
||||||
stdin.write_all(format!("password={}\n", password).as_bytes()).await?;
|
writeln!(stdin, "password={}", password)?;
|
||||||
stdin.write_all(b"\n").await?; // blank line terminator
|
writeln!(stdin)?; // blank line terminator
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = child.wait().await?;
|
let status = child.wait()?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
return Err(GenericError("Failed to approve git credential".to_string()));
|
return Err(GenericError("Failed to approve git credential".to_string()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ use crate::error::Error::GenericError;
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub async fn git_fetch_all(dir: &Path) -> Result<()> {
|
pub fn git_fetch_all(dir: &Path) -> Result<()> {
|
||||||
let out = new_binary_command(dir)
|
let out = new_binary_command(dir)?
|
||||||
.await?
|
|
||||||
.args(["fetch", "--all", "--prune", "--tags"])
|
.args(["fetch", "--all", "--prune", "--tags"])
|
||||||
.output()
|
.output()
|
||||||
.await
|
|
||||||
.map_err(|e| GenericError(format!("failed to run git pull: {e}")))?;
|
.map_err(|e| GenericError(format!("failed to run git pull: {e}")))?;
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
|||||||
@@ -1,38 +1,31 @@
|
|||||||
mod add;
|
mod add;
|
||||||
mod binary;
|
mod binary;
|
||||||
mod branch;
|
mod branch;
|
||||||
mod clone;
|
|
||||||
mod commit;
|
mod commit;
|
||||||
mod credential;
|
mod credential;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod fetch;
|
mod fetch;
|
||||||
mod init;
|
mod init;
|
||||||
mod log;
|
mod log;
|
||||||
|
mod merge;
|
||||||
mod pull;
|
mod pull;
|
||||||
mod push;
|
mod push;
|
||||||
mod remotes;
|
mod remotes;
|
||||||
mod repository;
|
mod repository;
|
||||||
mod reset;
|
|
||||||
mod status;
|
mod status;
|
||||||
mod unstage;
|
mod unstage;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
// Re-export all git functions for external use
|
// Re-export all git functions for external use
|
||||||
pub use add::git_add;
|
pub use add::git_add;
|
||||||
pub use branch::{
|
pub use branch::{git_checkout_branch, git_create_branch, git_delete_branch, git_merge_branch};
|
||||||
BranchDeleteResult, git_checkout_branch, git_create_branch, git_delete_branch,
|
|
||||||
git_delete_remote_branch, git_merge_branch, git_rename_branch,
|
|
||||||
};
|
|
||||||
pub use clone::{CloneResult, git_clone};
|
|
||||||
pub use commit::git_commit;
|
pub use commit::git_commit;
|
||||||
pub use credential::git_add_credential;
|
pub use credential::git_add_credential;
|
||||||
pub use fetch::git_fetch_all;
|
pub use fetch::git_fetch_all;
|
||||||
pub use init::git_init;
|
pub use init::git_init;
|
||||||
pub use log::{GitCommit, git_log};
|
pub use log::{GitCommit, git_log};
|
||||||
pub use pull::{PullResult, git_pull, git_pull_force_reset, git_pull_merge};
|
pub use pull::{PullResult, git_pull};
|
||||||
pub use push::{PushResult, git_push};
|
pub use push::{PushResult, git_push};
|
||||||
pub use remotes::{GitRemote, git_add_remote, git_remotes, git_rm_remote};
|
pub use remotes::{GitRemote, git_add_remote, git_remotes, git_rm_remote};
|
||||||
pub use reset::git_reset_changes;
|
|
||||||
pub use status::{GitStatusSummary, git_status};
|
pub use status::{GitStatusSummary, git_status};
|
||||||
pub use unstage::git_unstage;
|
pub use unstage::git_unstage;
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
use crate::error::Error::MergeConflicts;
|
||||||
|
use crate::util::bytes_to_string;
|
||||||
|
use git2::{AnnotatedCommit, Branch, IndexEntry, Reference, Repository};
|
||||||
|
use log::{debug, info};
|
||||||
|
|
||||||
|
pub(crate) fn do_merge(
|
||||||
|
repo: &Repository,
|
||||||
|
local_branch: &Branch,
|
||||||
|
commit_to_merge: &AnnotatedCommit,
|
||||||
|
) -> crate::error::Result<()> {
|
||||||
|
debug!("Merging remote branches");
|
||||||
|
let analysis = repo.merge_analysis(&[&commit_to_merge])?;
|
||||||
|
|
||||||
|
if analysis.0.is_fast_forward() {
|
||||||
|
let refname = bytes_to_string(local_branch.get().name_bytes())?;
|
||||||
|
match repo.find_reference(&refname) {
|
||||||
|
Ok(mut r) => {
|
||||||
|
merge_fast_forward(repo, &mut r, &commit_to_merge)?;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// The branch doesn't exist, so set the reference to the commit directly. Usually
|
||||||
|
// this is because you are pulling into an empty repository.
|
||||||
|
repo.reference(
|
||||||
|
&refname,
|
||||||
|
commit_to_merge.id(),
|
||||||
|
true,
|
||||||
|
&format!("Setting {} to {}", refname, commit_to_merge.id()),
|
||||||
|
)?;
|
||||||
|
repo.set_head(&refname)?;
|
||||||
|
repo.checkout_head(Some(
|
||||||
|
git2::build::CheckoutBuilder::default()
|
||||||
|
.allow_conflicts(true)
|
||||||
|
.conflict_style_merge(true)
|
||||||
|
.force(),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if analysis.0.is_normal() {
|
||||||
|
let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
|
||||||
|
merge_normal(repo, &head_commit, commit_to_merge)?;
|
||||||
|
} else {
|
||||||
|
debug!("Skipping merge. Nothing to do")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn merge_fast_forward(
|
||||||
|
repo: &Repository,
|
||||||
|
local_reference: &mut Reference,
|
||||||
|
remote_commit: &AnnotatedCommit,
|
||||||
|
) -> crate::error::Result<()> {
|
||||||
|
info!("Performing fast forward");
|
||||||
|
let name = match local_reference.name() {
|
||||||
|
Some(s) => s.to_string(),
|
||||||
|
None => String::from_utf8_lossy(local_reference.name_bytes()).to_string(),
|
||||||
|
};
|
||||||
|
let msg = format!("Fast-Forward: Setting {} to id: {}", name, remote_commit.id());
|
||||||
|
local_reference.set_target(remote_commit.id(), &msg)?;
|
||||||
|
repo.set_head(&name)?;
|
||||||
|
repo.checkout_head(Some(
|
||||||
|
git2::build::CheckoutBuilder::default()
|
||||||
|
// For some reason, the force is required to make the working directory actually get
|
||||||
|
// updated I suspect we should be adding some logic to handle dirty working directory
|
||||||
|
// states, but this is just an example so maybe not.
|
||||||
|
.force(),
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn merge_normal(
|
||||||
|
repo: &Repository,
|
||||||
|
local: &AnnotatedCommit,
|
||||||
|
remote: &AnnotatedCommit,
|
||||||
|
) -> crate::error::Result<()> {
|
||||||
|
info!("Performing normal merge");
|
||||||
|
let local_tree = repo.find_commit(local.id())?.tree()?;
|
||||||
|
let remote_tree = repo.find_commit(remote.id())?.tree()?;
|
||||||
|
let ancestor = repo.find_commit(repo.merge_base(local.id(), remote.id())?)?.tree()?;
|
||||||
|
|
||||||
|
let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?;
|
||||||
|
|
||||||
|
if idx.has_conflicts() {
|
||||||
|
let conflicts = idx.conflicts()?;
|
||||||
|
for conflict in conflicts {
|
||||||
|
if let Ok(conflict) = conflict {
|
||||||
|
print_conflict(&conflict);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(MergeConflicts);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?;
|
||||||
|
// now create the merge commit
|
||||||
|
let msg = format!("Merge: {} into {}", remote.id(), local.id());
|
||||||
|
let sig = repo.signature()?;
|
||||||
|
let local_commit = repo.find_commit(local.id())?;
|
||||||
|
let remote_commit = repo.find_commit(remote.id())?;
|
||||||
|
|
||||||
|
// Do our merge commit and set current branch head to that commit.
|
||||||
|
let _merge_commit = repo.commit(
|
||||||
|
Some("HEAD"),
|
||||||
|
&sig,
|
||||||
|
&sig,
|
||||||
|
&msg,
|
||||||
|
&result_tree,
|
||||||
|
&[&local_commit, &remote_commit],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Set working tree to match head.
|
||||||
|
repo.checkout_head(None)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_conflict(conflict: &git2::IndexConflict) {
|
||||||
|
let ancestor = conflict.ancestor.as_ref().map(path_from_index_entry);
|
||||||
|
let ours = conflict.our.as_ref().map(path_from_index_entry);
|
||||||
|
let theirs = conflict.their.as_ref().map(path_from_index_entry);
|
||||||
|
|
||||||
|
println!("Conflict detected:");
|
||||||
|
if let Some(path) = ancestor {
|
||||||
|
println!(" Common ancestor: {:?}", path);
|
||||||
|
}
|
||||||
|
if let Some(path) = ours {
|
||||||
|
println!(" Ours: {:?}", path);
|
||||||
|
}
|
||||||
|
if let Some(path) = theirs {
|
||||||
|
println!(" Theirs: {:?}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_from_index_entry(entry: &IndexEntry) -> String {
|
||||||
|
String::from_utf8_lossy(entry.path.as_slice()).into_owned()
|
||||||
|
}
|
||||||
@@ -15,41 +15,19 @@ pub enum PullResult {
|
|||||||
Success { message: String },
|
Success { message: String },
|
||||||
UpToDate,
|
UpToDate,
|
||||||
NeedsCredentials { url: String, error: Option<String> },
|
NeedsCredentials { url: String, error: Option<String> },
|
||||||
Diverged { remote: String, branch: String },
|
|
||||||
UncommittedChanges,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_uncommitted_changes(dir: &Path) -> Result<bool> {
|
pub fn git_pull(dir: &Path) -> Result<PullResult> {
|
||||||
let repo = open_repo(dir)?;
|
let repo = open_repo(dir)?;
|
||||||
let mut opts = git2::StatusOptions::new();
|
let branch_name = get_current_branch_name(&repo)?;
|
||||||
opts.include_ignored(false).include_untracked(false);
|
let remote = get_default_remote_in_repo(&repo)?;
|
||||||
let statuses = repo.statuses(Some(&mut opts))?;
|
let remote_name = remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?;
|
||||||
Ok(statuses.iter().any(|e| e.status() != git2::Status::CURRENT))
|
let remote_url = remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?;
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn git_pull(dir: &Path) -> Result<PullResult> {
|
let out = new_binary_command(dir)?
|
||||||
if has_uncommitted_changes(dir)? {
|
|
||||||
return Ok(PullResult::UncommittedChanges);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract all git2 data before any await points (git2 types are not Send)
|
|
||||||
let (branch_name, remote_name, remote_url) = {
|
|
||||||
let repo = open_repo(dir)?;
|
|
||||||
let branch_name = get_current_branch_name(&repo)?;
|
|
||||||
let remote = get_default_remote_in_repo(&repo)?;
|
|
||||||
let remote_name =
|
|
||||||
remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?.to_string();
|
|
||||||
let remote_url =
|
|
||||||
remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?.to_string();
|
|
||||||
(branch_name, remote_name, remote_url)
|
|
||||||
};
|
|
||||||
|
|
||||||
let out = new_binary_command(dir)
|
|
||||||
.await?
|
|
||||||
.args(["pull", &remote_name, &branch_name])
|
.args(["pull", &remote_name, &branch_name])
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
.env("GIT_TERMINAL_PROMPT", "0")
|
||||||
.output()
|
.output()
|
||||||
.await
|
|
||||||
.map_err(|e| GenericError(format!("failed to run git pull: {e}")))?;
|
.map_err(|e| GenericError(format!("failed to run git pull: {e}")))?;
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
@@ -70,13 +48,6 @@ pub async fn git_pull(dir: &Path) -> Result<PullResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !out.status.success() {
|
if !out.status.success() {
|
||||||
let combined_lower = combined.to_lowercase();
|
|
||||||
if combined_lower.contains("cannot fast-forward")
|
|
||||||
|| combined_lower.contains("not possible to fast-forward")
|
|
||||||
|| combined_lower.contains("diverged")
|
|
||||||
{
|
|
||||||
return Ok(PullResult::Diverged { remote: remote_name, branch: branch_name });
|
|
||||||
}
|
|
||||||
return Err(GenericError(format!("Failed to pull {combined}")));
|
return Err(GenericError(format!("Failed to pull {combined}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,65 +58,6 @@ pub async fn git_pull(dir: &Path) -> Result<PullResult> {
|
|||||||
Ok(PullResult::Success { message: format!("Pulled from {}/{}", remote_name, branch_name) })
|
Ok(PullResult::Success { message: format!("Pulled from {}/{}", remote_name, branch_name) })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn git_pull_force_reset(dir: &Path, remote: &str, branch: &str) -> Result<PullResult> {
|
|
||||||
// Step 1: fetch the remote
|
|
||||||
let fetch_out = new_binary_command(dir)
|
|
||||||
.await?
|
|
||||||
.args(["fetch", remote])
|
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.map_err(|e| GenericError(format!("failed to run git fetch: {e}")))?;
|
|
||||||
|
|
||||||
if !fetch_out.status.success() {
|
|
||||||
let stderr = String::from_utf8_lossy(&fetch_out.stderr);
|
|
||||||
return Err(GenericError(format!("Failed to fetch: {stderr}")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: reset --hard to remote/branch
|
|
||||||
let ref_name = format!("{}/{}", remote, branch);
|
|
||||||
let reset_out = new_binary_command(dir)
|
|
||||||
.await?
|
|
||||||
.args(["reset", "--hard", &ref_name])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.map_err(|e| GenericError(format!("failed to run git reset: {e}")))?;
|
|
||||||
|
|
||||||
if !reset_out.status.success() {
|
|
||||||
let stderr = String::from_utf8_lossy(&reset_out.stderr);
|
|
||||||
return Err(GenericError(format!("Failed to reset: {}", stderr.trim())));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(PullResult::Success { message: format!("Reset to {}/{}", remote, branch) })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn git_pull_merge(dir: &Path, remote: &str, branch: &str) -> Result<PullResult> {
|
|
||||||
let out = new_binary_command(dir)
|
|
||||||
.await?
|
|
||||||
.args(["pull", "--no-rebase", remote, branch])
|
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.map_err(|e| GenericError(format!("failed to run git pull --no-rebase: {e}")))?;
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
||||||
let combined = format!("{}{}", stdout, stderr);
|
|
||||||
|
|
||||||
info!("Pull merge status={} {combined}", out.status);
|
|
||||||
|
|
||||||
if !out.status.success() {
|
|
||||||
if combined.to_lowercase().contains("conflict") {
|
|
||||||
return Err(GenericError(
|
|
||||||
"Merge conflicts detected. Please resolve them manually.".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return Err(GenericError(format!("Failed to merge pull: {}", combined.trim())));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(PullResult::Success { message: format!("Merged from {}/{}", remote, branch) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub(crate) fn git_pull_old(dir: &Path) -> Result<PullResult> {
|
// pub(crate) fn git_pull_old(dir: &Path) -> Result<PullResult> {
|
||||||
// let repo = open_repo(dir)?;
|
// let repo = open_repo(dir)?;
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -17,25 +17,17 @@ pub enum PushResult {
|
|||||||
NeedsCredentials { url: String, error: Option<String> },
|
NeedsCredentials { url: String, error: Option<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn git_push(dir: &Path) -> Result<PushResult> {
|
pub fn git_push(dir: &Path) -> Result<PushResult> {
|
||||||
// Extract all git2 data before any await points (git2 types are not Send)
|
let repo = open_repo(dir)?;
|
||||||
let (branch_name, remote_name, remote_url) = {
|
let branch_name = get_current_branch_name(&repo)?;
|
||||||
let repo = open_repo(dir)?;
|
let remote = get_default_remote_for_push_in_repo(&repo)?;
|
||||||
let branch_name = get_current_branch_name(&repo)?;
|
let remote_name = remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?;
|
||||||
let remote = get_default_remote_for_push_in_repo(&repo)?;
|
let remote_url = remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?;
|
||||||
let remote_name =
|
|
||||||
remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?.to_string();
|
|
||||||
let remote_url =
|
|
||||||
remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?.to_string();
|
|
||||||
(branch_name, remote_name, remote_url)
|
|
||||||
};
|
|
||||||
|
|
||||||
let out = new_binary_command(dir)
|
let out = new_binary_command(dir)?
|
||||||
.await?
|
|
||||||
.args(["push", &remote_name, &branch_name])
|
.args(["push", &remote_name, &branch_name])
|
||||||
.env("GIT_TERMINAL_PROMPT", "0")
|
.env("GIT_TERMINAL_PROMPT", "0")
|
||||||
.output()
|
.output()
|
||||||
.await
|
|
||||||
.map_err(|e| GenericError(format!("failed to run git push: {e}")))?;
|
.map_err(|e| GenericError(format!("failed to run git push: {e}")))?;
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
use crate::binary::new_binary_command;
|
|
||||||
use crate::error::Error::GenericError;
|
|
||||||
use crate::error::Result;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
pub async fn git_reset_changes(dir: &Path) -> Result<()> {
|
|
||||||
let out = new_binary_command(dir)
|
|
||||||
.await?
|
|
||||||
.args(["reset", "--hard", "HEAD"])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.map_err(|e| GenericError(format!("failed to run git reset: {e}")))?;
|
|
||||||
|
|
||||||
if !out.status.success() {
|
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
||||||
return Err(GenericError(format!("Failed to reset: {}", stderr.trim())));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -18,8 +18,6 @@ pub struct GitStatusSummary {
|
|||||||
pub origins: Vec<String>,
|
pub origins: Vec<String>,
|
||||||
pub local_branches: Vec<String>,
|
pub local_branches: Vec<String>,
|
||||||
pub remote_branches: Vec<String>,
|
pub remote_branches: Vec<String>,
|
||||||
pub ahead: u32,
|
|
||||||
pub behind: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||||
@@ -162,18 +160,6 @@ pub fn git_status(dir: &Path) -> crate::error::Result<GitStatusSummary> {
|
|||||||
let local_branches = local_branch_names(&repo)?;
|
let local_branches = local_branch_names(&repo)?;
|
||||||
let remote_branches = remote_branch_names(&repo)?;
|
let remote_branches = remote_branch_names(&repo)?;
|
||||||
|
|
||||||
// Compute ahead/behind relative to remote tracking branch
|
|
||||||
let (ahead, behind) = (|| -> Option<(usize, usize)> {
|
|
||||||
let head = repo.head().ok()?;
|
|
||||||
let local_oid = head.target()?;
|
|
||||||
let branch_name = head.shorthand()?;
|
|
||||||
let upstream_ref =
|
|
||||||
repo.find_branch(&format!("origin/{branch_name}"), git2::BranchType::Remote).ok()?;
|
|
||||||
let upstream_oid = upstream_ref.get().target()?;
|
|
||||||
repo.graph_ahead_behind(local_oid, upstream_oid).ok()
|
|
||||||
})()
|
|
||||||
.unwrap_or((0, 0));
|
|
||||||
|
|
||||||
Ok(GitStatusSummary {
|
Ok(GitStatusSummary {
|
||||||
entries,
|
entries,
|
||||||
origins,
|
origins,
|
||||||
@@ -182,7 +168,5 @@ pub fn git_status(dir: &Path) -> crate::error::Result<GitStatusSummary> {
|
|||||||
head_ref_shorthand,
|
head_ref_shorthand,
|
||||||
local_branches,
|
local_branches,
|
||||||
remote_branches,
|
remote_branches,
|
||||||
ahead: ahead as u32,
|
|
||||||
behind: behind as u32,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ pub(crate) fn remote_branch_names(repo: &Repository) -> Result<Vec<String>> {
|
|||||||
Ok(branches)
|
Ok(branches)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_branch_by_name<'s>(repo: &'s Repository, name: &str) -> Result<Branch<'s>> {
|
||||||
|
Ok(repo.find_branch(name, BranchType::Local)?)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn bytes_to_string(bytes: &[u8]) -> Result<String> {
|
pub(crate) fn bytes_to_string(bytes: &[u8]) -> Result<String> {
|
||||||
Ok(String::from_utf8(bytes.to_vec())?)
|
Ok(String::from_utf8(bytes.to_vec())?)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,5 @@ tokio-stream = "0.1.14"
|
|||||||
tonic = { version = "0.12.3", default-features = false, features = ["transport"] }
|
tonic = { version = "0.12.3", default-features = false, features = ["transport"] }
|
||||||
tonic-reflection = "0.12.3"
|
tonic-reflection = "0.12.3"
|
||||||
uuid = { version = "1.7.0", features = ["v4"] }
|
uuid = { version = "1.7.0", features = ["v4"] }
|
||||||
yaak-common = { workspace = true }
|
|
||||||
yaak-tls = { workspace = true }
|
yaak-tls = { workspace = true }
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
|
|||||||
@@ -115,18 +115,14 @@ impl GrpcConnection {
|
|||||||
Ok(client.unary(req, path, codec).await?)
|
Ok(client.unary(req, path, codec).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn streaming<F>(
|
pub async fn streaming(
|
||||||
&self,
|
&self,
|
||||||
service: &str,
|
service: &str,
|
||||||
method: &str,
|
method: &str,
|
||||||
stream: ReceiverStream<String>,
|
stream: ReceiverStream<String>,
|
||||||
metadata: &BTreeMap<String, String>,
|
metadata: &BTreeMap<String, String>,
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
on_message: F,
|
) -> Result<Response<Streaming<DynamicMessage>>> {
|
||||||
) -> Result<Response<Streaming<DynamicMessage>>>
|
|
||||||
where
|
|
||||||
F: Fn(std::result::Result<String, String>) + Send + Sync + Clone + 'static,
|
|
||||||
{
|
|
||||||
let method = &self.method(&service, &method).await?;
|
let method = &self.method(&service, &method).await?;
|
||||||
let mapped_stream = {
|
let mapped_stream = {
|
||||||
let input_message = method.input();
|
let input_message = method.input();
|
||||||
@@ -135,39 +131,31 @@ impl GrpcConnection {
|
|||||||
let md = metadata.clone();
|
let md = metadata.clone();
|
||||||
let use_reflection = self.use_reflection.clone();
|
let use_reflection = self.use_reflection.clone();
|
||||||
let client_cert = client_cert.clone();
|
let client_cert = client_cert.clone();
|
||||||
stream
|
stream.filter_map(move |json| {
|
||||||
.then(move |json| {
|
let pool = pool.clone();
|
||||||
let pool = pool.clone();
|
let uri = uri.clone();
|
||||||
let uri = uri.clone();
|
let input_message = input_message.clone();
|
||||||
let input_message = input_message.clone();
|
let md = md.clone();
|
||||||
let md = md.clone();
|
let use_reflection = use_reflection.clone();
|
||||||
let use_reflection = use_reflection.clone();
|
let client_cert = client_cert.clone();
|
||||||
let client_cert = client_cert.clone();
|
tokio::runtime::Handle::current().block_on(async move {
|
||||||
let on_message = on_message.clone();
|
if use_reflection {
|
||||||
let json_clone = json.clone();
|
if let Err(e) =
|
||||||
async move {
|
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
||||||
if use_reflection {
|
{
|
||||||
if let Err(e) =
|
warn!("Failed to resolve Any types: {e}");
|
||||||
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
|
||||||
{
|
|
||||||
warn!("Failed to resolve Any types: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let mut de = Deserializer::from_str(&json);
|
}
|
||||||
match DynamicMessage::deserialize(input_message, &mut de) {
|
let mut de = Deserializer::from_str(&json);
|
||||||
Ok(m) => {
|
match DynamicMessage::deserialize(input_message, &mut de) {
|
||||||
on_message(Ok(json_clone));
|
Ok(m) => Some(m),
|
||||||
Some(m)
|
Err(e) => {
|
||||||
}
|
warn!("Failed to deserialize message: {e}");
|
||||||
Err(e) => {
|
None
|
||||||
warn!("Failed to deserialize message: {e}");
|
|
||||||
on_message(Err(e.to_string()));
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter_map(|x| x)
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
||||||
@@ -181,18 +169,14 @@ impl GrpcConnection {
|
|||||||
Ok(client.streaming(req, path, codec).await?)
|
Ok(client.streaming(req, path, codec).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn client_streaming<F>(
|
pub async fn client_streaming(
|
||||||
&self,
|
&self,
|
||||||
service: &str,
|
service: &str,
|
||||||
method: &str,
|
method: &str,
|
||||||
stream: ReceiverStream<String>,
|
stream: ReceiverStream<String>,
|
||||||
metadata: &BTreeMap<String, String>,
|
metadata: &BTreeMap<String, String>,
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
on_message: F,
|
) -> Result<Response<DynamicMessage>> {
|
||||||
) -> Result<Response<DynamicMessage>>
|
|
||||||
where
|
|
||||||
F: Fn(std::result::Result<String, String>) + Send + Sync + Clone + 'static,
|
|
||||||
{
|
|
||||||
let method = &self.method(&service, &method).await?;
|
let method = &self.method(&service, &method).await?;
|
||||||
let mapped_stream = {
|
let mapped_stream = {
|
||||||
let input_message = method.input();
|
let input_message = method.input();
|
||||||
@@ -201,39 +185,31 @@ impl GrpcConnection {
|
|||||||
let md = metadata.clone();
|
let md = metadata.clone();
|
||||||
let use_reflection = self.use_reflection.clone();
|
let use_reflection = self.use_reflection.clone();
|
||||||
let client_cert = client_cert.clone();
|
let client_cert = client_cert.clone();
|
||||||
stream
|
stream.filter_map(move |json| {
|
||||||
.then(move |json| {
|
let pool = pool.clone();
|
||||||
let pool = pool.clone();
|
let uri = uri.clone();
|
||||||
let uri = uri.clone();
|
let input_message = input_message.clone();
|
||||||
let input_message = input_message.clone();
|
let md = md.clone();
|
||||||
let md = md.clone();
|
let use_reflection = use_reflection.clone();
|
||||||
let use_reflection = use_reflection.clone();
|
let client_cert = client_cert.clone();
|
||||||
let client_cert = client_cert.clone();
|
tokio::runtime::Handle::current().block_on(async move {
|
||||||
let on_message = on_message.clone();
|
if use_reflection {
|
||||||
let json_clone = json.clone();
|
if let Err(e) =
|
||||||
async move {
|
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
||||||
if use_reflection {
|
{
|
||||||
if let Err(e) =
|
warn!("Failed to resolve Any types: {e}");
|
||||||
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
|
||||||
{
|
|
||||||
warn!("Failed to resolve Any types: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let mut de = Deserializer::from_str(&json);
|
}
|
||||||
match DynamicMessage::deserialize(input_message, &mut de) {
|
let mut de = Deserializer::from_str(&json);
|
||||||
Ok(m) => {
|
match DynamicMessage::deserialize(input_message, &mut de) {
|
||||||
on_message(Ok(json_clone));
|
Ok(m) => Some(m),
|
||||||
Some(m)
|
Err(e) => {
|
||||||
}
|
warn!("Failed to deserialize message: {e}");
|
||||||
Err(e) => {
|
None
|
||||||
warn!("Failed to deserialize message: {e}");
|
|
||||||
on_message(Err(e.to_string()));
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter_map(|x| x)
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
||||||
@@ -340,9 +316,10 @@ impl GrpcHandle {
|
|||||||
metadata: &BTreeMap<String, String>,
|
metadata: &BTreeMap<String, String>,
|
||||||
validate_certificates: bool,
|
validate_certificates: bool,
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
|
skip_cache: bool,
|
||||||
) -> Result<Vec<ServiceDefinition>> {
|
) -> Result<Vec<ServiceDefinition>> {
|
||||||
// Ensure we have a pool; reflect only if missing
|
// Ensure we have a pool; reflect only if missing
|
||||||
if self.get_pool(id, uri, proto_files).is_none() {
|
if skip_cache || self.get_pool(id, uri, proto_files).is_none() {
|
||||||
info!("Reflecting gRPC services for {} at {}", id, uri);
|
info!("Reflecting gRPC services for {} at {}", id, uri);
|
||||||
self.reflect(id, uri, proto_files, metadata, validate_certificates, client_cert)
|
self.reflect(id, uri, proto_files, metadata, validate_certificates, client_cert)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
use tokio::process::Command;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tonic::codegen::http::uri::PathAndQuery;
|
use tonic::codegen::http::uri::PathAndQuery;
|
||||||
use tonic::transport::Uri;
|
use tonic::transport::Uri;
|
||||||
use tonic_reflection::pb::v1::server_reflection_request::MessageRequest;
|
use tonic_reflection::pb::v1::server_reflection_request::MessageRequest;
|
||||||
use tonic_reflection::pb::v1::server_reflection_response::MessageResponse;
|
use tonic_reflection::pb::v1::server_reflection_response::MessageResponse;
|
||||||
use yaak_common::command::new_xplatform_command;
|
|
||||||
use yaak_tls::ClientCertificateConfig;
|
use yaak_tls::ClientCertificateConfig;
|
||||||
|
|
||||||
pub async fn fill_pool_from_files(
|
pub async fn fill_pool_from_files(
|
||||||
@@ -91,11 +91,11 @@ pub async fn fill_pool_from_files(
|
|||||||
|
|
||||||
info!("Invoking protoc with {}", args.join(" "));
|
info!("Invoking protoc with {}", args.join(" "));
|
||||||
|
|
||||||
let mut cmd = new_xplatform_command(&config.protoc_bin_path);
|
let out = Command::new(&config.protoc_bin_path)
|
||||||
cmd.args(&args);
|
.args(&args)
|
||||||
|
.output()
|
||||||
let out =
|
.await
|
||||||
cmd.output().await.map_err(|e| GenericError(format!("Failed to run protoc: {}", e)))?;
|
.map_err(|e| GenericError(format!("Failed to run protoc: {}", e)))?;
|
||||||
|
|
||||||
if !out.status.success() {
|
if !out.status.success() {
|
||||||
return Err(GenericError(format!(
|
return Err(GenericError(format!(
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ use crate::dns::LocalhostResolver;
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use reqwest::{Client, Proxy, redirect};
|
use reqwest::{Client, Proxy, redirect};
|
||||||
use std::sync::Arc;
|
|
||||||
use yaak_models::models::DnsOverride;
|
|
||||||
use yaak_tls::{ClientCertificateConfig, get_tls_config};
|
use yaak_tls::{ClientCertificateConfig, get_tls_config};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -30,14 +28,10 @@ pub struct HttpConnectionOptions {
|
|||||||
pub validate_certificates: bool,
|
pub validate_certificates: bool,
|
||||||
pub proxy: HttpConnectionProxySetting,
|
pub proxy: HttpConnectionProxySetting,
|
||||||
pub client_certificate: Option<ClientCertificateConfig>,
|
pub client_certificate: Option<ClientCertificateConfig>,
|
||||||
pub dns_overrides: Vec<DnsOverride>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpConnectionOptions {
|
impl HttpConnectionOptions {
|
||||||
/// Build a reqwest Client and return it along with the DNS resolver.
|
pub(crate) fn build_client(&self) -> Result<Client> {
|
||||||
/// The resolver is returned separately so it can be configured per-request
|
|
||||||
/// to emit DNS timing events to the appropriate channel.
|
|
||||||
pub(crate) fn build_client(&self) -> Result<(Client, Arc<LocalhostResolver>)> {
|
|
||||||
let mut client = Client::builder()
|
let mut client = Client::builder()
|
||||||
.connection_verbose(true)
|
.connection_verbose(true)
|
||||||
.redirect(redirect::Policy::none())
|
.redirect(redirect::Policy::none())
|
||||||
@@ -46,19 +40,15 @@ impl HttpConnectionOptions {
|
|||||||
.no_brotli()
|
.no_brotli()
|
||||||
.no_deflate()
|
.no_deflate()
|
||||||
.referer(false)
|
.referer(false)
|
||||||
.tls_info(true)
|
.tls_info(true);
|
||||||
// Disable connection pooling to ensure DNS resolution happens on each request
|
|
||||||
// This is needed so we can emit DNS timing events for each request
|
|
||||||
.pool_max_idle_per_host(0);
|
|
||||||
|
|
||||||
// Configure TLS with optional client certificate
|
// Configure TLS with optional client certificate
|
||||||
let config =
|
let config =
|
||||||
get_tls_config(self.validate_certificates, true, self.client_certificate.clone())?;
|
get_tls_config(self.validate_certificates, true, self.client_certificate.clone())?;
|
||||||
client = client.use_preconfigured_tls(config);
|
client = client.use_preconfigured_tls(config);
|
||||||
|
|
||||||
// Configure DNS resolver - keep a reference to configure per-request
|
// Configure DNS resolver
|
||||||
let resolver = LocalhostResolver::new(self.dns_overrides.clone());
|
client = client.dns_resolver(LocalhostResolver::new());
|
||||||
client = client.dns_resolver(resolver.clone());
|
|
||||||
|
|
||||||
// Configure proxy
|
// Configure proxy
|
||||||
match self.proxy.clone() {
|
match self.proxy.clone() {
|
||||||
@@ -79,7 +69,7 @@ impl HttpConnectionOptions {
|
|||||||
self.client_certificate.is_some()
|
self.client_certificate.is_some()
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok((client.build()?, resolver))
|
Ok(client.build()?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-143
@@ -1,185 +1,53 @@
|
|||||||
use crate::sender::HttpResponseEvent;
|
|
||||||
use hyper_util::client::legacy::connect::dns::{
|
use hyper_util::client::legacy::connect::dns::{
|
||||||
GaiResolver as HyperGaiResolver, Name as HyperName,
|
GaiResolver as HyperGaiResolver, Name as HyperName,
|
||||||
};
|
};
|
||||||
use log::info;
|
|
||||||
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
|
||||||
use tokio::sync::{RwLock, mpsc};
|
|
||||||
use tower_service::Service;
|
use tower_service::Service;
|
||||||
use yaak_models::models::DnsOverride;
|
|
||||||
|
|
||||||
/// Stores resolved addresses for a hostname override
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ResolvedOverride {
|
|
||||||
pub ipv4: Vec<Ipv4Addr>,
|
|
||||||
pub ipv6: Vec<Ipv6Addr>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct LocalhostResolver {
|
pub struct LocalhostResolver {
|
||||||
fallback: HyperGaiResolver,
|
fallback: HyperGaiResolver,
|
||||||
event_tx: Arc<RwLock<Option<mpsc::Sender<HttpResponseEvent>>>>,
|
|
||||||
overrides: Arc<HashMap<String, ResolvedOverride>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalhostResolver {
|
impl LocalhostResolver {
|
||||||
pub fn new(dns_overrides: Vec<DnsOverride>) -> Arc<Self> {
|
pub fn new() -> Arc<Self> {
|
||||||
let resolver = HyperGaiResolver::new();
|
let resolver = HyperGaiResolver::new();
|
||||||
|
Arc::new(Self { fallback: resolver })
|
||||||
// Pre-parse DNS overrides into a lookup map
|
|
||||||
let mut overrides = HashMap::new();
|
|
||||||
for o in dns_overrides {
|
|
||||||
if !o.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let hostname = o.hostname.to_lowercase();
|
|
||||||
|
|
||||||
let ipv4: Vec<Ipv4Addr> =
|
|
||||||
o.ipv4.iter().filter_map(|s| s.parse::<Ipv4Addr>().ok()).collect();
|
|
||||||
|
|
||||||
let ipv6: Vec<Ipv6Addr> =
|
|
||||||
o.ipv6.iter().filter_map(|s| s.parse::<Ipv6Addr>().ok()).collect();
|
|
||||||
|
|
||||||
// Only add if at least one address is valid
|
|
||||||
if !ipv4.is_empty() || !ipv6.is_empty() {
|
|
||||||
overrides.insert(hostname, ResolvedOverride { ipv4, ipv6 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Arc::new(Self {
|
|
||||||
fallback: resolver,
|
|
||||||
event_tx: Arc::new(RwLock::new(None)),
|
|
||||||
overrides: Arc::new(overrides),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the event sender for the current request.
|
|
||||||
/// This should be called before each request to direct DNS events
|
|
||||||
/// to the appropriate channel.
|
|
||||||
pub async fn set_event_sender(&self, tx: Option<mpsc::Sender<HttpResponseEvent>>) {
|
|
||||||
let mut guard = self.event_tx.write().await;
|
|
||||||
*guard = tx;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resolve for LocalhostResolver {
|
impl Resolve for LocalhostResolver {
|
||||||
fn resolve(&self, name: Name) -> Resolving {
|
fn resolve(&self, name: Name) -> Resolving {
|
||||||
let host = name.as_str().to_lowercase();
|
let host = name.as_str().to_lowercase();
|
||||||
let event_tx = self.event_tx.clone();
|
|
||||||
let overrides = self.overrides.clone();
|
|
||||||
|
|
||||||
info!("DNS resolve called for: {}", host);
|
|
||||||
|
|
||||||
// Check for DNS override first
|
|
||||||
if let Some(resolved) = overrides.get(&host) {
|
|
||||||
log::debug!("DNS override found for: {}", host);
|
|
||||||
let hostname = host.clone();
|
|
||||||
let mut addrs: Vec<SocketAddr> = Vec::new();
|
|
||||||
|
|
||||||
// Add IPv4 addresses
|
|
||||||
for ip in &resolved.ipv4 {
|
|
||||||
addrs.push(SocketAddr::new(IpAddr::V4(*ip), 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add IPv6 addresses
|
|
||||||
for ip in &resolved.ipv6 {
|
|
||||||
addrs.push(SocketAddr::new(IpAddr::V6(*ip), 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
let addresses: Vec<String> = addrs.iter().map(|a| a.ip().to_string()).collect();
|
|
||||||
|
|
||||||
return Box::pin(async move {
|
|
||||||
// Emit DNS event for override
|
|
||||||
let guard = event_tx.read().await;
|
|
||||||
if let Some(tx) = guard.as_ref() {
|
|
||||||
let _ = tx
|
|
||||||
.send(HttpResponseEvent::DnsResolved {
|
|
||||||
hostname,
|
|
||||||
addresses,
|
|
||||||
duration: 0,
|
|
||||||
overridden: true,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<Addrs, Box<dyn std::error::Error + Send + Sync>>(Box::new(addrs.into_iter()))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for .localhost suffix
|
|
||||||
let is_localhost = host.ends_with(".localhost");
|
let is_localhost = host.ends_with(".localhost");
|
||||||
if is_localhost {
|
if is_localhost {
|
||||||
let hostname = host.clone();
|
|
||||||
// Port 0 is fine; reqwest replaces it with the URL's explicit
|
// Port 0 is fine; reqwest replaces it with the URL's explicit
|
||||||
// port or the scheme's default (80/443, etc.).
|
// port or the scheme’s default (80/443, etc.).
|
||||||
|
// (See docs note below.)
|
||||||
let addrs: Vec<SocketAddr> = vec![
|
let addrs: Vec<SocketAddr> = vec![
|
||||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
|
||||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0),
|
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0),
|
||||||
];
|
];
|
||||||
|
|
||||||
let addresses: Vec<String> = addrs.iter().map(|a| a.ip().to_string()).collect();
|
|
||||||
|
|
||||||
return Box::pin(async move {
|
return Box::pin(async move {
|
||||||
// Emit DNS event for localhost resolution
|
|
||||||
let guard = event_tx.read().await;
|
|
||||||
if let Some(tx) = guard.as_ref() {
|
|
||||||
let _ = tx
|
|
||||||
.send(HttpResponseEvent::DnsResolved {
|
|
||||||
hostname,
|
|
||||||
addresses,
|
|
||||||
duration: 0,
|
|
||||||
overridden: false,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<Addrs, Box<dyn std::error::Error + Send + Sync>>(Box::new(addrs.into_iter()))
|
Ok::<Addrs, Box<dyn std::error::Error + Send + Sync>>(Box::new(addrs.into_iter()))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to system DNS
|
|
||||||
let mut fallback = self.fallback.clone();
|
let mut fallback = self.fallback.clone();
|
||||||
let name_str = name.as_str().to_string();
|
let name_str = name.as_str().to_string();
|
||||||
let hostname = host.clone();
|
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let start = Instant::now();
|
match HyperName::from_str(&name_str) {
|
||||||
|
Ok(n) => fallback
|
||||||
let result = match HyperName::from_str(&name_str) {
|
.call(n)
|
||||||
Ok(n) => fallback.call(n).await,
|
.await
|
||||||
Err(e) => return Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>),
|
.map(|addrs| Box::new(addrs) as Addrs)
|
||||||
};
|
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync>),
|
||||||
|
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>),
|
||||||
let duration = start.elapsed().as_millis() as u64;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(addrs) => {
|
|
||||||
// Collect addresses for event emission
|
|
||||||
let addr_vec: Vec<SocketAddr> = addrs.collect();
|
|
||||||
let addresses: Vec<String> =
|
|
||||||
addr_vec.iter().map(|a| a.ip().to_string()).collect();
|
|
||||||
|
|
||||||
// Emit DNS event
|
|
||||||
let guard = event_tx.read().await;
|
|
||||||
if let Some(tx) = guard.as_ref() {
|
|
||||||
let _ = tx
|
|
||||||
.send(HttpResponseEvent::DnsResolved {
|
|
||||||
hostname,
|
|
||||||
addresses,
|
|
||||||
duration,
|
|
||||||
overridden: false,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Box::new(addr_vec.into_iter()) as Addrs)
|
|
||||||
}
|
|
||||||
Err(err) => Err(Box::new(err) as Box<dyn std::error::Error + Send + Sync>),
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::client::HttpConnectionOptions;
|
use crate::client::HttpConnectionOptions;
|
||||||
use crate::dns::LocalhostResolver;
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use log::info;
|
use log::info;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
@@ -8,15 +7,8 @@ use std::sync::Arc;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
/// A cached HTTP client along with its DNS resolver.
|
|
||||||
/// The resolver is needed to set the event sender per-request.
|
|
||||||
pub struct CachedClient {
|
|
||||||
pub client: Client,
|
|
||||||
pub resolver: Arc<LocalhostResolver>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct HttpConnectionManager {
|
pub struct HttpConnectionManager {
|
||||||
connections: Arc<RwLock<BTreeMap<String, (CachedClient, Instant)>>>,
|
connections: Arc<RwLock<BTreeMap<String, (Client, Instant)>>>,
|
||||||
ttl: Duration,
|
ttl: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,26 +20,21 @@ impl HttpConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_client(&self, opt: &HttpConnectionOptions) -> Result<CachedClient> {
|
pub async fn get_client(&self, opt: &HttpConnectionOptions) -> Result<Client> {
|
||||||
let mut connections = self.connections.write().await;
|
let mut connections = self.connections.write().await;
|
||||||
let id = opt.id.clone();
|
let id = opt.id.clone();
|
||||||
|
|
||||||
// Clean old connections
|
// Clean old connections
|
||||||
connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl);
|
connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl);
|
||||||
|
|
||||||
if let Some((cached, last_used)) = connections.get_mut(&id) {
|
if let Some((c, last_used)) = connections.get_mut(&id) {
|
||||||
info!("Re-using HTTP client {id}");
|
info!("Re-using HTTP client {id}");
|
||||||
*last_used = Instant::now();
|
*last_used = Instant::now();
|
||||||
return Ok(CachedClient {
|
return Ok(c.clone());
|
||||||
client: cached.client.clone(),
|
|
||||||
resolver: cached.resolver.clone(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (client, resolver) = opt.build_client()?;
|
let c = opt.build_client()?;
|
||||||
let cached = CachedClient { client: client.clone(), resolver: resolver.clone() };
|
connections.insert(id.into(), (c.clone(), Instant::now()));
|
||||||
connections.insert(id.into(), (cached, Instant::now()));
|
Ok(c)
|
||||||
|
|
||||||
Ok(CachedClient { client, resolver })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,7 @@ pub enum HttpResponseEvent {
|
|||||||
},
|
},
|
||||||
SendUrl {
|
SendUrl {
|
||||||
method: String,
|
method: String,
|
||||||
scheme: String,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
host: String,
|
|
||||||
port: u16,
|
|
||||||
path: String,
|
path: String,
|
||||||
query: String,
|
|
||||||
fragment: String,
|
|
||||||
},
|
},
|
||||||
ReceiveUrl {
|
ReceiveUrl {
|
||||||
version: Version,
|
version: Version,
|
||||||
@@ -52,12 +45,6 @@ pub enum HttpResponseEvent {
|
|||||||
ChunkReceived {
|
ChunkReceived {
|
||||||
bytes: usize,
|
bytes: usize,
|
||||||
},
|
},
|
||||||
DnsResolved {
|
|
||||||
hostname: String,
|
|
||||||
addresses: Vec<String>,
|
|
||||||
duration: u64,
|
|
||||||
overridden: bool,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for HttpResponseEvent {
|
impl Display for HttpResponseEvent {
|
||||||
@@ -72,16 +59,7 @@ impl Display for HttpResponseEvent {
|
|||||||
};
|
};
|
||||||
write!(f, "* Redirect {} -> {} ({})", status, url, behavior_str)
|
write!(f, "* Redirect {} -> {} ({})", status, url, behavior_str)
|
||||||
}
|
}
|
||||||
HttpResponseEvent::SendUrl { method, scheme, username, password, host, port, path, query, fragment } => {
|
HttpResponseEvent::SendUrl { method, path } => write!(f, "> {} {}", method, path),
|
||||||
let auth_str = if username.is_empty() && password.is_empty() {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!("{}:{}@", username, password)
|
|
||||||
};
|
|
||||||
let query_str = if query.is_empty() { String::new() } else { format!("?{}", query) };
|
|
||||||
let fragment_str = if fragment.is_empty() { String::new() } else { format!("#{}", fragment) };
|
|
||||||
write!(f, "> {} {}://{}{}:{}{}{}{}", method, scheme, auth_str, host, port, path, query_str, fragment_str)
|
|
||||||
}
|
|
||||||
HttpResponseEvent::ReceiveUrl { version, status } => {
|
HttpResponseEvent::ReceiveUrl { version, status } => {
|
||||||
write!(f, "< {} {}", version_to_str(version), status)
|
write!(f, "< {} {}", version_to_str(version), status)
|
||||||
}
|
}
|
||||||
@@ -89,19 +67,6 @@ impl Display for HttpResponseEvent {
|
|||||||
HttpResponseEvent::HeaderDown(name, value) => write!(f, "< {}: {}", name, value),
|
HttpResponseEvent::HeaderDown(name, value) => write!(f, "< {}: {}", name, value),
|
||||||
HttpResponseEvent::ChunkSent { bytes } => write!(f, "> [{} bytes sent]", bytes),
|
HttpResponseEvent::ChunkSent { bytes } => write!(f, "> [{} bytes sent]", bytes),
|
||||||
HttpResponseEvent::ChunkReceived { bytes } => write!(f, "< [{} bytes received]", bytes),
|
HttpResponseEvent::ChunkReceived { bytes } => write!(f, "< [{} bytes received]", bytes),
|
||||||
HttpResponseEvent::DnsResolved { hostname, addresses, duration, overridden } => {
|
|
||||||
if *overridden {
|
|
||||||
write!(f, "* DNS override {} -> {}", hostname, addresses.join(", "))
|
|
||||||
} else {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"* DNS resolved {} to {} ({}ms)",
|
|
||||||
hostname,
|
|
||||||
addresses.join(", "),
|
|
||||||
duration
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,9 +85,7 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
|
|||||||
RedirectBehavior::DropBody => "drop_body".to_string(),
|
RedirectBehavior::DropBody => "drop_body".to_string(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
HttpResponseEvent::SendUrl { method, scheme, username, password, host, port, path, query, fragment } => {
|
HttpResponseEvent::SendUrl { method, path } => D::SendUrl { method, path },
|
||||||
D::SendUrl { method, scheme, username, password, host, port, path, query, fragment }
|
|
||||||
}
|
|
||||||
HttpResponseEvent::ReceiveUrl { version, status } => {
|
HttpResponseEvent::ReceiveUrl { version, status } => {
|
||||||
D::ReceiveUrl { version: format!("{:?}", version), status }
|
D::ReceiveUrl { version: format!("{:?}", version), status }
|
||||||
}
|
}
|
||||||
@@ -130,9 +93,6 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
|
|||||||
HttpResponseEvent::HeaderDown(name, value) => D::HeaderDown { name, value },
|
HttpResponseEvent::HeaderDown(name, value) => D::HeaderDown { name, value },
|
||||||
HttpResponseEvent::ChunkSent { bytes } => D::ChunkSent { bytes },
|
HttpResponseEvent::ChunkSent { bytes } => D::ChunkSent { bytes },
|
||||||
HttpResponseEvent::ChunkReceived { bytes } => D::ChunkReceived { bytes },
|
HttpResponseEvent::ChunkReceived { bytes } => D::ChunkReceived { bytes },
|
||||||
HttpResponseEvent::DnsResolved { hostname, addresses, duration, overridden } => {
|
|
||||||
D::DnsResolved { hostname, addresses, duration, overridden }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,9 +354,6 @@ impl HttpSender for ReqwestSender {
|
|||||||
|
|
||||||
// Add headers
|
// Add headers
|
||||||
for header in request.headers {
|
for header in request.headers {
|
||||||
if header.0.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
req_builder = req_builder.header(&header.0, &header.1);
|
req_builder = req_builder.header(&header.0, &header.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,15 +390,8 @@ impl HttpSender for ReqwestSender {
|
|||||||
));
|
));
|
||||||
|
|
||||||
send_event(HttpResponseEvent::SendUrl {
|
send_event(HttpResponseEvent::SendUrl {
|
||||||
method: sendable_req.method().to_string(),
|
|
||||||
scheme: sendable_req.url().scheme().to_string(),
|
|
||||||
username: sendable_req.url().username().to_string(),
|
|
||||||
password: sendable_req.url().password().unwrap_or_default().to_string(),
|
|
||||||
host: sendable_req.url().host_str().unwrap_or_default().to_string(),
|
|
||||||
port: sendable_req.url().port_or_known_default().unwrap_or(0),
|
|
||||||
path: sendable_req.url().path().to_string(),
|
path: sendable_req.url().path().to_string(),
|
||||||
query: sendable_req.url().query().unwrap_or_default().to_string(),
|
method: sendable_req.method().to_string(),
|
||||||
fragment: sendable_req.url().fragment().unwrap_or_default().to_string(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut request_headers = Vec::new();
|
let mut request_headers = Vec::new();
|
||||||
|
|||||||
@@ -168,7 +168,6 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
response.drain().await?;
|
response.drain().await?;
|
||||||
|
|
||||||
// Update the request URL
|
// Update the request URL
|
||||||
let previous_url = current_url.clone();
|
|
||||||
current_url = if location.starts_with("http://") || location.starts_with("https://") {
|
current_url = if location.starts_with("http://") || location.starts_with("https://") {
|
||||||
// Absolute URL
|
// Absolute URL
|
||||||
location
|
location
|
||||||
@@ -182,8 +181,6 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
format!("{}/{}", base_path, location)
|
format!("{}/{}", base_path, location)
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::remove_sensitive_headers(&mut current_headers, &previous_url, ¤t_url);
|
|
||||||
|
|
||||||
// Determine redirect behavior based on status code and method
|
// Determine redirect behavior based on status code and method
|
||||||
let behavior = if status == 303 {
|
let behavior = if status == 303 {
|
||||||
// 303 See Other always changes to GET
|
// 303 See Other always changes to GET
|
||||||
@@ -223,33 +220,6 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove sensitive headers when redirecting to a different host.
|
|
||||||
/// This matches reqwest's `remove_sensitive_headers()` behavior and prevents
|
|
||||||
/// credentials from being forwarded to third-party servers (e.g., an
|
|
||||||
/// Authorization header sent from an API redirect to an S3 bucket).
|
|
||||||
fn remove_sensitive_headers(
|
|
||||||
headers: &mut Vec<(String, String)>,
|
|
||||||
previous_url: &str,
|
|
||||||
next_url: &str,
|
|
||||||
) {
|
|
||||||
let previous_host = Url::parse(previous_url).ok().and_then(|u| {
|
|
||||||
u.host_str().map(|h| format!("{}:{}", h, u.port_or_known_default().unwrap_or(0)))
|
|
||||||
});
|
|
||||||
let next_host = Url::parse(next_url).ok().and_then(|u| {
|
|
||||||
u.host_str().map(|h| format!("{}:{}", h, u.port_or_known_default().unwrap_or(0)))
|
|
||||||
});
|
|
||||||
if previous_host != next_host {
|
|
||||||
headers.retain(|h| {
|
|
||||||
let name_lower = h.0.to_lowercase();
|
|
||||||
name_lower != "authorization"
|
|
||||||
&& name_lower != "cookie"
|
|
||||||
&& name_lower != "cookie2"
|
|
||||||
&& name_lower != "proxy-authorization"
|
|
||||||
&& name_lower != "www-authenticate"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a status code indicates a redirect
|
/// Check if a status code indicates a redirect
|
||||||
fn is_redirect(status: u16) -> bool {
|
fn is_redirect(status: u16) -> bool {
|
||||||
matches!(status, 301 | 302 | 303 | 307 | 308)
|
matches!(status, 301 | 302 | 303 | 307 | 308)
|
||||||
@@ -299,20 +269,9 @@ mod tests {
|
|||||||
use tokio::io::AsyncRead;
|
use tokio::io::AsyncRead;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
/// Captured request metadata for test assertions
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
struct CapturedRequest {
|
|
||||||
url: String,
|
|
||||||
method: String,
|
|
||||||
headers: Vec<(String, String)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mock sender for testing
|
/// Mock sender for testing
|
||||||
struct MockSender {
|
struct MockSender {
|
||||||
responses: Arc<Mutex<Vec<MockResponse>>>,
|
responses: Arc<Mutex<Vec<MockResponse>>>,
|
||||||
/// Captured requests for assertions
|
|
||||||
captured_requests: Arc<Mutex<Vec<CapturedRequest>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MockResponse {
|
struct MockResponse {
|
||||||
@@ -323,10 +282,7 @@ mod tests {
|
|||||||
|
|
||||||
impl MockSender {
|
impl MockSender {
|
||||||
fn new(responses: Vec<MockResponse>) -> Self {
|
fn new(responses: Vec<MockResponse>) -> Self {
|
||||||
Self {
|
Self { responses: Arc::new(Mutex::new(responses)) }
|
||||||
responses: Arc::new(Mutex::new(responses)),
|
|
||||||
captured_requests: Arc::new(Mutex::new(Vec::new())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,16 +290,9 @@ mod tests {
|
|||||||
impl HttpSender for MockSender {
|
impl HttpSender for MockSender {
|
||||||
async fn send(
|
async fn send(
|
||||||
&self,
|
&self,
|
||||||
request: SendableHttpRequest,
|
_request: SendableHttpRequest,
|
||||||
_event_tx: mpsc::Sender<HttpResponseEvent>,
|
_event_tx: mpsc::Sender<HttpResponseEvent>,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
// Capture the request metadata for later assertions
|
|
||||||
self.captured_requests.lock().await.push(CapturedRequest {
|
|
||||||
url: request.url.clone(),
|
|
||||||
method: request.method.clone(),
|
|
||||||
headers: request.headers.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut responses = self.responses.lock().await;
|
let mut responses = self.responses.lock().await;
|
||||||
if responses.is_empty() {
|
if responses.is_empty() {
|
||||||
Err(crate::error::Error::RequestError("No more mock responses".to_string()))
|
Err(crate::error::Error::RequestError("No more mock responses".to_string()))
|
||||||
@@ -393,8 +342,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_transaction_single_redirect() {
|
async fn test_transaction_single_redirect() {
|
||||||
let redirect_headers =
|
let redirect_headers = vec![("Location".to_string(), "https://example.com/new".to_string())];
|
||||||
vec![("Location".to_string(), "https://example.com/new".to_string())];
|
|
||||||
|
|
||||||
let responses = vec![
|
let responses = vec![
|
||||||
MockResponse { status: 302, headers: redirect_headers, body: vec![] },
|
MockResponse { status: 302, headers: redirect_headers, body: vec![] },
|
||||||
@@ -425,8 +373,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_transaction_max_redirects_exceeded() {
|
async fn test_transaction_max_redirects_exceeded() {
|
||||||
let redirect_headers =
|
let redirect_headers = vec![("Location".to_string(), "https://example.com/loop".to_string())];
|
||||||
vec![("Location".to_string(), "https://example.com/loop".to_string())];
|
|
||||||
|
|
||||||
// Create more redirects than allowed
|
// Create more redirects than allowed
|
||||||
let responses: Vec<MockResponse> = (0..12)
|
let responses: Vec<MockResponse> = (0..12)
|
||||||
@@ -578,8 +525,7 @@ mod tests {
|
|||||||
_request: SendableHttpRequest,
|
_request: SendableHttpRequest,
|
||||||
_event_tx: mpsc::Sender<HttpResponseEvent>,
|
_event_tx: mpsc::Sender<HttpResponseEvent>,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
let headers =
|
let headers = vec![("set-cookie".to_string(), "session=xyz789; Path=/".to_string())];
|
||||||
vec![("set-cookie".to_string(), "session=xyz789; Path=/".to_string())];
|
|
||||||
|
|
||||||
let body_stream: Pin<Box<dyn AsyncRead + Send>> =
|
let body_stream: Pin<Box<dyn AsyncRead + Send>> =
|
||||||
Box::pin(std::io::Cursor::new(vec![]));
|
Box::pin(std::io::Cursor::new(vec![]));
|
||||||
@@ -638,10 +584,7 @@ mod tests {
|
|||||||
let headers = vec![
|
let headers = vec![
|
||||||
("set-cookie".to_string(), "session=abc123; Path=/".to_string()),
|
("set-cookie".to_string(), "session=abc123; Path=/".to_string()),
|
||||||
("set-cookie".to_string(), "user_id=42; Path=/".to_string()),
|
("set-cookie".to_string(), "user_id=42; Path=/".to_string()),
|
||||||
(
|
("set-cookie".to_string(), "preferences=dark; Path=/; Max-Age=86400".to_string()),
|
||||||
"set-cookie".to_string(),
|
|
||||||
"preferences=dark; Path=/; Max-Age=86400".to_string(),
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let body_stream: Pin<Box<dyn AsyncRead + Send>> =
|
let body_stream: Pin<Box<dyn AsyncRead + Send>> =
|
||||||
@@ -777,116 +720,4 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(request_count.load(Ordering::SeqCst), 2);
|
assert_eq!(request_count.load(Ordering::SeqCst), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_cross_origin_redirect_strips_auth_headers() {
|
|
||||||
// Redirect from api.example.com -> s3.amazonaws.com should strip Authorization
|
|
||||||
let responses = vec![
|
|
||||||
MockResponse {
|
|
||||||
status: 302,
|
|
||||||
headers: vec![(
|
|
||||||
"Location".to_string(),
|
|
||||||
"https://s3.amazonaws.com/bucket/file.pdf".to_string(),
|
|
||||||
)],
|
|
||||||
body: vec![],
|
|
||||||
},
|
|
||||||
MockResponse { status: 200, headers: Vec::new(), body: b"PDF content".to_vec() },
|
|
||||||
];
|
|
||||||
|
|
||||||
let sender = MockSender::new(responses);
|
|
||||||
let captured = sender.captured_requests.clone();
|
|
||||||
let transaction = HttpTransaction::new(sender);
|
|
||||||
|
|
||||||
let request = SendableHttpRequest {
|
|
||||||
url: "https://api.example.com/download".to_string(),
|
|
||||||
method: "GET".to_string(),
|
|
||||||
headers: vec![
|
|
||||||
("Authorization".to_string(), "Basic dXNlcjpwYXNz".to_string()),
|
|
||||||
("Accept".to_string(), "application/pdf".to_string()),
|
|
||||||
],
|
|
||||||
options: crate::types::SendableHttpRequestOptions {
|
|
||||||
follow_redirects: true,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (_tx, rx) = tokio::sync::watch::channel(false);
|
|
||||||
let (event_tx, _event_rx) = mpsc::channel(100);
|
|
||||||
let result = transaction.execute_with_cancellation(request, rx, event_tx).await.unwrap();
|
|
||||||
assert_eq!(result.status, 200);
|
|
||||||
|
|
||||||
let requests = captured.lock().await;
|
|
||||||
assert_eq!(requests.len(), 2);
|
|
||||||
|
|
||||||
// First request should have the Authorization header
|
|
||||||
assert!(
|
|
||||||
requests[0].headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("authorization")),
|
|
||||||
"First request should have Authorization header"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Second request (to different host) should NOT have the Authorization header
|
|
||||||
assert!(
|
|
||||||
!requests[1].headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("authorization")),
|
|
||||||
"Redirected request to different host should NOT have Authorization header"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Non-sensitive headers should still be present
|
|
||||||
assert!(
|
|
||||||
requests[1].headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("accept")),
|
|
||||||
"Non-sensitive headers should be preserved across cross-origin redirects"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_same_origin_redirect_preserves_auth_headers() {
|
|
||||||
// Redirect within the same host should keep Authorization
|
|
||||||
let responses = vec![
|
|
||||||
MockResponse {
|
|
||||||
status: 302,
|
|
||||||
headers: vec![(
|
|
||||||
"Location".to_string(),
|
|
||||||
"https://api.example.com/v2/download".to_string(),
|
|
||||||
)],
|
|
||||||
body: vec![],
|
|
||||||
},
|
|
||||||
MockResponse { status: 200, headers: Vec::new(), body: b"OK".to_vec() },
|
|
||||||
];
|
|
||||||
|
|
||||||
let sender = MockSender::new(responses);
|
|
||||||
let captured = sender.captured_requests.clone();
|
|
||||||
let transaction = HttpTransaction::new(sender);
|
|
||||||
|
|
||||||
let request = SendableHttpRequest {
|
|
||||||
url: "https://api.example.com/v1/download".to_string(),
|
|
||||||
method: "GET".to_string(),
|
|
||||||
headers: vec![
|
|
||||||
("Authorization".to_string(), "Bearer token123".to_string()),
|
|
||||||
("Accept".to_string(), "application/json".to_string()),
|
|
||||||
],
|
|
||||||
options: crate::types::SendableHttpRequestOptions {
|
|
||||||
follow_redirects: true,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (_tx, rx) = tokio::sync::watch::channel(false);
|
|
||||||
let (event_tx, _event_rx) = mpsc::channel(100);
|
|
||||||
let result = transaction.execute_with_cancellation(request, rx, event_tx).await.unwrap();
|
|
||||||
assert_eq!(result.status, 200);
|
|
||||||
|
|
||||||
let requests = captured.lock().await;
|
|
||||||
assert_eq!(requests.len(), 2);
|
|
||||||
|
|
||||||
// Both requests should have the Authorization header (same host)
|
|
||||||
assert!(
|
|
||||||
requests[0].headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("authorization")),
|
|
||||||
"First request should have Authorization header"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
requests[1].headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("authorization")),
|
|
||||||
"Redirected request to same host should preserve Authorization header"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-5
@@ -12,8 +12,6 @@ export type CookieExpires = { "AtUtc": string } | "SessionEnd";
|
|||||||
|
|
||||||
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
||||||
|
|
||||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
|
||||||
|
|
||||||
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||||
|
|
||||||
export type EncryptedKey = { encryptedKey: string, };
|
export type EncryptedKey = { encryptedKey: string, };
|
||||||
@@ -40,7 +38,7 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
|
|||||||
|
|
||||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||||
|
|
||||||
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
||||||
|
|
||||||
@@ -49,7 +47,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
@@ -93,6 +91,6 @@ export type WebsocketMessageType = "text" | "binary";
|
|||||||
|
|
||||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||||
|
|
||||||
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
||||||
|
|||||||
@@ -206,34 +206,6 @@ export function replaceModelsInStore<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeModelsInStore<
|
|
||||||
M extends AnyModel['model'],
|
|
||||||
T extends Extract<AnyModel, { model: M }>,
|
|
||||||
>(model: M, models: T[], filter?: (model: T) => boolean) {
|
|
||||||
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
|
||||||
const existingModels = { ...prev[model] } as Record<string, T>;
|
|
||||||
|
|
||||||
// Merge in new models first
|
|
||||||
for (const m of models) {
|
|
||||||
existingModels[m.id] = m;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then filter out unwanted models
|
|
||||||
if (filter) {
|
|
||||||
for (const [id, m] of Object.entries(existingModels)) {
|
|
||||||
if (!filter(m)) {
|
|
||||||
delete existingModels[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[model]: existingModels,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
|
function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
|
||||||
// Never ignore updates from non-user sources
|
// Never ignore updates from non-user sources
|
||||||
if (updateSource.type !== 'window') {
|
if (updateSource.type !== 'window') {
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
-- Add DNS resolution timing to http_responses
|
|
||||||
ALTER TABLE http_responses ADD COLUMN elapsed_dns INTEGER DEFAULT 0 NOT NULL;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- Add DNS overrides setting to workspaces
|
|
||||||
ALTER TABLE workspaces ADD COLUMN setting_dns_overrides TEXT DEFAULT '[]' NOT NULL;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
-- Filter out headers that match the hardcoded defaults (User-Agent: yaak, Accept: */*),
|
|
||||||
-- keeping any other custom headers the user may have added.
|
|
||||||
UPDATE workspaces
|
|
||||||
SET headers = (
|
|
||||||
SELECT json_group_array(json(value))
|
|
||||||
FROM json_each(headers)
|
|
||||||
WHERE NOT (
|
|
||||||
(LOWER(json_extract(value, '$.name')) = 'user-agent' AND json_extract(value, '$.value') = 'yaak')
|
|
||||||
OR (LOWER(json_extract(value, '$.name')) = 'accept' AND json_extract(value, '$.value') = '*/*')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
WHERE json_array_length(headers) > 0;
|
|
||||||
@@ -73,20 +73,6 @@ pub struct ClientCertificate {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
|
||||||
pub struct DnsOverride {
|
|
||||||
pub hostname: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub ipv4: Vec<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub ipv6: Vec<String>,
|
|
||||||
#[serde(default = "default_true")]
|
|
||||||
#[ts(optional, as = "Option<bool>")]
|
|
||||||
pub enabled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
@@ -317,8 +303,6 @@ pub struct Workspace {
|
|||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub setting_follow_redirects: bool,
|
pub setting_follow_redirects: bool,
|
||||||
pub setting_request_timeout: i32,
|
pub setting_request_timeout: i32,
|
||||||
#[serde(default)]
|
|
||||||
pub setting_dns_overrides: Vec<DnsOverride>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for Workspace {
|
impl UpsertModelInfo for Workspace {
|
||||||
@@ -359,7 +343,6 @@ impl UpsertModelInfo for Workspace {
|
|||||||
(SettingFollowRedirects, self.setting_follow_redirects.into()),
|
(SettingFollowRedirects, self.setting_follow_redirects.into()),
|
||||||
(SettingRequestTimeout, self.setting_request_timeout.into()),
|
(SettingRequestTimeout, self.setting_request_timeout.into()),
|
||||||
(SettingValidateCertificates, self.setting_validate_certificates.into()),
|
(SettingValidateCertificates, self.setting_validate_certificates.into()),
|
||||||
(SettingDnsOverrides, serde_json::to_string(&self.setting_dns_overrides)?.into()),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +359,6 @@ impl UpsertModelInfo for Workspace {
|
|||||||
WorkspaceIden::SettingFollowRedirects,
|
WorkspaceIden::SettingFollowRedirects,
|
||||||
WorkspaceIden::SettingRequestTimeout,
|
WorkspaceIden::SettingRequestTimeout,
|
||||||
WorkspaceIden::SettingValidateCertificates,
|
WorkspaceIden::SettingValidateCertificates,
|
||||||
WorkspaceIden::SettingDnsOverrides,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +368,6 @@ impl UpsertModelInfo for Workspace {
|
|||||||
{
|
{
|
||||||
let headers: String = row.get("headers")?;
|
let headers: String = row.get("headers")?;
|
||||||
let authentication: String = row.get("authentication")?;
|
let authentication: String = row.get("authentication")?;
|
||||||
let setting_dns_overrides: String = row.get("setting_dns_overrides")?;
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
model: row.get("model")?,
|
model: row.get("model")?,
|
||||||
@@ -401,7 +382,6 @@ impl UpsertModelInfo for Workspace {
|
|||||||
setting_follow_redirects: row.get("setting_follow_redirects")?,
|
setting_follow_redirects: row.get("setting_follow_redirects")?,
|
||||||
setting_request_timeout: row.get("setting_request_timeout")?,
|
setting_request_timeout: row.get("setting_request_timeout")?,
|
||||||
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
||||||
setting_dns_overrides: serde_json::from_str(&setting_dns_overrides).unwrap_or_default(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1353,7 +1333,6 @@ pub struct HttpResponse {
|
|||||||
pub content_length_compressed: Option<i32>,
|
pub content_length_compressed: Option<i32>,
|
||||||
pub elapsed: i32,
|
pub elapsed: i32,
|
||||||
pub elapsed_headers: i32,
|
pub elapsed_headers: i32,
|
||||||
pub elapsed_dns: i32,
|
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
pub headers: Vec<HttpResponseHeader>,
|
pub headers: Vec<HttpResponseHeader>,
|
||||||
pub remote_addr: Option<String>,
|
pub remote_addr: Option<String>,
|
||||||
@@ -1402,7 +1381,6 @@ impl UpsertModelInfo for HttpResponse {
|
|||||||
(ContentLengthCompressed, self.content_length_compressed.into()),
|
(ContentLengthCompressed, self.content_length_compressed.into()),
|
||||||
(Elapsed, self.elapsed.into()),
|
(Elapsed, self.elapsed.into()),
|
||||||
(ElapsedHeaders, self.elapsed_headers.into()),
|
(ElapsedHeaders, self.elapsed_headers.into()),
|
||||||
(ElapsedDns, self.elapsed_dns.into()),
|
|
||||||
(Error, self.error.into()),
|
(Error, self.error.into()),
|
||||||
(Headers, serde_json::to_string(&self.headers)?.into()),
|
(Headers, serde_json::to_string(&self.headers)?.into()),
|
||||||
(RemoteAddr, self.remote_addr.into()),
|
(RemoteAddr, self.remote_addr.into()),
|
||||||
@@ -1424,7 +1402,6 @@ impl UpsertModelInfo for HttpResponse {
|
|||||||
HttpResponseIden::ContentLengthCompressed,
|
HttpResponseIden::ContentLengthCompressed,
|
||||||
HttpResponseIden::Elapsed,
|
HttpResponseIden::Elapsed,
|
||||||
HttpResponseIden::ElapsedHeaders,
|
HttpResponseIden::ElapsedHeaders,
|
||||||
HttpResponseIden::ElapsedDns,
|
|
||||||
HttpResponseIden::Error,
|
HttpResponseIden::Error,
|
||||||
HttpResponseIden::Headers,
|
HttpResponseIden::Headers,
|
||||||
HttpResponseIden::RemoteAddr,
|
HttpResponseIden::RemoteAddr,
|
||||||
@@ -1458,7 +1435,6 @@ impl UpsertModelInfo for HttpResponse {
|
|||||||
version: r.get("version")?,
|
version: r.get("version")?,
|
||||||
elapsed: r.get("elapsed")?,
|
elapsed: r.get("elapsed")?,
|
||||||
elapsed_headers: r.get("elapsed_headers")?,
|
elapsed_headers: r.get("elapsed_headers")?,
|
||||||
elapsed_dns: r.get("elapsed_dns").unwrap_or_default(),
|
|
||||||
remote_addr: r.get("remote_addr")?,
|
remote_addr: r.get("remote_addr")?,
|
||||||
status: r.get("status")?,
|
status: r.get("status")?,
|
||||||
status_reason: r.get("status_reason")?,
|
status_reason: r.get("status_reason")?,
|
||||||
@@ -1495,21 +1471,7 @@ pub enum HttpResponseEventData {
|
|||||||
},
|
},
|
||||||
SendUrl {
|
SendUrl {
|
||||||
method: String,
|
method: String,
|
||||||
#[serde(default)]
|
|
||||||
scheme: String,
|
|
||||||
#[serde(default)]
|
|
||||||
username: String,
|
|
||||||
#[serde(default)]
|
|
||||||
password: String,
|
|
||||||
#[serde(default)]
|
|
||||||
host: String,
|
|
||||||
#[serde(default)]
|
|
||||||
port: u16,
|
|
||||||
path: String,
|
path: String,
|
||||||
#[serde(default)]
|
|
||||||
query: String,
|
|
||||||
#[serde(default)]
|
|
||||||
fragment: String,
|
|
||||||
},
|
},
|
||||||
ReceiveUrl {
|
ReceiveUrl {
|
||||||
version: String,
|
version: String,
|
||||||
@@ -1529,12 +1491,6 @@ pub enum HttpResponseEventData {
|
|||||||
ChunkReceived {
|
ChunkReceived {
|
||||||
bytes: usize,
|
bytes: usize,
|
||||||
},
|
},
|
||||||
DnsResolved {
|
|
||||||
hostname: String,
|
|
||||||
addresses: Vec<String>,
|
|
||||||
duration: u64,
|
|
||||||
overridden: bool,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HttpResponseEventData {
|
impl Default for HttpResponseEventData {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use super::dedupe_headers;
|
|
||||||
use crate::db_context::DbContext;
|
use crate::db_context::DbContext;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::{GrpcRequest, GrpcRequestIden, HttpRequestHeader};
|
use crate::models::{GrpcRequest, GrpcRequestIden, HttpRequestHeader};
|
||||||
@@ -88,6 +87,6 @@ impl<'a> DbContext<'a> {
|
|||||||
|
|
||||||
metadata.append(&mut grpc_request.metadata.clone());
|
metadata.append(&mut grpc_request.metadata.clone());
|
||||||
|
|
||||||
Ok(dedupe_headers(metadata))
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use super::dedupe_headers;
|
|
||||||
use crate::db_context::DbContext;
|
use crate::db_context::DbContext;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::{Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden};
|
use crate::models::{Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden};
|
||||||
@@ -88,7 +87,7 @@ impl<'a> DbContext<'a> {
|
|||||||
|
|
||||||
headers.append(&mut http_request.headers.clone());
|
headers.append(&mut http_request.headers.clone());
|
||||||
|
|
||||||
Ok(dedupe_headers(headers))
|
Ok(headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_http_requests_for_folder_recursive(
|
pub fn list_http_requests_for_folder_recursive(
|
||||||
|
|||||||
@@ -19,26 +19,6 @@ mod websocket_connections;
|
|||||||
mod websocket_events;
|
mod websocket_events;
|
||||||
mod websocket_requests;
|
mod websocket_requests;
|
||||||
mod workspace_metas;
|
mod workspace_metas;
|
||||||
pub mod workspaces;
|
mod workspaces;
|
||||||
|
|
||||||
const MAX_HISTORY_ITEMS: usize = 20;
|
const MAX_HISTORY_ITEMS: usize = 20;
|
||||||
|
|
||||||
use crate::models::HttpRequestHeader;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Deduplicate headers by name (case-insensitive), keeping the latest (most specific) value.
|
|
||||||
/// Preserves the order of first occurrence for each header name.
|
|
||||||
pub(crate) fn dedupe_headers(headers: Vec<HttpRequestHeader>) -> Vec<HttpRequestHeader> {
|
|
||||||
let mut index_by_name: HashMap<String, usize> = HashMap::new();
|
|
||||||
let mut deduped: Vec<HttpRequestHeader> = Vec::new();
|
|
||||||
for header in headers {
|
|
||||||
let key = header.name.to_lowercase();
|
|
||||||
if let Some(&idx) = index_by_name.get(&key) {
|
|
||||||
deduped[idx] = header;
|
|
||||||
} else {
|
|
||||||
index_by_name.insert(key, deduped.len());
|
|
||||||
deduped.push(header);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deduped
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use super::dedupe_headers;
|
|
||||||
use crate::db_context::DbContext;
|
use crate::db_context::DbContext;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::{HttpRequestHeader, WebsocketRequest, WebsocketRequestIden};
|
use crate::models::{HttpRequestHeader, WebsocketRequest, WebsocketRequestIden};
|
||||||
@@ -96,6 +95,6 @@ impl<'a> DbContext<'a> {
|
|||||||
|
|
||||||
headers.append(&mut websocket_request.headers.clone());
|
headers.append(&mut websocket_request.headers.clone());
|
||||||
|
|
||||||
Ok(dedupe_headers(headers))
|
Ok(headers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,28 +80,6 @@ impl<'a> DbContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_headers_for_workspace(&self, workspace: &Workspace) -> Vec<HttpRequestHeader> {
|
pub fn resolve_headers_for_workspace(&self, workspace: &Workspace) -> Vec<HttpRequestHeader> {
|
||||||
let mut headers = default_headers();
|
workspace.headers.clone()
|
||||||
headers.extend(workspace.headers.clone());
|
|
||||||
headers
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Global default headers that are always sent with requests unless overridden.
|
|
||||||
/// These are prepended to the inheritance chain so workspace/folder/request headers
|
|
||||||
/// can override or disable them.
|
|
||||||
pub fn default_headers() -> Vec<HttpRequestHeader> {
|
|
||||||
vec![
|
|
||||||
HttpRequestHeader {
|
|
||||||
enabled: true,
|
|
||||||
name: "User-Agent".to_string(),
|
|
||||||
value: "yaak".to_string(),
|
|
||||||
id: None,
|
|
||||||
},
|
|
||||||
HttpRequestHeader {
|
|
||||||
enabled: true,
|
|
||||||
name: "Accept".to_string(),
|
|
||||||
value: "*/*".to_string(),
|
|
||||||
id: None,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
+3
-5
@@ -12,8 +12,6 @@ export type CookieExpires = { "AtUtc": string } | "SessionEnd";
|
|||||||
|
|
||||||
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
||||||
|
|
||||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
|
||||||
|
|
||||||
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||||
|
|
||||||
export type EncryptedKey = { encryptedKey: string, };
|
export type EncryptedKey = { encryptedKey: string, };
|
||||||
@@ -40,7 +38,7 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
|
|||||||
|
|
||||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||||
|
|
||||||
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
||||||
|
|
||||||
@@ -49,7 +47,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
@@ -79,6 +77,6 @@ export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping"
|
|||||||
|
|
||||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||||
|
|
||||||
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
||||||
|
|||||||
@@ -80,7 +80,10 @@ pub async fn check_plugin_updates(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Search for plugins in the registry.
|
/// Search for plugins in the registry.
|
||||||
pub async fn search_plugins(http_client: &Client, query: &str) -> Result<PluginSearchResponse> {
|
pub async fn search_plugins(
|
||||||
|
http_client: &Client,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<PluginSearchResponse> {
|
||||||
let mut url = build_url("/search");
|
let mut url = build_url("/search");
|
||||||
{
|
{
|
||||||
let mut query_pairs = url.query_pairs_mut();
|
let mut query_pairs = url.query_pairs_mut();
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ use std::time::Duration;
|
|||||||
use tokio::fs::read_dir;
|
use tokio::fs::read_dir;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::sync::mpsc::error::TrySendError;
|
use tokio::sync::mpsc::error::TrySendError;
|
||||||
use tokio::sync::{Mutex, mpsc, oneshot};
|
use tokio::sync::{Mutex, mpsc};
|
||||||
use tokio::time::{Instant, timeout};
|
use tokio::time::{Instant, timeout};
|
||||||
use yaak_models::models::Plugin;
|
use yaak_models::models::Plugin;
|
||||||
use yaak_models::util::generate_id;
|
use yaak_models::util::generate_id;
|
||||||
@@ -43,7 +43,6 @@ pub struct PluginManager {
|
|||||||
subscribers: Arc<Mutex<HashMap<String, mpsc::Sender<InternalEvent>>>>,
|
subscribers: Arc<Mutex<HashMap<String, mpsc::Sender<InternalEvent>>>>,
|
||||||
plugin_handles: Arc<Mutex<Vec<PluginHandle>>>,
|
plugin_handles: Arc<Mutex<Vec<PluginHandle>>>,
|
||||||
kill_tx: tokio::sync::watch::Sender<bool>,
|
kill_tx: tokio::sync::watch::Sender<bool>,
|
||||||
killed_rx: Arc<Mutex<Option<oneshot::Receiver<()>>>>,
|
|
||||||
ws_service: Arc<PluginRuntimeServerWebsocket>,
|
ws_service: Arc<PluginRuntimeServerWebsocket>,
|
||||||
vendored_plugin_dir: PathBuf,
|
vendored_plugin_dir: PathBuf,
|
||||||
pub(crate) installed_plugin_dir: PathBuf,
|
pub(crate) installed_plugin_dir: PathBuf,
|
||||||
@@ -71,7 +70,6 @@ impl PluginManager {
|
|||||||
) -> PluginManager {
|
) -> PluginManager {
|
||||||
let (events_tx, mut events_rx) = mpsc::channel(2048);
|
let (events_tx, mut events_rx) = mpsc::channel(2048);
|
||||||
let (kill_server_tx, kill_server_rx) = tokio::sync::watch::channel(false);
|
let (kill_server_tx, kill_server_rx) = tokio::sync::watch::channel(false);
|
||||||
let (killed_tx, killed_rx) = oneshot::channel();
|
|
||||||
|
|
||||||
let (client_disconnect_tx, mut client_disconnect_rx) = mpsc::channel(128);
|
let (client_disconnect_tx, mut client_disconnect_rx) = mpsc::channel(128);
|
||||||
let (client_connect_tx, mut client_connect_rx) = tokio::sync::watch::channel(false);
|
let (client_connect_tx, mut client_connect_rx) = tokio::sync::watch::channel(false);
|
||||||
@@ -83,7 +81,6 @@ impl PluginManager {
|
|||||||
subscribers: Default::default(),
|
subscribers: Default::default(),
|
||||||
ws_service: Arc::new(ws_service.clone()),
|
ws_service: Arc::new(ws_service.clone()),
|
||||||
kill_tx: kill_server_tx,
|
kill_tx: kill_server_tx,
|
||||||
killed_rx: Arc::new(Mutex::new(Some(killed_rx))),
|
|
||||||
vendored_plugin_dir,
|
vendored_plugin_dir,
|
||||||
installed_plugin_dir,
|
installed_plugin_dir,
|
||||||
dev_mode,
|
dev_mode,
|
||||||
@@ -144,15 +141,9 @@ impl PluginManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 2. Start Node.js runtime
|
// 2. Start Node.js runtime
|
||||||
start_nodejs_plugin_runtime(
|
start_nodejs_plugin_runtime(&node_bin_path, &plugin_runtime_main, addr, &kill_server_rx)
|
||||||
&node_bin_path,
|
.await
|
||||||
&plugin_runtime_main,
|
.unwrap();
|
||||||
addr,
|
|
||||||
&kill_server_rx,
|
|
||||||
killed_tx,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
info!("Waiting for plugins to initialize");
|
info!("Waiting for plugins to initialize");
|
||||||
init_plugins_task.await.unwrap();
|
init_plugins_task.await.unwrap();
|
||||||
|
|
||||||
@@ -305,15 +296,8 @@ impl PluginManager {
|
|||||||
pub async fn terminate(&self) {
|
pub async fn terminate(&self) {
|
||||||
self.kill_tx.send_replace(true);
|
self.kill_tx.send_replace(true);
|
||||||
|
|
||||||
// Wait for the plugin runtime process to actually exit
|
// Give it a bit of time to kill
|
||||||
let killed_rx = self.killed_rx.lock().await.take();
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
if let Some(rx) = killed_rx {
|
|
||||||
if timeout(Duration::from_secs(5), rx).await.is_err() {
|
|
||||||
warn!("Timed out waiting for plugin runtime to exit");
|
|
||||||
} else {
|
|
||||||
info!("Plugin runtime exited")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reply(
|
pub async fn reply(
|
||||||
@@ -394,8 +378,7 @@ impl PluginManager {
|
|||||||
plugins: Vec<PluginHandle>,
|
plugins: Vec<PluginHandle>,
|
||||||
timeout_duration: Duration,
|
timeout_duration: Duration,
|
||||||
) -> Result<Vec<InternalEvent>> {
|
) -> Result<Vec<InternalEvent>> {
|
||||||
let event_type = payload.type_name();
|
let label = format!("wait[{}.{}]", plugins.len(), payload.type_name());
|
||||||
let label = format!("wait[{}.{}]", plugins.len(), event_type);
|
|
||||||
let (rx_id, mut rx) = self.subscribe(label.as_str()).await;
|
let (rx_id, mut rx) = self.subscribe(label.as_str()).await;
|
||||||
|
|
||||||
// 1. Build the events with IDs and everything
|
// 1. Build the events with IDs and everything
|
||||||
@@ -429,21 +412,10 @@ impl PluginManager {
|
|||||||
|
|
||||||
// Timeout to prevent hanging forever if plugin doesn't respond
|
// Timeout to prevent hanging forever if plugin doesn't respond
|
||||||
if timeout(timeout_duration, collect_events).await.is_err() {
|
if timeout(timeout_duration, collect_events).await.is_err() {
|
||||||
let responded_ids: Vec<&String> =
|
|
||||||
found_events.iter().filter_map(|e| e.reply_id.as_ref()).collect();
|
|
||||||
let non_responding: Vec<&str> = events_to_send
|
|
||||||
.iter()
|
|
||||||
.filter(|e| !responded_ids.contains(&&e.id))
|
|
||||||
.map(|e| e.plugin_name.as_str())
|
|
||||||
.collect();
|
|
||||||
warn!(
|
warn!(
|
||||||
"Timeout ({:?}) waiting for {} responses. Got {}/{} responses. \
|
"Timeout waiting for plugin responses. Got {}/{} responses",
|
||||||
Non-responding plugins: [{}]",
|
|
||||||
timeout_duration,
|
|
||||||
event_type,
|
|
||||||
found_events.len(),
|
found_events.len(),
|
||||||
events_to_send.len(),
|
events_to_send.len()
|
||||||
non_responding.join(", ")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,11 +196,7 @@ pub fn decrypt_secure_template_function(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
new_tokens.push(Token::Raw {
|
new_tokens.push(Token::Raw {
|
||||||
text: template_function_secure_run(
|
text: template_function_secure_run(encryption_manager, args_map, plugin_context)?,
|
||||||
encryption_manager,
|
|
||||||
args_map,
|
|
||||||
plugin_context,
|
|
||||||
)?,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
t => {
|
t => {
|
||||||
@@ -220,8 +216,7 @@ pub fn encrypt_secure_template_function(
|
|||||||
plugin_context: &PluginContext,
|
plugin_context: &PluginContext,
|
||||||
template: &str,
|
template: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let decrypted =
|
let decrypted = decrypt_secure_template_function(&encryption_manager, plugin_context, template)?;
|
||||||
decrypt_secure_template_function(&encryption_manager, plugin_context, template)?;
|
|
||||||
let tokens = Tokens {
|
let tokens = Tokens {
|
||||||
tokens: vec. Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
|
||||||
|
|
||||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||||
|
|
||||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
@@ -22,4 +20,4 @@ export type SyncState = { model: "sync_state", id: string, workspaceId: string,
|
|||||||
|
|
||||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||||
|
|||||||
@@ -296,7 +296,11 @@ pub fn compute_sync_ops(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_models(db: &DbContext, version: &str, workspace_id: &str) -> Result<Vec<SyncModel>> {
|
fn workspace_models(
|
||||||
|
db: &DbContext,
|
||||||
|
version: &str,
|
||||||
|
workspace_id: &str,
|
||||||
|
) -> Result<Vec<SyncModel>> {
|
||||||
// We want to include private environments here so that we can take them into account during
|
// We want to include private environments here so that we can take them into account during
|
||||||
// the sync process. Otherwise, they would be treated as deleted.
|
// the sync process. Otherwise, they would be treated as deleted.
|
||||||
let include_private_environments = true;
|
let include_private_environments = true;
|
||||||
|
|||||||
+41
-1
@@ -1,5 +1,31 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { WebsocketConnection } from '@yaakapp-internal/models';
|
import { WebsocketConnection, WebsocketEvent, WebsocketRequest } from '@yaakapp-internal/models';
|
||||||
|
|
||||||
|
export function upsertWebsocketRequest(
|
||||||
|
request: WebsocketRequest | Partial<Omit<WebsocketRequest, 'id'>>,
|
||||||
|
) {
|
||||||
|
return invoke('cmd_ws_upsert_request', {
|
||||||
|
request,
|
||||||
|
}) as Promise<WebsocketRequest>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function duplicateWebsocketRequest(requestId: string) {
|
||||||
|
return invoke('cmd_ws_duplicate_request', {
|
||||||
|
requestId,
|
||||||
|
}) as Promise<WebsocketRequest>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteWebsocketRequest(requestId: string) {
|
||||||
|
return invoke('cmd_ws_delete_request', {
|
||||||
|
requestId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteWebsocketConnection(connectionId: string) {
|
||||||
|
return invoke('cmd_ws_delete_connection', {
|
||||||
|
connectionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteWebsocketConnections(requestId: string) {
|
export function deleteWebsocketConnections(requestId: string) {
|
||||||
return invoke('cmd_ws_delete_connections', {
|
return invoke('cmd_ws_delete_connections', {
|
||||||
@@ -7,6 +33,20 @@ export function deleteWebsocketConnections(requestId: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listWebsocketRequests({ workspaceId }: { workspaceId: string }) {
|
||||||
|
return invoke('cmd_ws_list_requests', { workspaceId }) as Promise<WebsocketRequest[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listWebsocketEvents({ connectionId }: { connectionId: string }) {
|
||||||
|
return invoke('cmd_ws_list_events', { connectionId }) as Promise<WebsocketEvent[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listWebsocketConnections({ workspaceId }: { workspaceId: string }) {
|
||||||
|
return invoke('cmd_ws_list_connections', { workspaceId }) as Promise<
|
||||||
|
WebsocketConnection[]
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
export function connectWebsocket({
|
export function connectWebsocket({
|
||||||
requestId,
|
requestId,
|
||||||
environmentId,
|
environmentId,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use crate::connect::ws_connect;
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use futures_util::stream::SplitSink;
|
use futures_util::stream::SplitSink;
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use http::HeaderMap;
|
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -11,6 +10,7 @@ use tokio::net::TcpStream;
|
|||||||
use tokio::sync::{Mutex, mpsc};
|
use tokio::sync::{Mutex, mpsc};
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
use tokio_tungstenite::tungstenite::handshake::client::Response;
|
use tokio_tungstenite::tungstenite::handshake::client::Response;
|
||||||
|
use http::HeaderMap;
|
||||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||||
use yaak_tls::ClientCertificateConfig;
|
use yaak_tls::ClientCertificateConfig;
|
||||||
|
|||||||
Generated
+41
-82
@@ -63,7 +63,7 @@
|
|||||||
"src-web"
|
"src-web"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.13",
|
"@biomejs/biome": "^2.3.10",
|
||||||
"@tauri-apps/cli": "^2.9.6",
|
"@tauri-apps/cli": "^2.9.6",
|
||||||
"@yaakapp/cli": "^0.3.4",
|
"@yaakapp/cli": "^0.3.4",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
@@ -501,9 +501,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/biome": {
|
"node_modules/@biomejs/biome": {
|
||||||
"version": "2.3.13",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.11.tgz",
|
||||||
"integrity": "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA==",
|
"integrity": "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -517,20 +517,20 @@
|
|||||||
"url": "https://opencollective.com/biome"
|
"url": "https://opencollective.com/biome"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@biomejs/cli-darwin-arm64": "2.3.13",
|
"@biomejs/cli-darwin-arm64": "2.3.11",
|
||||||
"@biomejs/cli-darwin-x64": "2.3.13",
|
"@biomejs/cli-darwin-x64": "2.3.11",
|
||||||
"@biomejs/cli-linux-arm64": "2.3.13",
|
"@biomejs/cli-linux-arm64": "2.3.11",
|
||||||
"@biomejs/cli-linux-arm64-musl": "2.3.13",
|
"@biomejs/cli-linux-arm64-musl": "2.3.11",
|
||||||
"@biomejs/cli-linux-x64": "2.3.13",
|
"@biomejs/cli-linux-x64": "2.3.11",
|
||||||
"@biomejs/cli-linux-x64-musl": "2.3.13",
|
"@biomejs/cli-linux-x64-musl": "2.3.11",
|
||||||
"@biomejs/cli-win32-arm64": "2.3.13",
|
"@biomejs/cli-win32-arm64": "2.3.11",
|
||||||
"@biomejs/cli-win32-x64": "2.3.13"
|
"@biomejs/cli-win32-x64": "2.3.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-arm64": {
|
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||||
"version": "2.3.13",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.11.tgz",
|
||||||
"integrity": "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ==",
|
"integrity": "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -545,9 +545,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-x64": {
|
"node_modules/@biomejs/cli-darwin-x64": {
|
||||||
"version": "2.3.13",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.11.tgz",
|
||||||
"integrity": "sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw==",
|
"integrity": "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -562,9 +562,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-arm64": {
|
"node_modules/@biomejs/cli-linux-arm64": {
|
||||||
"version": "2.3.13",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.11.tgz",
|
||||||
"integrity": "sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw==",
|
"integrity": "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -579,9 +579,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||||
"version": "2.3.13",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.11.tgz",
|
||||||
"integrity": "sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA==",
|
"integrity": "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -596,9 +596,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64": {
|
"node_modules/@biomejs/cli-linux-x64": {
|
||||||
"version": "2.3.13",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.11.tgz",
|
||||||
"integrity": "sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw==",
|
"integrity": "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -613,9 +613,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64-musl": {
|
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||||
"version": "2.3.13",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.11.tgz",
|
||||||
"integrity": "sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ==",
|
"integrity": "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -630,9 +630,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-win32-arm64": {
|
"node_modules/@biomejs/cli-win32-arm64": {
|
||||||
"version": "2.3.13",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.11.tgz",
|
||||||
"integrity": "sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA==",
|
"integrity": "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -647,9 +647,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-win32-x64": {
|
"node_modules/@biomejs/cli-win32-x64": {
|
||||||
"version": "2.3.13",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.11.tgz",
|
||||||
"integrity": "sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ==",
|
"integrity": "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -807,21 +807,6 @@
|
|||||||
"@lezer/xml": "^1.0.0"
|
"@lezer/xml": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@codemirror/lang-yaml": {
|
|
||||||
"version": "6.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz",
|
|
||||||
"integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@codemirror/autocomplete": "^6.0.0",
|
|
||||||
"@codemirror/language": "^6.0.0",
|
|
||||||
"@codemirror/state": "^6.0.0",
|
|
||||||
"@lezer/common": "^1.2.0",
|
|
||||||
"@lezer/highlight": "^1.2.0",
|
|
||||||
"@lezer/lr": "^1.0.0",
|
|
||||||
"@lezer/yaml": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@codemirror/language": {
|
"node_modules/@codemirror/language": {
|
||||||
"version": "6.12.1",
|
"version": "6.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
|
||||||
@@ -847,19 +832,6 @@
|
|||||||
"crelt": "^1.0.5"
|
"crelt": "^1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@codemirror/merge": {
|
|
||||||
"version": "6.11.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/merge/-/merge-6.11.2.tgz",
|
|
||||||
"integrity": "sha512-NO5EJd2rLRbwVWLgMdhIntDIhfDtMOKYEZgqV5WnkNUS2oXOCVWLPjG/kgl/Jth2fGiOuG947bteqxP9nBXmMg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@codemirror/language": "^6.0.0",
|
|
||||||
"@codemirror/state": "^6.0.0",
|
|
||||||
"@codemirror/view": "^6.17.0",
|
|
||||||
"@lezer/highlight": "^1.0.0",
|
|
||||||
"style-mod": "^4.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@codemirror/search": {
|
"node_modules/@codemirror/search": {
|
||||||
"version": "6.5.11",
|
"version": "6.5.11",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
|
||||||
@@ -1642,17 +1614,6 @@
|
|||||||
"@lezer/lr": "^1.0.0"
|
"@lezer/lr": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@lezer/yaml": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@lezer/common": "^1.2.0",
|
|
||||||
"@lezer/highlight": "^1.0.0",
|
|
||||||
"@lezer/lr": "^1.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@marijn/find-cluster-break": {
|
"node_modules/@marijn/find-cluster-break": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||||
@@ -7850,9 +7811,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hono": {
|
"node_modules/hono": {
|
||||||
"version": "4.11.7",
|
"version": "4.11.3",
|
||||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.3.tgz",
|
||||||
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
|
"integrity": "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.9.0"
|
"node": ">=16.9.0"
|
||||||
@@ -15760,7 +15721,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin-runtime-types": {
|
"packages/plugin-runtime-types": {
|
||||||
"name": "@yaakapp/api",
|
"name": "@yaakapp/api",
|
||||||
"version": "0.8.0",
|
"version": "0.7.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^24.0.13"
|
"@types/node": "^24.0.13"
|
||||||
},
|
},
|
||||||
@@ -15782,7 +15743,7 @@
|
|||||||
"@hono/mcp": "^0.2.3",
|
"@hono/mcp": "^0.2.3",
|
||||||
"@hono/node-server": "^1.19.7",
|
"@hono/node-server": "^1.19.7",
|
||||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||||
"hono": "^4.11.7",
|
"hono": "^4.11.3",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -16023,9 +15984,7 @@
|
|||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-markdown": "^6.3.2",
|
"@codemirror/lang-markdown": "^6.3.2",
|
||||||
"@codemirror/lang-xml": "^6.1.0",
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
"@codemirror/lang-yaml": "^6.1.2",
|
|
||||||
"@codemirror/language": "^6.11.0",
|
"@codemirror/language": "^6.11.0",
|
||||||
"@codemirror/merge": "^6.11.2",
|
|
||||||
"@codemirror/search": "^6.5.11",
|
"@codemirror/search": "^6.5.11",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@gilbarbara/deep-equal": "^0.3.1",
|
"@gilbarbara/deep-equal": "^0.3.1",
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@
|
|||||||
"js-yaml": "^4.1.1"
|
"js-yaml": "^4.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.13",
|
"@biomejs/biome": "^2.3.10",
|
||||||
"@tauri-apps/cli": "^2.9.6",
|
"@tauri-apps/cli": "^2.9.6",
|
||||||
"@yaakapp/cli": "^0.3.4",
|
"@yaakapp/cli": "^0.3.4",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ npx @yaakapp/cli generate
|
|||||||
```
|
```
|
||||||
|
|
||||||
For more details on creating plugins, check out
|
For more details on creating plugins, check out
|
||||||
the [Quick Start Guide](https://yaak.app/docs/plugin-development/plugins-quick-start)
|
the [Quick Start Guide](https://feedback.yaak.app/help/articles/6911763-plugins-quick-start)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp/api",
|
"name": "@yaakapp/api",
|
||||||
"version": "0.8.0",
|
"version": "0.7.1",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"api-client",
|
"api-client",
|
||||||
"insomnia-alternative",
|
"insomnia-alternative",
|
||||||
|
|||||||
+3
-5
@@ -12,8 +12,6 @@ export type CookieExpires = { "AtUtc": string } | "SessionEnd";
|
|||||||
|
|
||||||
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
||||||
|
|
||||||
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
|
||||||
|
|
||||||
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||||
|
|
||||||
export type EncryptedKey = { encryptedKey: string, };
|
export type EncryptedKey = { encryptedKey: string, };
|
||||||
@@ -40,7 +38,7 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
|
|||||||
|
|
||||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||||
|
|
||||||
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
||||||
|
|
||||||
@@ -49,7 +47,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
@@ -79,6 +77,6 @@ export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping"
|
|||||||
|
|
||||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
||||||
|
|
||||||
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import type {
|
|||||||
TemplateRenderRequest,
|
TemplateRenderRequest,
|
||||||
WorkspaceInfo,
|
WorkspaceInfo,
|
||||||
} from '../bindings/gen_events.ts';
|
} from '../bindings/gen_events.ts';
|
||||||
import type { Folder, HttpRequest } from '../bindings/gen_models.ts';
|
import type { HttpRequest } from '../bindings/gen_models.ts';
|
||||||
import type { JsonValue } from '../bindings/serde_json/JsonValue';
|
import type { JsonValue } from '../bindings/serde_json/JsonValue';
|
||||||
|
|
||||||
export type WorkspaceHandle = Pick<WorkspaceInfo, 'id' | 'name'>;
|
export type WorkspaceHandle = Pick<WorkspaceInfo, 'id' | 'name'>;
|
||||||
@@ -82,15 +82,6 @@ export interface Context {
|
|||||||
};
|
};
|
||||||
folder: {
|
folder: {
|
||||||
list(args?: ListFoldersRequest): Promise<ListFoldersResponse['folders']>;
|
list(args?: ListFoldersRequest): Promise<ListFoldersResponse['folders']>;
|
||||||
getById(args: { id: string }): Promise<Folder | null>;
|
|
||||||
create(
|
|
||||||
args: Omit<Partial<Folder>, 'id' | 'model' | 'createdAt' | 'updatedAt'> &
|
|
||||||
Pick<Folder, 'workspaceId' | 'name'>,
|
|
||||||
): Promise<Folder>;
|
|
||||||
update(
|
|
||||||
args: Omit<Partial<Folder>, 'model' | 'createdAt' | 'updatedAt'> & Pick<Folder, 'id'>,
|
|
||||||
): Promise<Folder>;
|
|
||||||
delete(args: { id: string }): Promise<Folder>;
|
|
||||||
};
|
};
|
||||||
httpResponse: {
|
httpResponse: {
|
||||||
find(args: FindHttpResponsesRequest): Promise<FindHttpResponsesResponse['httpResponses']>;
|
find(args: FindHttpResponsesRequest): Promise<FindHttpResponsesResponse['httpResponses']>;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import type {
|
|||||||
DeleteKeyValueResponse,
|
DeleteKeyValueResponse,
|
||||||
DeleteModelResponse,
|
DeleteModelResponse,
|
||||||
FindHttpResponsesResponse,
|
FindHttpResponsesResponse,
|
||||||
Folder,
|
|
||||||
GetCookieValueRequest,
|
GetCookieValueRequest,
|
||||||
GetCookieValueResponse,
|
GetCookieValueResponse,
|
||||||
GetHttpRequestByIdResponse,
|
GetHttpRequestByIdResponse,
|
||||||
@@ -338,8 +337,8 @@ export class PluginInstance {
|
|||||||
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) {
|
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) {
|
||||||
const auth = this.#mod.authentication;
|
const auth = this.#mod.authentication;
|
||||||
if (typeof auth?.onApply === 'function') {
|
if (typeof auth?.onApply === 'function') {
|
||||||
const resolvedArgs = await applyDynamicFormInput(ctx, auth.args, payload);
|
auth.args = await applyDynamicFormInput(ctx, auth.args, payload);
|
||||||
payload.values = applyFormInputDefaults(resolvedArgs, payload.values);
|
payload.values = applyFormInputDefaults(auth.args, payload.values);
|
||||||
this.#sendPayload(
|
this.#sendPayload(
|
||||||
context,
|
context,
|
||||||
{
|
{
|
||||||
@@ -783,44 +782,6 @@ export class PluginInstance {
|
|||||||
const { folders } = await this.#sendForReply<ListFoldersResponse>(context, payload);
|
const { folders } = await this.#sendForReply<ListFoldersResponse>(context, payload);
|
||||||
return folders;
|
return folders;
|
||||||
},
|
},
|
||||||
getById: async (args: { id: string }) => {
|
|
||||||
const payload = { type: 'list_folders_request' } as const;
|
|
||||||
const { folders } = await this.#sendForReply<ListFoldersResponse>(context, payload);
|
|
||||||
return folders.find((f) => f.id === args.id) ?? null;
|
|
||||||
},
|
|
||||||
create: async ({ name, ...args }) => {
|
|
||||||
const payload = {
|
|
||||||
type: 'upsert_model_request',
|
|
||||||
model: {
|
|
||||||
...args,
|
|
||||||
name: name ?? '',
|
|
||||||
id: '',
|
|
||||||
model: 'folder',
|
|
||||||
},
|
|
||||||
} as InternalEventPayload;
|
|
||||||
const response = await this.#sendForReply<UpsertModelResponse>(context, payload);
|
|
||||||
return response.model as Folder;
|
|
||||||
},
|
|
||||||
update: async (args) => {
|
|
||||||
const payload = {
|
|
||||||
type: 'upsert_model_request',
|
|
||||||
model: {
|
|
||||||
model: 'folder',
|
|
||||||
...args,
|
|
||||||
},
|
|
||||||
} as InternalEventPayload;
|
|
||||||
const response = await this.#sendForReply<UpsertModelResponse>(context, payload);
|
|
||||||
return response.model as Folder;
|
|
||||||
},
|
|
||||||
delete: async (args: { id: string }) => {
|
|
||||||
const payload = {
|
|
||||||
type: 'delete_model_request',
|
|
||||||
model: 'folder',
|
|
||||||
id: args.id,
|
|
||||||
} as InternalEventPayload;
|
|
||||||
const response = await this.#sendForReply<DeleteModelResponse>(context, payload);
|
|
||||||
return response.model as Folder;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
cookies: {
|
cookies: {
|
||||||
getValue: async (args: GetCookieValueRequest) => {
|
getValue: async (args: GetCookieValueRequest) => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"@hono/mcp": "^0.2.3",
|
"@hono/mcp": "^0.2.3",
|
||||||
"@hono/node-server": "^1.19.7",
|
"@hono/node-server": "^1.19.7",
|
||||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||||
"hono": "^4.11.7",
|
"hono": "^4.11.3",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -2,12 +2,6 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { McpServerContext } from '../types.js';
|
import type { McpServerContext } from '../types.js';
|
||||||
import { getWorkspaceContext } from './helpers.js';
|
import { getWorkspaceContext } from './helpers.js';
|
||||||
import {
|
|
||||||
authenticationSchema,
|
|
||||||
authenticationTypeSchema,
|
|
||||||
headersSchema,
|
|
||||||
workspaceIdSchema,
|
|
||||||
} from './schemas.js';
|
|
||||||
|
|
||||||
export function registerFolderTools(server: McpServer, ctx: McpServerContext) {
|
export function registerFolderTools(server: McpServer, ctx: McpServerContext) {
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
@@ -16,7 +10,10 @@ export function registerFolderTools(server: McpServer, ctx: McpServerContext) {
|
|||||||
title: 'List Folders',
|
title: 'List Folders',
|
||||||
description: 'List all folders in a workspace',
|
description: 'List all folders in a workspace',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
workspaceId: workspaceIdSchema,
|
workspaceId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Workspace ID (required if multiple workspaces are open)'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ workspaceId }) => {
|
async ({ workspaceId }) => {
|
||||||
@@ -33,116 +30,4 @@ export function registerFolderTools(server: McpServer, ctx: McpServerContext) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.registerTool(
|
|
||||||
'get_folder',
|
|
||||||
{
|
|
||||||
title: 'Get Folder',
|
|
||||||
description: 'Get details of a specific folder by ID',
|
|
||||||
inputSchema: {
|
|
||||||
id: z.string().describe('The folder ID'),
|
|
||||||
workspaceId: workspaceIdSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ id, workspaceId }) => {
|
|
||||||
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
|
|
||||||
const folder = await workspaceCtx.yaak.folder.getById({ id });
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text' as const,
|
|
||||||
text: JSON.stringify(folder, null, 2),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
server.registerTool(
|
|
||||||
'create_folder',
|
|
||||||
{
|
|
||||||
title: 'Create Folder',
|
|
||||||
description: 'Create a new folder in a workspace',
|
|
||||||
inputSchema: {
|
|
||||||
workspaceId: workspaceIdSchema,
|
|
||||||
name: z.string().describe('Folder name'),
|
|
||||||
folderId: z.string().optional().describe('Parent folder ID (for nested folders)'),
|
|
||||||
description: z.string().optional().describe('Folder description'),
|
|
||||||
sortPriority: z.number().optional().describe('Sort priority for ordering'),
|
|
||||||
headers: headersSchema.describe('Default headers to apply to requests in this folder'),
|
|
||||||
authenticationType: authenticationTypeSchema,
|
|
||||||
authentication: authenticationSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ workspaceId: ogWorkspaceId, ...args }) => {
|
|
||||||
const workspaceCtx = await getWorkspaceContext(ctx, ogWorkspaceId);
|
|
||||||
const workspaceId = await workspaceCtx.yaak.window.workspaceId();
|
|
||||||
if (!workspaceId) {
|
|
||||||
throw new Error('No workspace is open');
|
|
||||||
}
|
|
||||||
|
|
||||||
const folder = await workspaceCtx.yaak.folder.create({
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
...args,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [{ type: 'text' as const, text: JSON.stringify(folder, null, 2) }],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
server.registerTool(
|
|
||||||
'update_folder',
|
|
||||||
{
|
|
||||||
title: 'Update Folder',
|
|
||||||
description: 'Update an existing folder',
|
|
||||||
inputSchema: {
|
|
||||||
id: z.string().describe('Folder ID to update'),
|
|
||||||
workspaceId: workspaceIdSchema,
|
|
||||||
name: z.string().optional().describe('Folder name'),
|
|
||||||
folderId: z.string().optional().describe('Parent folder ID (for nested folders)'),
|
|
||||||
description: z.string().optional().describe('Folder description'),
|
|
||||||
sortPriority: z.number().optional().describe('Sort priority for ordering'),
|
|
||||||
headers: headersSchema.describe('Default headers to apply to requests in this folder'),
|
|
||||||
authenticationType: authenticationTypeSchema,
|
|
||||||
authentication: authenticationSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ id, workspaceId, ...updates }) => {
|
|
||||||
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
|
|
||||||
// Fetch existing folder to merge with updates
|
|
||||||
const existing = await workspaceCtx.yaak.folder.getById({ id });
|
|
||||||
if (!existing) {
|
|
||||||
throw new Error(`Folder with ID ${id} not found`);
|
|
||||||
}
|
|
||||||
// Merge existing fields with updates
|
|
||||||
const folder = await workspaceCtx.yaak.folder.update({
|
|
||||||
...existing,
|
|
||||||
...updates,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
content: [{ type: 'text' as const, text: JSON.stringify(folder, null, 2) }],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
server.registerTool(
|
|
||||||
'delete_folder',
|
|
||||||
{
|
|
||||||
title: 'Delete Folder',
|
|
||||||
description: 'Delete a folder by ID',
|
|
||||||
inputSchema: {
|
|
||||||
id: z.string().describe('Folder ID to delete'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ id }) => {
|
|
||||||
const folder = await ctx.yaak.folder.delete({ id });
|
|
||||||
return {
|
|
||||||
content: [{ type: 'text' as const, text: `Deleted: ${folder.name} (${folder.id})` }],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,6 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import type { McpServerContext } from '../types.js';
|
import type { McpServerContext } from '../types.js';
|
||||||
import { getWorkspaceContext } from './helpers.js';
|
import { getWorkspaceContext } from './helpers.js';
|
||||||
import {
|
|
||||||
authenticationSchema,
|
|
||||||
authenticationTypeSchema,
|
|
||||||
bodySchema,
|
|
||||||
bodyTypeSchema,
|
|
||||||
headersSchema,
|
|
||||||
urlParametersSchema,
|
|
||||||
workspaceIdSchema,
|
|
||||||
} from './schemas.js';
|
|
||||||
|
|
||||||
export function registerHttpRequestTools(server: McpServer, ctx: McpServerContext) {
|
export function registerHttpRequestTools(server: McpServer, ctx: McpServerContext) {
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
@@ -19,7 +10,10 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex
|
|||||||
title: 'List HTTP Requests',
|
title: 'List HTTP Requests',
|
||||||
description: 'List all HTTP requests in a workspace',
|
description: 'List all HTTP requests in a workspace',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
workspaceId: workspaceIdSchema,
|
workspaceId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Workspace ID (required if multiple workspaces are open)'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ workspaceId }) => {
|
async ({ workspaceId }) => {
|
||||||
@@ -44,7 +38,10 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex
|
|||||||
description: 'Get details of a specific HTTP request by ID',
|
description: 'Get details of a specific HTTP request by ID',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
id: z.string().describe('The HTTP request ID'),
|
id: z.string().describe('The HTTP request ID'),
|
||||||
workspaceId: workspaceIdSchema,
|
workspaceId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Workspace ID (required if multiple workspaces are open)'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ id, workspaceId }) => {
|
async ({ id, workspaceId }) => {
|
||||||
@@ -70,7 +67,10 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
id: z.string().describe('The HTTP request ID to send'),
|
id: z.string().describe('The HTTP request ID to send'),
|
||||||
environmentId: z.string().optional().describe('Optional environment ID to use'),
|
environmentId: z.string().optional().describe('Optional environment ID to use'),
|
||||||
workspaceId: workspaceIdSchema,
|
workspaceId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Workspace ID (required if multiple workspaces are open)'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ id, workspaceId }) => {
|
async ({ id, workspaceId }) => {
|
||||||
@@ -99,7 +99,10 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex
|
|||||||
title: 'Create HTTP Request',
|
title: 'Create HTTP Request',
|
||||||
description: 'Create a new HTTP request',
|
description: 'Create a new HTTP request',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
workspaceId: workspaceIdSchema,
|
workspaceId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe('Workspace ID (required if multiple workspaces are open)'),
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -108,12 +111,62 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex
|
|||||||
method: z.string().optional().describe('HTTP method (defaults to GET)'),
|
method: z.string().optional().describe('HTTP method (defaults to GET)'),
|
||||||
folderId: z.string().optional().describe('Parent folder ID'),
|
folderId: z.string().optional().describe('Parent folder ID'),
|
||||||
description: z.string().optional().describe('Request description'),
|
description: z.string().optional().describe('Request description'),
|
||||||
headers: headersSchema.describe('Request headers'),
|
headers: z
|
||||||
urlParameters: urlParametersSchema,
|
.array(
|
||||||
bodyType: bodyTypeSchema,
|
z.object({
|
||||||
body: bodySchema,
|
name: z.string(),
|
||||||
authenticationType: authenticationTypeSchema,
|
value: z.string(),
|
||||||
authentication: authenticationSchema,
|
enabled: z.boolean().default(true),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.describe('Request headers'),
|
||||||
|
urlParameters: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.describe('URL query parameters'),
|
||||||
|
bodyType: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Body type. Supported values: "binary", "graphql", "application/x-www-form-urlencoded", "multipart/form-data", or any text-based type (e.g., "application/json", "text/plain")',
|
||||||
|
),
|
||||||
|
body: z
|
||||||
|
.record(z.string(), z.any())
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Body content object. Structure varies by bodyType:\n' +
|
||||||
|
'- "binary": { filePath: "/path/to/file" }\n' +
|
||||||
|
'- "graphql": { query: "{ users { name } }", variables: "{\\"id\\": \\"123\\"}" }\n' +
|
||||||
|
'- "application/x-www-form-urlencoded": { form: [{ name: "key", value: "val", enabled: true }] }\n' +
|
||||||
|
'- "multipart/form-data": { form: [{ name: "field", value: "text", file: "/path/to/file", enabled: true }] }\n' +
|
||||||
|
'- text-based (application/json, etc.): { text: "raw body content" }',
|
||||||
|
),
|
||||||
|
authenticationType: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Authentication type. Common values: "basic", "bearer", "oauth2", "apikey", "jwt", "awsv4", "oauth1", "ntlm", "none". Use null to inherit from parent folder/workspace.',
|
||||||
|
),
|
||||||
|
authentication: z
|
||||||
|
.record(z.string(), z.any())
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Authentication configuration object. Structure varies by authenticationType:\n' +
|
||||||
|
'- "basic": { username: "user", password: "pass" }\n' +
|
||||||
|
'- "bearer": { token: "abc123", prefix: "Bearer" }\n' +
|
||||||
|
'- "oauth2": { clientId: "...", clientSecret: "...", grantType: "authorization_code", authorizationUrl: "...", accessTokenUrl: "...", scope: "...", ... }\n' +
|
||||||
|
'- "apikey": { location: "header" | "query", key: "X-API-Key", value: "..." }\n' +
|
||||||
|
'- "jwt": { algorithm: "HS256", secret: "...", payload: "{ ... }" }\n' +
|
||||||
|
'- "awsv4": { accessKeyId: "...", secretAccessKey: "...", service: "sts", region: "us-east-1", sessionToken: "..." }\n' +
|
||||||
|
'- "none": {}',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ workspaceId: ogWorkspaceId, ...args }) => {
|
async ({ workspaceId: ogWorkspaceId, ...args }) => {
|
||||||
@@ -141,18 +194,68 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex
|
|||||||
description: 'Update an existing HTTP request',
|
description: 'Update an existing HTTP request',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
id: z.string().describe('HTTP request ID to update'),
|
id: z.string().describe('HTTP request ID to update'),
|
||||||
workspaceId: workspaceIdSchema,
|
workspaceId: z.string().describe('Workspace ID'),
|
||||||
name: z.string().optional().describe('Request name'),
|
name: z.string().optional().describe('Request name'),
|
||||||
url: z.string().optional().describe('Request URL'),
|
url: z.string().optional().describe('Request URL'),
|
||||||
method: z.string().optional().describe('HTTP method'),
|
method: z.string().optional().describe('HTTP method'),
|
||||||
folderId: z.string().optional().describe('Parent folder ID'),
|
folderId: z.string().optional().describe('Parent folder ID'),
|
||||||
description: z.string().optional().describe('Request description'),
|
description: z.string().optional().describe('Request description'),
|
||||||
headers: headersSchema.describe('Request headers'),
|
headers: z
|
||||||
urlParameters: urlParametersSchema,
|
.array(
|
||||||
bodyType: bodyTypeSchema,
|
z.object({
|
||||||
body: bodySchema,
|
name: z.string(),
|
||||||
authenticationType: authenticationTypeSchema,
|
value: z.string(),
|
||||||
authentication: authenticationSchema,
|
enabled: z.boolean().default(true),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.describe('Request headers'),
|
||||||
|
urlParameters: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.describe('URL query parameters'),
|
||||||
|
bodyType: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Body type. Supported values: "binary", "graphql", "application/x-www-form-urlencoded", "multipart/form-data", or any text-based type (e.g., "application/json", "text/plain")',
|
||||||
|
),
|
||||||
|
body: z
|
||||||
|
.record(z.string(), z.any())
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Body content object. Structure varies by bodyType:\n' +
|
||||||
|
'- "binary": { filePath: "/path/to/file" }\n' +
|
||||||
|
'- "graphql": { query: "{ users { name } }", variables: "{\\"id\\": \\"123\\"}" }\n' +
|
||||||
|
'- "application/x-www-form-urlencoded": { form: [{ name: "key", value: "val", enabled: true }] }\n' +
|
||||||
|
'- "multipart/form-data": { form: [{ name: "field", value: "text", file: "/path/to/file", enabled: true }] }\n' +
|
||||||
|
'- text-based (application/json, etc.): { text: "raw body content" }',
|
||||||
|
),
|
||||||
|
authenticationType: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Authentication type. Common values: "basic", "bearer", "oauth2", "apikey", "jwt", "awsv4", "oauth1", "ntlm", "none". Use null to inherit from parent folder/workspace.',
|
||||||
|
),
|
||||||
|
authentication: z
|
||||||
|
.record(z.string(), z.any())
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Authentication configuration object. Structure varies by authenticationType:\n' +
|
||||||
|
'- "basic": { username: "user", password: "pass" }\n' +
|
||||||
|
'- "bearer": { token: "abc123", prefix: "Bearer" }\n' +
|
||||||
|
'- "oauth2": { clientId: "...", clientSecret: "...", grantType: "authorization_code", authorizationUrl: "...", accessTokenUrl: "...", scope: "...", ... }\n' +
|
||||||
|
'- "apikey": { location: "header" | "query", key: "X-API-Key", value: "..." }\n' +
|
||||||
|
'- "jwt": { algorithm: "HS256", secret: "...", payload: "{ ... }" }\n' +
|
||||||
|
'- "awsv4": { accessKeyId: "...", secretAccessKey: "...", service: "sts", region: "us-east-1", sessionToken: "..." }\n' +
|
||||||
|
'- "none": {}',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ id, workspaceId, ...updates }) => {
|
async ({ id, workspaceId, ...updates }) => {
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import * as z from 'zod';
|
|
||||||
|
|
||||||
export const workspaceIdSchema = z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe('Workspace ID (required if multiple workspaces are open)');
|
|
||||||
|
|
||||||
export const headersSchema = z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
name: z.string(),
|
|
||||||
value: z.string(),
|
|
||||||
enabled: z.boolean().default(true),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
export const urlParametersSchema = z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
name: z.string(),
|
|
||||||
value: z.string(),
|
|
||||||
enabled: z.boolean().default(true),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.optional()
|
|
||||||
.describe('URL query parameters');
|
|
||||||
|
|
||||||
export const bodyTypeSchema = z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
'Body type. Supported values: "binary", "graphql", "application/x-www-form-urlencoded", "multipart/form-data", or any text-based type (e.g., "application/json", "text/plain")',
|
|
||||||
);
|
|
||||||
|
|
||||||
export const bodySchema = z
|
|
||||||
.record(z.string(), z.any())
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
'Body content object. Structure varies by bodyType:\n' +
|
|
||||||
'- "binary": { filePath: "/path/to/file" }\n' +
|
|
||||||
'- "graphql": { query: "{ users { name } }", variables: "{\\"id\\": \\"123\\"}" }\n' +
|
|
||||||
'- "application/x-www-form-urlencoded": { form: [{ name: "key", value: "val", enabled: true }] }\n' +
|
|
||||||
'- "multipart/form-data": { form: [{ name: "field", value: "text", file: "/path/to/file", enabled: true }] }\n' +
|
|
||||||
'- text-based (application/json, etc.): { text: "raw body content" }',
|
|
||||||
);
|
|
||||||
|
|
||||||
export const authenticationTypeSchema = z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
'Authentication type. Common values: "basic", "bearer", "oauth2", "apikey", "jwt", "awsv4", "oauth1", "ntlm", "none". Use null to inherit from parent.',
|
|
||||||
);
|
|
||||||
|
|
||||||
export const authenticationSchema = z
|
|
||||||
.record(z.string(), z.any())
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
'Authentication configuration object. Structure varies by authenticationType:\n' +
|
|
||||||
'- "basic": { username: "user", password: "pass" }\n' +
|
|
||||||
'- "bearer": { token: "abc123", prefix: "Bearer" }\n' +
|
|
||||||
'- "oauth2": { clientId: "...", clientSecret: "...", grantType: "authorization_code", authorizationUrl: "...", accessTokenUrl: "...", scope: "...", ... }\n' +
|
|
||||||
'- "apikey": { location: "header" | "query", key: "X-API-Key", value: "..." }\n' +
|
|
||||||
'- "jwt": { algorithm: "HS256", secret: "...", payload: "{ ... }" }\n' +
|
|
||||||
'- "awsv4": { accessKeyId: "...", secretAccessKey: "...", service: "sts", region: "us-east-1", sessionToken: "..." }\n' +
|
|
||||||
'- "none": {}',
|
|
||||||
);
|
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yaakcli build",
|
"build": "yaakcli build",
|
||||||
"dev": "yaakcli dev",
|
"dev": "yaakcli dev"
|
||||||
"test": "vitest --run tests"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ export const plugin: PluginDefinition = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
async onApply(_ctx, { values }) {
|
async onApply(_ctx, { values }) {
|
||||||
const username = values.username ?? '';
|
const { username, password } = values;
|
||||||
const password = values.password ?? '';
|
|
||||||
const value = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
const value = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||||
return { setHeaders: [{ name: 'Authorization', value }] };
|
return { setHeaders: [{ name: 'Authorization', value }] };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
import type { Context } from '@yaakapp/api';
|
|
||||||
import { describe, expect, test } from 'vitest';
|
|
||||||
import { plugin } from '../src';
|
|
||||||
|
|
||||||
const ctx = {} as Context;
|
|
||||||
|
|
||||||
describe('auth-basic', () => {
|
|
||||||
test('Both username and password', async () => {
|
|
||||||
expect(
|
|
||||||
await plugin.authentication?.onApply(ctx, {
|
|
||||||
values: { username: 'user', password: 'pass' },
|
|
||||||
headers: [],
|
|
||||||
url: 'https://yaak.app',
|
|
||||||
method: 'POST',
|
|
||||||
contextId: '111',
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
setHeaders: [{ name: 'Authorization', value: `Basic ${Buffer.from('user:pass').toString('base64')}` }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Empty password', async () => {
|
|
||||||
expect(
|
|
||||||
await plugin.authentication?.onApply(ctx, {
|
|
||||||
values: { username: 'apikey', password: '' },
|
|
||||||
headers: [],
|
|
||||||
url: 'https://yaak.app',
|
|
||||||
method: 'POST',
|
|
||||||
contextId: '111',
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
setHeaders: [{ name: 'Authorization', value: `Basic ${Buffer.from('apikey:').toString('base64')}` }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Missing password (undefined)', async () => {
|
|
||||||
expect(
|
|
||||||
await plugin.authentication?.onApply(ctx, {
|
|
||||||
values: { username: 'apikey' },
|
|
||||||
headers: [],
|
|
||||||
url: 'https://yaak.app',
|
|
||||||
method: 'POST',
|
|
||||||
contextId: '111',
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
setHeaders: [{ name: 'Authorization', value: `Basic ${Buffer.from('apikey:').toString('base64')}` }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Missing username (undefined)', async () => {
|
|
||||||
expect(
|
|
||||||
await plugin.authentication?.onApply(ctx, {
|
|
||||||
values: { password: 'secret' },
|
|
||||||
headers: [],
|
|
||||||
url: 'https://yaak.app',
|
|
||||||
method: 'POST',
|
|
||||||
contextId: '111',
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
setHeaders: [{ name: 'Authorization', value: `Basic ${Buffer.from(':secret').toString('base64')}` }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('No values (both undefined)', async () => {
|
|
||||||
expect(
|
|
||||||
await plugin.authentication?.onApply(ctx, {
|
|
||||||
values: {},
|
|
||||||
headers: [],
|
|
||||||
url: 'https://yaak.app',
|
|
||||||
method: 'POST',
|
|
||||||
contextId: '111',
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
setHeaders: [{ name: 'Authorization', value: `Basic ${Buffer.from(':').toString('base64')}` }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
||||||
import http from 'node:http';
|
|
||||||
import type { Context } from '@yaakapp/api';
|
|
||||||
|
|
||||||
export const HOSTED_CALLBACK_URL = 'https://oauth.yaak.app/redirect';
|
|
||||||
export const DEFAULT_LOCALHOST_PORT = 8765;
|
|
||||||
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
||||||
|
|
||||||
/** Singleton: only one callback server runs at a time across all OAuth flows. */
|
|
||||||
let activeServer: CallbackServerResult | null = null;
|
|
||||||
|
|
||||||
export interface CallbackServerResult {
|
|
||||||
/** The port the server is listening on */
|
|
||||||
port: number;
|
|
||||||
/** The full redirect URI to register with the OAuth provider */
|
|
||||||
redirectUri: string;
|
|
||||||
/** Promise that resolves with the callback URL when received */
|
|
||||||
waitForCallback: () => Promise<string>;
|
|
||||||
/** Stop the server */
|
|
||||||
stop: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a local HTTP server to receive OAuth callbacks.
|
|
||||||
* Only one server runs at a time — if a previous server is still active,
|
|
||||||
* it is stopped before starting the new one.
|
|
||||||
* Returns the port, redirect URI, and a promise that resolves when the callback is received.
|
|
||||||
*/
|
|
||||||
export function startCallbackServer(options: {
|
|
||||||
/** Specific port to use, or 0 for random available port */
|
|
||||||
port?: number;
|
|
||||||
/** Path for the callback endpoint */
|
|
||||||
path?: string;
|
|
||||||
/** Timeout in milliseconds (default 5 minutes) */
|
|
||||||
timeoutMs?: number;
|
|
||||||
}): Promise<CallbackServerResult> {
|
|
||||||
// Stop any previously active server before starting a new one
|
|
||||||
if (activeServer) {
|
|
||||||
console.log('[oauth2] Stopping previous callback server before starting new one');
|
|
||||||
activeServer.stop();
|
|
||||||
activeServer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { port = 0, path = '/callback', timeoutMs = CALLBACK_TIMEOUT_MS } = options;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let callbackResolve: ((url: string) => void) | null = null;
|
|
||||||
let callbackReject: ((err: Error) => void) | null = null;
|
|
||||||
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let stopped = false;
|
|
||||||
|
|
||||||
const server = http.createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
||||||
const reqUrl = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
||||||
|
|
||||||
// Only handle the callback path
|
|
||||||
if (reqUrl.pathname !== path && reqUrl.pathname !== `${path}/`) {
|
|
||||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end('Not Found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
// POST: read JSON body with the final callback URL and resolve
|
|
||||||
let body = '';
|
|
||||||
req.on('data', (chunk: Buffer) => {
|
|
||||||
body += chunk.toString();
|
|
||||||
});
|
|
||||||
req.on('end', () => {
|
|
||||||
try {
|
|
||||||
const { url: callbackUrl } = JSON.parse(body);
|
|
||||||
if (!callbackUrl || typeof callbackUrl !== 'string') {
|
|
||||||
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end('Missing url in request body');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send success response
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end('OK');
|
|
||||||
|
|
||||||
// Resolve the callback promise
|
|
||||||
if (callbackResolve) {
|
|
||||||
callbackResolve(callbackUrl);
|
|
||||||
callbackResolve = null;
|
|
||||||
callbackReject = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the server after a short delay to ensure response is sent
|
|
||||||
setTimeout(() => stopServer(), 100);
|
|
||||||
} catch {
|
|
||||||
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end('Invalid JSON');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET: serve intermediate page that reads the fragment and POSTs back
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
||||||
res.end(getFragmentForwardingHtml());
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on('error', (err: Error) => {
|
|
||||||
if (!stopped) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const stopServer = () => {
|
|
||||||
if (stopped) return;
|
|
||||||
stopped = true;
|
|
||||||
|
|
||||||
// Clear the singleton reference
|
|
||||||
if (activeServer?.stop === stopServer) {
|
|
||||||
activeServer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeoutHandle) {
|
|
||||||
clearTimeout(timeoutHandle);
|
|
||||||
timeoutHandle = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
server.close();
|
|
||||||
|
|
||||||
if (callbackReject) {
|
|
||||||
callbackReject(new Error('Callback server stopped'));
|
|
||||||
callbackResolve = null;
|
|
||||||
callbackReject = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
server.listen(port, '127.0.0.1', () => {
|
|
||||||
const address = server.address();
|
|
||||||
if (!address || typeof address === 'string') {
|
|
||||||
reject(new Error('Failed to get server address'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualPort = address.port;
|
|
||||||
const redirectUri = `http://127.0.0.1:${actualPort}${path}`;
|
|
||||||
|
|
||||||
console.log(`[oauth2] Callback server listening on ${redirectUri}`);
|
|
||||||
|
|
||||||
const result: CallbackServerResult = {
|
|
||||||
port: actualPort,
|
|
||||||
redirectUri,
|
|
||||||
waitForCallback: () => {
|
|
||||||
return new Promise<string>((res, rej) => {
|
|
||||||
if (stopped) {
|
|
||||||
rej(new Error('Callback server already stopped'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
callbackResolve = res;
|
|
||||||
callbackReject = rej;
|
|
||||||
|
|
||||||
// Set timeout
|
|
||||||
timeoutHandle = setTimeout(() => {
|
|
||||||
if (callbackReject) {
|
|
||||||
callbackReject(new Error('Authorization timed out'));
|
|
||||||
callbackResolve = null;
|
|
||||||
callbackReject = null;
|
|
||||||
}
|
|
||||||
stopServer();
|
|
||||||
}, timeoutMs);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
stop: stopServer,
|
|
||||||
};
|
|
||||||
|
|
||||||
activeServer = result;
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the redirect URI for the hosted callback page.
|
|
||||||
* The hosted page will redirect to the local server with the OAuth response.
|
|
||||||
*/
|
|
||||||
export function buildHostedCallbackRedirectUri(localPort: number, localPath: string): string {
|
|
||||||
const localRedirectUri = `http://127.0.0.1:${localPort}${localPath}`;
|
|
||||||
// The hosted callback page will read params and redirect to the local server
|
|
||||||
return `${HOSTED_CALLBACK_URL}?redirect_to=${encodeURIComponent(localRedirectUri)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the active callback server if one is running.
|
|
||||||
* Called during plugin dispose to ensure the server is cleaned up before the process exits.
|
|
||||||
*/
|
|
||||||
export function stopActiveServer(): void {
|
|
||||||
if (activeServer) {
|
|
||||||
console.log('[oauth2] Stopping active callback server during dispose');
|
|
||||||
activeServer.stop();
|
|
||||||
activeServer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open an authorization URL in the system browser, start a local callback server,
|
|
||||||
* and wait for the OAuth provider to redirect back.
|
|
||||||
*
|
|
||||||
* Returns the raw callback URL and the redirect URI that was registered with the
|
|
||||||
* OAuth provider (needed for token exchange).
|
|
||||||
*/
|
|
||||||
export async function getRedirectUrlViaExternalBrowser(
|
|
||||||
ctx: Context,
|
|
||||||
authorizationUrl: URL,
|
|
||||||
options: {
|
|
||||||
callbackType: 'localhost' | 'hosted';
|
|
||||||
callbackPort?: number;
|
|
||||||
},
|
|
||||||
): Promise<{ callbackUrl: string; redirectUri: string }> {
|
|
||||||
const { callbackType, callbackPort } = options;
|
|
||||||
|
|
||||||
// Determine port based on callback type:
|
|
||||||
// - localhost: use specified port or default stable port
|
|
||||||
// - hosted: use random port (0) since hosted page redirects to local
|
|
||||||
const port = callbackType === 'localhost' ? (callbackPort ?? DEFAULT_LOCALHOST_PORT) : 0;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[oauth2] Starting callback server (type: ${callbackType}, port: ${port || 'random'})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const server = await startCallbackServer({
|
|
||||||
port,
|
|
||||||
path: '/callback',
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Determine the redirect URI to send to the OAuth provider
|
|
||||||
let oauthRedirectUri: string;
|
|
||||||
|
|
||||||
if (callbackType === 'hosted') {
|
|
||||||
oauthRedirectUri = buildHostedCallbackRedirectUri(server.port, '/callback');
|
|
||||||
console.log('[oauth2] Using hosted callback redirect:', oauthRedirectUri);
|
|
||||||
} else {
|
|
||||||
oauthRedirectUri = server.redirectUri;
|
|
||||||
console.log('[oauth2] Using localhost callback redirect:', oauthRedirectUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the redirect URI on the authorization URL
|
|
||||||
authorizationUrl.searchParams.set('redirect_uri', oauthRedirectUri);
|
|
||||||
|
|
||||||
const authorizationUrlStr = authorizationUrl.toString();
|
|
||||||
console.log('[oauth2] Opening external browser:', authorizationUrlStr);
|
|
||||||
|
|
||||||
// Show toast to inform user
|
|
||||||
await ctx.toast.show({
|
|
||||||
message: 'Opening browser for authorization...',
|
|
||||||
icon: 'info',
|
|
||||||
timeout: 3000,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open the system browser
|
|
||||||
await ctx.window.openExternalUrl(authorizationUrlStr);
|
|
||||||
|
|
||||||
// Wait for the callback
|
|
||||||
console.log('[oauth2] Waiting for callback on', server.redirectUri);
|
|
||||||
const callbackUrl = await server.waitForCallback();
|
|
||||||
|
|
||||||
console.log('[oauth2] Received callback:', callbackUrl);
|
|
||||||
|
|
||||||
return { callbackUrl, redirectUri: oauthRedirectUri };
|
|
||||||
} finally {
|
|
||||||
server.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intermediate HTML page that reads the URL fragment and _fragment query param,
|
|
||||||
* reconstructs a proper OAuth callback URL, and POSTs it back to the server.
|
|
||||||
*
|
|
||||||
* Handles three cases:
|
|
||||||
* - Localhost implicit: fragment is in location.hash (e.g. #access_token=...)
|
|
||||||
* - Hosted implicit: fragment was converted to ?_fragment=... by the hosted redirect page
|
|
||||||
* - Auth code: no fragment, code is already in query params
|
|
||||||
*/
|
|
||||||
function getFragmentForwardingHtml(): string {
|
|
||||||
return `<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Yaak</title>
|
|
||||||
<style>
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: hsl(244,23%,14%);
|
|
||||||
color: hsl(245,23%,85%);
|
|
||||||
}
|
|
||||||
.container { text-align: center; }
|
|
||||||
.logo { display: block; width: 100px; height: 100px; margin: 0 auto 32px; border-radius: 50%; }
|
|
||||||
h1 { font-size: 28px; font-weight: 600; margin-bottom: 12px; }
|
|
||||||
p { font-size: 16px; color: hsl(245,18%,58%); }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<svg class="logo" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(649.94,712.03,-712.03,649.94,179.25,220.59)"><stop offset="0" stop-color="#4cc48c"/><stop offset=".5" stop-color="#476cc9"/><stop offset="1" stop-color="#ba1ab7"/></linearGradient></defs><rect x="0" y="0" width="1024" height="1024" fill="url(#g)"/><g transform="matrix(0.822,0,0,0.822,91.26,91.26)"><path d="M766.775,105.176C902.046,190.129 992.031,340.639 992.031,512C992.031,706.357 876.274,873.892 710,949.361C684.748,838.221 632.417,791.074 538.602,758.96C536.859,790.593 545.561,854.983 522.327,856.611C477.951,859.719 321.557,782.368 310.75,710.135C300.443,641.237 302.536,535.834 294.475,482.283C86.974,483.114 245.65,303.256 245.65,303.256L261.925,368.357L294.475,368.357C294.475,368.357 298.094,296.03 310.75,286.981C326.511,275.713 366.457,254.592 473.502,254.431C519.506,190.629 692.164,133.645 766.775,105.176ZM603.703,352.082C598.577,358.301 614.243,384.787 623.39,401.682C639.967,432.299 672.34,459.32 760.231,456.739C780.796,456.135 808.649,456.743 831.555,448.316C919.689,369.191 665.548,260.941 652.528,270.706C629.157,288.235 677.433,340.481 685.079,352.082C663.595,350.818 630.521,352.121 603.703,352.082ZM515.817,516.822C491.026,516.822 470.898,536.949 470.898,561.741C470.898,586.532 491.026,606.66 515.817,606.66C540.609,606.66 560.736,586.532 560.736,561.741C560.736,536.949 540.609,516.822 515.817,516.822ZM656.608,969.83C610.979,984.25 562.391,992.031 512,992.031C247.063,992.031 31.969,776.937 31.969,512C31.969,247.063 247.063,31.969 512,31.969C581.652,31.969 647.859,46.835 707.634,73.574C674.574,86.913 627.224,104.986 620,103.081C343.573,30.201 98.64,283.528 98.64,511.993C98.64,761.842 376.244,989.043 627.831,910C637.21,907.053 645.743,936.753 656.608,969.83Z" fill="#fff"/></g></svg>
|
|
||||||
<h1 id="title">Authorizing...</h1>
|
|
||||||
<p id="message">Please wait</p>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
var title = document.getElementById('title');
|
|
||||||
var message = document.getElementById('message');
|
|
||||||
var url = new URL(window.location.href);
|
|
||||||
var fragment = window.location.hash;
|
|
||||||
var fragmentParam = url.searchParams.get('_fragment');
|
|
||||||
|
|
||||||
// Build the final callback URL:
|
|
||||||
// 1. If _fragment query param exists (from hosted redirect), convert it back to a real fragment
|
|
||||||
// 2. If location.hash exists (direct localhost implicit), use it as-is
|
|
||||||
// 3. Otherwise (auth code flow), use the URL as-is with query params
|
|
||||||
if (fragmentParam) {
|
|
||||||
url.searchParams.delete('_fragment');
|
|
||||||
url.hash = fragmentParam;
|
|
||||||
} else if (fragment && fragment.length > 1) {
|
|
||||||
url.hash = fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST the final URL back to the callback server
|
|
||||||
fetch(url.pathname, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ url: url.toString() })
|
|
||||||
}).then(function(res) {
|
|
||||||
if (res.ok) {
|
|
||||||
title.textContent = 'Authorization Complete';
|
|
||||||
message.textContent = 'You may close this tab and return to Yaak';
|
|
||||||
} else {
|
|
||||||
title.textContent = 'Authorization Failed';
|
|
||||||
message.textContent = 'Something went wrong. Please try again.';
|
|
||||||
}
|
|
||||||
}).catch(function() {
|
|
||||||
title.textContent = 'Authorization Failed';
|
|
||||||
message.textContent = 'Something went wrong. Please try again.';
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createHash, randomBytes } from 'node:crypto';
|
import { createHash, randomBytes } from 'node:crypto';
|
||||||
import type { Context } from '@yaakapp/api';
|
import type { Context } from '@yaakapp/api';
|
||||||
import { getRedirectUrlViaExternalBrowser } from '../callbackServer';
|
|
||||||
import { fetchAccessToken } from '../fetchAccessToken';
|
import { fetchAccessToken } from '../fetchAccessToken';
|
||||||
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
|
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
|
||||||
import type { AccessToken, TokenStoreArgs } from '../store';
|
import type { AccessToken, TokenStoreArgs } from '../store';
|
||||||
@@ -11,15 +10,6 @@ export const PKCE_SHA256 = 'S256';
|
|||||||
export const PKCE_PLAIN = 'plain';
|
export const PKCE_PLAIN = 'plain';
|
||||||
export const DEFAULT_PKCE_METHOD = PKCE_SHA256;
|
export const DEFAULT_PKCE_METHOD = PKCE_SHA256;
|
||||||
|
|
||||||
export type CallbackType = 'localhost' | 'hosted';
|
|
||||||
|
|
||||||
export interface ExternalBrowserOptions {
|
|
||||||
useExternalBrowser: boolean;
|
|
||||||
callbackType: CallbackType;
|
|
||||||
/** Port for localhost callback (only used when callbackType is 'localhost') */
|
|
||||||
callbackPort?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAuthorizationCode(
|
export async function getAuthorizationCode(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
contextId: string,
|
contextId: string,
|
||||||
@@ -35,7 +25,6 @@ export async function getAuthorizationCode(
|
|||||||
credentialsInBody,
|
credentialsInBody,
|
||||||
pkce,
|
pkce,
|
||||||
tokenName,
|
tokenName,
|
||||||
externalBrowser,
|
|
||||||
}: {
|
}: {
|
||||||
authorizationUrl: string;
|
authorizationUrl: string;
|
||||||
accessTokenUrl: string;
|
accessTokenUrl: string;
|
||||||
@@ -51,7 +40,6 @@ export async function getAuthorizationCode(
|
|||||||
codeVerifier: string;
|
codeVerifier: string;
|
||||||
} | null;
|
} | null;
|
||||||
tokenName: 'access_token' | 'id_token';
|
tokenName: 'access_token' | 'id_token';
|
||||||
externalBrowser?: ExternalBrowserOptions;
|
|
||||||
},
|
},
|
||||||
): Promise<AccessToken> {
|
): Promise<AccessToken> {
|
||||||
const tokenArgs: TokenStoreArgs = {
|
const tokenArgs: TokenStoreArgs = {
|
||||||
@@ -80,6 +68,7 @@ export async function getAuthorizationCode(
|
|||||||
}
|
}
|
||||||
authorizationUrl.searchParams.set('response_type', 'code');
|
authorizationUrl.searchParams.set('response_type', 'code');
|
||||||
authorizationUrl.searchParams.set('client_id', clientId);
|
authorizationUrl.searchParams.set('client_id', clientId);
|
||||||
|
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
|
||||||
if (scope) authorizationUrl.searchParams.set('scope', scope);
|
if (scope) authorizationUrl.searchParams.set('scope', scope);
|
||||||
if (state) authorizationUrl.searchParams.set('state', state);
|
if (state) authorizationUrl.searchParams.set('state', state);
|
||||||
if (audience) authorizationUrl.searchParams.set('audience', audience);
|
if (audience) authorizationUrl.searchParams.set('audience', audience);
|
||||||
@@ -91,65 +80,12 @@ export async function getAuthorizationCode(
|
|||||||
authorizationUrl.searchParams.set('code_challenge_method', pkce.challengeMethod);
|
authorizationUrl.searchParams.set('code_challenge_method', pkce.challengeMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
let code: string;
|
|
||||||
let actualRedirectUri: string | null = redirectUri;
|
|
||||||
|
|
||||||
// Use external browser flow if enabled
|
|
||||||
if (externalBrowser?.useExternalBrowser) {
|
|
||||||
const result = await getRedirectUrlViaExternalBrowser(ctx, authorizationUrl, {
|
|
||||||
callbackType: externalBrowser.callbackType,
|
|
||||||
callbackPort: externalBrowser.callbackPort,
|
|
||||||
});
|
|
||||||
// Pass null to skip redirect URI matching — the callback came from our own local server
|
|
||||||
const extractedCode = extractCode(result.callbackUrl, null);
|
|
||||||
if (!extractedCode) {
|
|
||||||
throw new Error('No authorization code found in callback URL');
|
|
||||||
}
|
|
||||||
code = extractedCode;
|
|
||||||
actualRedirectUri = result.redirectUri;
|
|
||||||
} else {
|
|
||||||
// Use embedded browser flow (original behavior)
|
|
||||||
if (redirectUri) {
|
|
||||||
authorizationUrl.searchParams.set('redirect_uri', redirectUri);
|
|
||||||
}
|
|
||||||
code = await getCodeViaEmbeddedBrowser(ctx, contextId, authorizationUrl, redirectUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[oauth2] Code found');
|
|
||||||
const response = await fetchAccessToken(ctx, {
|
|
||||||
grantType: 'authorization_code',
|
|
||||||
accessTokenUrl,
|
|
||||||
clientId,
|
|
||||||
clientSecret,
|
|
||||||
scope,
|
|
||||||
audience,
|
|
||||||
credentialsInBody,
|
|
||||||
params: [
|
|
||||||
{ name: 'code', value: code },
|
|
||||||
...(pkce ? [{ name: 'code_verifier', value: pkce.codeVerifier }] : []),
|
|
||||||
...(actualRedirectUri ? [{ name: 'redirect_uri', value: actualRedirectUri }] : []),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
return storeToken(ctx, tokenArgs, response, tokenName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get authorization code using the embedded browser window.
|
|
||||||
* This is the original flow that monitors navigation events.
|
|
||||||
*/
|
|
||||||
async function getCodeViaEmbeddedBrowser(
|
|
||||||
ctx: Context,
|
|
||||||
contextId: string,
|
|
||||||
authorizationUrl: URL,
|
|
||||||
redirectUri: string | null,
|
|
||||||
): Promise<string> {
|
|
||||||
const dataDirKey = await getDataDirKey(ctx, contextId);
|
const dataDirKey = await getDataDirKey(ctx, contextId);
|
||||||
const authorizationUrlStr = authorizationUrl.toString();
|
const authorizationUrlStr = authorizationUrl.toString();
|
||||||
console.log('[oauth2] Authorizing via embedded browser', authorizationUrlStr);
|
console.log('[oauth2] Authorizing', authorizationUrlStr);
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: Required for this pattern
|
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: none
|
||||||
return new Promise<string>(async (resolve, reject) => {
|
const code = await new Promise<string>(async (resolve, reject) => {
|
||||||
let foundCode = false;
|
let foundCode = false;
|
||||||
const { close } = await ctx.window.openUrl({
|
const { close } = await ctx.window.openUrl({
|
||||||
dataDirKey,
|
dataDirKey,
|
||||||
@@ -174,12 +110,31 @@ async function getCodeViaEmbeddedBrowser(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the window here, because we don't need it anymore!
|
||||||
foundCode = true;
|
foundCode = true;
|
||||||
close();
|
close();
|
||||||
resolve(code);
|
resolve(code);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[oauth2] Code found');
|
||||||
|
const response = await fetchAccessToken(ctx, {
|
||||||
|
grantType: 'authorization_code',
|
||||||
|
accessTokenUrl,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
scope,
|
||||||
|
audience,
|
||||||
|
credentialsInBody,
|
||||||
|
params: [
|
||||||
|
{ name: 'code', value: code },
|
||||||
|
...(pkce ? [{ name: 'code_verifier', value: pkce.codeVerifier }] : []),
|
||||||
|
...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return storeToken(ctx, tokenArgs, response, tokenName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function genPkceCodeVerifier() {
|
export function genPkceCodeVerifier() {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import type { Context } from '@yaakapp/api';
|
import type { Context } from '@yaakapp/api';
|
||||||
import { getRedirectUrlViaExternalBrowser } from '../callbackServer';
|
|
||||||
import type { AccessToken, AccessTokenRawResponse } from '../store';
|
import type { AccessToken, AccessTokenRawResponse } from '../store';
|
||||||
import { getDataDirKey, getToken, storeToken } from '../store';
|
import { getDataDirKey, getToken, storeToken } from '../store';
|
||||||
import { isTokenExpired } from '../util';
|
import { isTokenExpired } from '../util';
|
||||||
import type { ExternalBrowserOptions } from './authorizationCode';
|
|
||||||
|
|
||||||
export async function getImplicit(
|
export async function getImplicit(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
@@ -17,7 +15,6 @@ export async function getImplicit(
|
|||||||
state,
|
state,
|
||||||
audience,
|
audience,
|
||||||
tokenName,
|
tokenName,
|
||||||
externalBrowser,
|
|
||||||
}: {
|
}: {
|
||||||
authorizationUrl: string;
|
authorizationUrl: string;
|
||||||
responseType: string;
|
responseType: string;
|
||||||
@@ -27,7 +24,6 @@ export async function getImplicit(
|
|||||||
state: string | null;
|
state: string | null;
|
||||||
audience: string | null;
|
audience: string | null;
|
||||||
tokenName: 'access_token' | 'id_token';
|
tokenName: 'access_token' | 'id_token';
|
||||||
externalBrowser?: ExternalBrowserOptions;
|
|
||||||
},
|
},
|
||||||
): Promise<AccessToken> {
|
): Promise<AccessToken> {
|
||||||
const tokenArgs = {
|
const tokenArgs = {
|
||||||
@@ -47,8 +43,9 @@ export async function getImplicit(
|
|||||||
} catch {
|
} catch {
|
||||||
throw new Error(`Invalid authorization URL "${authorizationUrlRaw}"`);
|
throw new Error(`Invalid authorization URL "${authorizationUrlRaw}"`);
|
||||||
}
|
}
|
||||||
authorizationUrl.searchParams.set('response_type', responseType);
|
authorizationUrl.searchParams.set('response_type', 'token');
|
||||||
authorizationUrl.searchParams.set('client_id', clientId);
|
authorizationUrl.searchParams.set('client_id', clientId);
|
||||||
|
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
|
||||||
if (scope) authorizationUrl.searchParams.set('scope', scope);
|
if (scope) authorizationUrl.searchParams.set('scope', scope);
|
||||||
if (state) authorizationUrl.searchParams.set('state', state);
|
if (state) authorizationUrl.searchParams.set('state', state);
|
||||||
if (audience) authorizationUrl.searchParams.set('audience', audience);
|
if (audience) authorizationUrl.searchParams.set('audience', audience);
|
||||||
@@ -59,55 +56,11 @@ export async function getImplicit(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let newToken: AccessToken;
|
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: none
|
||||||
|
const newToken = await new Promise<AccessToken>(async (resolve, reject) => {
|
||||||
// Use external browser flow if enabled
|
|
||||||
if (externalBrowser?.useExternalBrowser) {
|
|
||||||
const result = await getRedirectUrlViaExternalBrowser(ctx, authorizationUrl, {
|
|
||||||
callbackType: externalBrowser.callbackType,
|
|
||||||
callbackPort: externalBrowser.callbackPort,
|
|
||||||
});
|
|
||||||
newToken = await extractImplicitToken(ctx, result.callbackUrl, tokenArgs, tokenName);
|
|
||||||
} else {
|
|
||||||
// Use embedded browser flow (original behavior)
|
|
||||||
if (redirectUri) {
|
|
||||||
authorizationUrl.searchParams.set('redirect_uri', redirectUri);
|
|
||||||
}
|
|
||||||
newToken = await getTokenViaEmbeddedBrowser(
|
|
||||||
ctx,
|
|
||||||
contextId,
|
|
||||||
authorizationUrl,
|
|
||||||
tokenArgs,
|
|
||||||
tokenName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get token using the embedded browser window.
|
|
||||||
* This is the original flow that monitors navigation events.
|
|
||||||
*/
|
|
||||||
async function getTokenViaEmbeddedBrowser(
|
|
||||||
ctx: Context,
|
|
||||||
contextId: string,
|
|
||||||
authorizationUrl: URL,
|
|
||||||
tokenArgs: {
|
|
||||||
contextId: string;
|
|
||||||
clientId: string;
|
|
||||||
accessTokenUrl: null;
|
|
||||||
authorizationUrl: string;
|
|
||||||
},
|
|
||||||
tokenName: 'access_token' | 'id_token',
|
|
||||||
): Promise<AccessToken> {
|
|
||||||
const dataDirKey = await getDataDirKey(ctx, contextId);
|
|
||||||
const authorizationUrlStr = authorizationUrl.toString();
|
|
||||||
console.log('[oauth2] Authorizing via embedded browser (implicit)', authorizationUrlStr);
|
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: Required for this pattern
|
|
||||||
return new Promise<AccessToken>(async (resolve, reject) => {
|
|
||||||
let foundAccessToken = false;
|
let foundAccessToken = false;
|
||||||
|
const authorizationUrlStr = authorizationUrl.toString();
|
||||||
|
const dataDirKey = await getDataDirKey(ctx, contextId);
|
||||||
const { close } = await ctx.window.openUrl({
|
const { close } = await ctx.window.openUrl({
|
||||||
dataDirKey,
|
dataDirKey,
|
||||||
url: authorizationUrlStr,
|
url: authorizationUrlStr,
|
||||||
@@ -144,56 +97,6 @@ async function getTokenViaEmbeddedBrowser(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
return newToken;
|
||||||
/**
|
|
||||||
* Extract the implicit grant token from a callback URL and store it.
|
|
||||||
*/
|
|
||||||
async function extractImplicitToken(
|
|
||||||
ctx: Context,
|
|
||||||
callbackUrl: string,
|
|
||||||
tokenArgs: {
|
|
||||||
contextId: string;
|
|
||||||
clientId: string;
|
|
||||||
accessTokenUrl: null;
|
|
||||||
authorizationUrl: string;
|
|
||||||
},
|
|
||||||
tokenName: 'access_token' | 'id_token',
|
|
||||||
): Promise<AccessToken> {
|
|
||||||
const url = new URL(callbackUrl);
|
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
if (url.searchParams.has('error')) {
|
|
||||||
throw new Error(`Failed to authorize: ${url.searchParams.get('error')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract token from fragment
|
|
||||||
const hash = url.hash.slice(1);
|
|
||||||
const params = new URLSearchParams(hash);
|
|
||||||
|
|
||||||
// Also check query params (in case fragment was converted)
|
|
||||||
const accessToken = params.get(tokenName) ?? url.searchParams.get(tokenName);
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error(`No ${tokenName} found in callback URL`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build response from params (prefer fragment, fall back to query)
|
|
||||||
const response: AccessTokenRawResponse = {
|
|
||||||
access_token: params.get('access_token') ?? url.searchParams.get('access_token') ?? '',
|
|
||||||
token_type: params.get('token_type') ?? url.searchParams.get('token_type') ?? undefined,
|
|
||||||
expires_in: params.has('expires_in')
|
|
||||||
? parseInt(params.get('expires_in') ?? '0', 10)
|
|
||||||
: url.searchParams.has('expires_in')
|
|
||||||
? parseInt(url.searchParams.get('expires_in') ?? '0', 10)
|
|
||||||
: undefined,
|
|
||||||
scope: params.get('scope') ?? url.searchParams.get('scope') ?? undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Include id_token if present
|
|
||||||
const idToken = params.get('id_token') ?? url.searchParams.get('id_token');
|
|
||||||
if (idToken) {
|
|
||||||
response.id_token = idToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
return storeToken(ctx, tokenArgs, response);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import type {
|
|||||||
JsonPrimitive,
|
JsonPrimitive,
|
||||||
PluginDefinition,
|
PluginDefinition,
|
||||||
} from '@yaakapp/api';
|
} from '@yaakapp/api';
|
||||||
import { DEFAULT_LOCALHOST_PORT, HOSTED_CALLBACK_URL, stopActiveServer } from './callbackServer';
|
|
||||||
import {
|
import {
|
||||||
type CallbackType,
|
|
||||||
DEFAULT_PKCE_METHOD,
|
DEFAULT_PKCE_METHOD,
|
||||||
genPkceCodeVerifier,
|
genPkceCodeVerifier,
|
||||||
getAuthorizationCode,
|
getAuthorizationCode,
|
||||||
@@ -78,9 +76,6 @@ const accessTokenUrls = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const plugin: PluginDefinition = {
|
export const plugin: PluginDefinition = {
|
||||||
dispose() {
|
|
||||||
stopActiveServer();
|
|
||||||
},
|
|
||||||
authentication: {
|
authentication: {
|
||||||
name: 'oauth2',
|
name: 'oauth2',
|
||||||
label: 'OAuth 2.0',
|
label: 'OAuth 2.0',
|
||||||
@@ -139,6 +134,8 @@ export const plugin: PluginDefinition = {
|
|||||||
defaultValue: defaultGrantType,
|
defaultValue: defaultGrantType,
|
||||||
options: grantTypes,
|
options: grantTypes,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Always-present fields
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'clientId',
|
name: 'clientId',
|
||||||
@@ -172,105 +169,11 @@ export const plugin: PluginDefinition = {
|
|||||||
completionOptions: accessTokenUrls.map((url) => ({ label: url, value: url })),
|
completionOptions: accessTokenUrls.map((url) => ({ label: url, value: url })),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'banner',
|
type: 'text',
|
||||||
inputs: [
|
name: 'redirectUri',
|
||||||
{
|
label: 'Redirect URI',
|
||||||
type: 'checkbox',
|
optional: true,
|
||||||
name: 'useExternalBrowser',
|
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
||||||
label: 'Use External Browser',
|
|
||||||
description:
|
|
||||||
'Open authorization URL in your system browser instead of the embedded browser. ' +
|
|
||||||
'Useful when the OAuth provider blocks embedded browsers or you need existing browser sessions.',
|
|
||||||
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
name: 'redirectUri',
|
|
||||||
label: 'Redirect URI',
|
|
||||||
description:
|
|
||||||
'URI the OAuth provider redirects to after authorization. Yaak intercepts this automatically in its embedded browser so any valid URI will work.',
|
|
||||||
optional: true,
|
|
||||||
dynamic: hiddenIfNot(
|
|
||||||
['authorization_code', 'implicit'],
|
|
||||||
({ useExternalBrowser }) => !useExternalBrowser,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'h_stack',
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
type: 'select',
|
|
||||||
name: 'callbackType',
|
|
||||||
label: 'Callback Type',
|
|
||||||
description:
|
|
||||||
'"Hosted Redirect" uses an external Yaak-hosted endpoint. "Localhost" starts a local server to receive the callback.',
|
|
||||||
defaultValue: 'hosted',
|
|
||||||
options: [
|
|
||||||
{ label: 'Hosted Redirect', value: 'hosted' },
|
|
||||||
{ label: 'Localhost', value: 'localhost' },
|
|
||||||
],
|
|
||||||
dynamic: hiddenIfNot(
|
|
||||||
['authorization_code', 'implicit'],
|
|
||||||
({ useExternalBrowser }) => !!useExternalBrowser,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
name: 'callbackPort',
|
|
||||||
label: 'Callback Port',
|
|
||||||
placeholder: `${DEFAULT_LOCALHOST_PORT}`,
|
|
||||||
description:
|
|
||||||
'Port for the local callback server. Defaults to ' +
|
|
||||||
DEFAULT_LOCALHOST_PORT +
|
|
||||||
' if empty.',
|
|
||||||
optional: true,
|
|
||||||
dynamic: hiddenIfNot(
|
|
||||||
['authorization_code', 'implicit'],
|
|
||||||
({ useExternalBrowser, callbackType }) =>
|
|
||||||
!!useExternalBrowser && callbackType === 'localhost',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'banner',
|
|
||||||
color: 'info',
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
type: 'markdown',
|
|
||||||
content: 'Redirect URI to Register',
|
|
||||||
async dynamic(_ctx, { values }) {
|
|
||||||
const grantType = String(values.grantType ?? defaultGrantType);
|
|
||||||
const useExternalBrowser = !!values.useExternalBrowser;
|
|
||||||
const callbackType = (stringArg(values, 'callbackType') ||
|
|
||||||
'localhost') as CallbackType;
|
|
||||||
|
|
||||||
// Only show for authorization_code and implicit with external browser enabled
|
|
||||||
if (
|
|
||||||
!['authorization_code', 'implicit'].includes(grantType) ||
|
|
||||||
!useExternalBrowser
|
|
||||||
) {
|
|
||||||
return { hidden: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the redirect URI based on callback type
|
|
||||||
let redirectUri: string;
|
|
||||||
if (callbackType === 'hosted') {
|
|
||||||
redirectUri = HOSTED_CALLBACK_URL;
|
|
||||||
} else {
|
|
||||||
const port = intArg(values, 'callbackPort') || DEFAULT_LOCALHOST_PORT;
|
|
||||||
redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hidden: false,
|
|
||||||
content: `Register \`${redirectUri}\` as a redirect URI with your OAuth provider.`,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@@ -279,8 +182,12 @@ export const plugin: PluginDefinition = {
|
|||||||
optional: true,
|
optional: true,
|
||||||
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
||||||
},
|
},
|
||||||
{ type: 'text', name: 'scope', label: 'Scope', optional: true },
|
{
|
||||||
{ type: 'text', name: 'audience', label: 'Audience', optional: true },
|
type: 'text',
|
||||||
|
name: 'audience',
|
||||||
|
label: 'Audience',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'select',
|
type: 'select',
|
||||||
name: 'tokenName',
|
name: 'tokenName',
|
||||||
@@ -296,54 +203,44 @@ export const plugin: PluginDefinition = {
|
|||||||
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
dynamic: hiddenIfNot(['authorization_code', 'implicit']),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'banner',
|
type: 'checkbox',
|
||||||
inputs: [
|
name: 'usePkce',
|
||||||
{
|
label: 'Use PKCE',
|
||||||
type: 'checkbox',
|
dynamic: hiddenIfNot(['authorization_code']),
|
||||||
name: 'usePkce',
|
|
||||||
label: 'Use PKCE',
|
|
||||||
dynamic: hiddenIfNot(['authorization_code']),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'select',
|
|
||||||
name: 'pkceChallengeMethod',
|
|
||||||
label: 'Code Challenge Method',
|
|
||||||
options: [
|
|
||||||
{ label: 'SHA-256', value: PKCE_SHA256 },
|
|
||||||
{ label: 'Plain', value: PKCE_PLAIN },
|
|
||||||
],
|
|
||||||
defaultValue: DEFAULT_PKCE_METHOD,
|
|
||||||
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
name: 'pkceCodeChallenge',
|
|
||||||
label: 'Code Verifier',
|
|
||||||
placeholder: 'Automatically generated when not set',
|
|
||||||
optional: true,
|
|
||||||
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'h_stack',
|
type: 'select',
|
||||||
inputs: [
|
name: 'pkceChallengeMethod',
|
||||||
{
|
label: 'Code Challenge Method',
|
||||||
type: 'text',
|
options: [
|
||||||
name: 'username',
|
{ label: 'SHA-256', value: PKCE_SHA256 },
|
||||||
label: 'Username',
|
{ label: 'Plain', value: PKCE_PLAIN },
|
||||||
optional: true,
|
|
||||||
dynamic: hiddenIfNot(['password']),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
name: 'password',
|
|
||||||
label: 'Password',
|
|
||||||
password: true,
|
|
||||||
optional: true,
|
|
||||||
dynamic: hiddenIfNot(['password']),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
defaultValue: DEFAULT_PKCE_METHOD,
|
||||||
|
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'pkceCodeChallenge',
|
||||||
|
label: 'Code Verifier',
|
||||||
|
placeholder: 'Automatically generated when not set',
|
||||||
|
optional: true,
|
||||||
|
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'username',
|
||||||
|
label: 'Username',
|
||||||
|
optional: true,
|
||||||
|
dynamic: hiddenIfNot(['password']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'password',
|
||||||
|
label: 'Password',
|
||||||
|
password: true,
|
||||||
|
optional: true,
|
||||||
|
dynamic: hiddenIfNot(['password']),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'select',
|
type: 'select',
|
||||||
@@ -361,6 +258,7 @@ export const plugin: PluginDefinition = {
|
|||||||
type: 'accordion',
|
type: 'accordion',
|
||||||
label: 'Advanced',
|
label: 'Advanced',
|
||||||
inputs: [
|
inputs: [
|
||||||
|
{ type: 'text', name: 'scope', label: 'Scope', optional: true },
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'headerName',
|
name: 'headerName',
|
||||||
@@ -423,16 +321,6 @@ export const plugin: PluginDefinition = {
|
|||||||
const credentialsInBody = values.credentials === 'body';
|
const credentialsInBody = values.credentials === 'body';
|
||||||
const tokenName = values.tokenName === 'id_token' ? 'id_token' : 'access_token';
|
const tokenName = values.tokenName === 'id_token' ? 'id_token' : 'access_token';
|
||||||
|
|
||||||
// Build external browser options if enabled
|
|
||||||
const useExternalBrowser = !!values.useExternalBrowser;
|
|
||||||
const externalBrowserOptions = useExternalBrowser
|
|
||||||
? {
|
|
||||||
useExternalBrowser: true,
|
|
||||||
callbackType: (stringArg(values, 'callbackType') || 'localhost') as CallbackType,
|
|
||||||
callbackPort: intArg(values, 'callbackPort') ?? undefined,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
let token: AccessToken;
|
let token: AccessToken;
|
||||||
if (grantType === 'authorization_code') {
|
if (grantType === 'authorization_code') {
|
||||||
const authorizationUrl = stringArg(values, 'authorizationUrl');
|
const authorizationUrl = stringArg(values, 'authorizationUrl');
|
||||||
@@ -460,7 +348,6 @@ export const plugin: PluginDefinition = {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
tokenName: tokenName,
|
tokenName: tokenName,
|
||||||
externalBrowser: externalBrowserOptions,
|
|
||||||
});
|
});
|
||||||
} else if (grantType === 'implicit') {
|
} else if (grantType === 'implicit') {
|
||||||
const authorizationUrl = stringArg(values, 'authorizationUrl');
|
const authorizationUrl = stringArg(values, 'authorizationUrl');
|
||||||
@@ -475,7 +362,6 @@ export const plugin: PluginDefinition = {
|
|||||||
audience: stringArgOrNull(values, 'audience'),
|
audience: stringArgOrNull(values, 'audience'),
|
||||||
state: stringArgOrNull(values, 'state'),
|
state: stringArgOrNull(values, 'state'),
|
||||||
tokenName: tokenName,
|
tokenName: tokenName,
|
||||||
externalBrowser: externalBrowserOptions,
|
|
||||||
});
|
});
|
||||||
} else if (grantType === 'client_credentials') {
|
} else if (grantType === 'client_credentials') {
|
||||||
const accessTokenUrl = stringArg(values, 'accessTokenUrl');
|
const accessTokenUrl = stringArg(values, 'accessTokenUrl');
|
||||||
@@ -528,10 +414,3 @@ function stringArg(values: Record<string, JsonPrimitive | undefined>, name: stri
|
|||||||
if (!arg) return '';
|
if (!arg) return '';
|
||||||
return arg;
|
return arg;
|
||||||
}
|
}
|
||||||
|
|
||||||
function intArg(values: Record<string, JsonPrimitive | undefined>, name: string): number | null {
|
|
||||||
const arg = values[name];
|
|
||||||
if (arg == null || arg === '') return null;
|
|
||||||
const num = parseInt(`${arg}`, 10);
|
|
||||||
return Number.isNaN(num) ? null : num;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export const synthwave84: Theme = {
|
|||||||
danger: 'hsl(340, 100%, 65%)',
|
danger: 'hsl(340, 100%, 65%)',
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
dialog: {
|
||||||
|
surface: 'hsl(253, 45%, 12%)',
|
||||||
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
surface: 'hsl(253, 42%, 18%)',
|
surface: 'hsl(253, 42%, 18%)',
|
||||||
border: 'hsl(253, 40%, 22%)',
|
border: 'hsl(253, 40%, 22%)',
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { getModel } from '@yaakapp-internal/models';
|
import { getModel } from '@yaakapp-internal/models';
|
||||||
|
import { Icon } from '../components/core/Icon';
|
||||||
|
import { HStack } from '../components/core/Stacks';
|
||||||
import type { FolderSettingsTab } from '../components/FolderSettingsDialog';
|
import type { FolderSettingsTab } from '../components/FolderSettingsDialog';
|
||||||
import { FolderSettingsDialog } from '../components/FolderSettingsDialog';
|
import { FolderSettingsDialog } from '../components/FolderSettingsDialog';
|
||||||
import { showDialog } from '../lib/dialog';
|
import { showDialog } from '../lib/dialog';
|
||||||
|
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||||
|
|
||||||
export function openFolderSettings(folderId: string, tab?: FolderSettingsTab) {
|
export function openFolderSettings(folderId: string, tab?: FolderSettingsTab) {
|
||||||
const folder = getModel('folder', folderId);
|
const folder = getModel('folder', folderId);
|
||||||
if (folder == null) return;
|
|
||||||
showDialog({
|
showDialog({
|
||||||
id: 'folder-settings',
|
id: 'folder-settings',
|
||||||
title: null,
|
title: (
|
||||||
|
<HStack space={2} alignItems="center">
|
||||||
|
<Icon icon="folder_cog" size="xl" color="secondary" />
|
||||||
|
{resolvedModelName(folder)}
|
||||||
|
</HStack>
|
||||||
|
),
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
className: 'h-[50rem]',
|
className: 'h-[50rem]',
|
||||||
noPadding: true,
|
noPadding: true,
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
import { open } from '@tauri-apps/plugin-dialog';
|
|
||||||
import { gitClone } from '@yaakapp-internal/git';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { openWorkspaceFromSyncDir } from '../commands/openWorkspaceFromSyncDir';
|
|
||||||
import { appInfo } from '../lib/appInfo';
|
|
||||||
import { showErrorToast } from '../lib/toast';
|
|
||||||
import { Banner } from './core/Banner';
|
|
||||||
import { Button } from './core/Button';
|
|
||||||
import { Checkbox } from './core/Checkbox';
|
|
||||||
import { IconButton } from './core/IconButton';
|
|
||||||
import { PlainInput } from './core/PlainInput';
|
|
||||||
import { VStack } from './core/Stacks';
|
|
||||||
import { promptCredentials } from './git/credentials';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
hide: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect path separator from an existing path (defaults to /)
|
|
||||||
function getPathSeparator(path: string): string {
|
|
||||||
return path.includes('\\') ? '\\' : '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CloneGitRepositoryDialog({ hide }: Props) {
|
|
||||||
const [url, setUrl] = useState<string>('');
|
|
||||||
const [baseDirectory, setBaseDirectory] = useState<string>(appInfo.defaultProjectDir);
|
|
||||||
const [directoryOverride, setDirectoryOverride] = useState<string | null>(null);
|
|
||||||
const [hasSubdirectory, setHasSubdirectory] = useState(false);
|
|
||||||
const [subdirectory, setSubdirectory] = useState<string>('');
|
|
||||||
const [isCloning, setIsCloning] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const repoName = extractRepoName(url);
|
|
||||||
const sep = getPathSeparator(baseDirectory);
|
|
||||||
const computedDirectory = repoName ? `${baseDirectory}${sep}${repoName}` : baseDirectory;
|
|
||||||
const directory = directoryOverride ?? computedDirectory;
|
|
||||||
const workspaceDirectory =
|
|
||||||
hasSubdirectory && subdirectory ? `${directory}${sep}${subdirectory}` : directory;
|
|
||||||
|
|
||||||
const handleSelectDirectory = async () => {
|
|
||||||
const dir = await open({
|
|
||||||
title: 'Select Directory',
|
|
||||||
directory: true,
|
|
||||||
multiple: false,
|
|
||||||
});
|
|
||||||
if (dir != null) {
|
|
||||||
setBaseDirectory(dir);
|
|
||||||
setDirectoryOverride(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClone = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!url || !directory) return;
|
|
||||||
|
|
||||||
setIsCloning(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await gitClone(url, directory, promptCredentials);
|
|
||||||
|
|
||||||
if (result.type === 'needs_credentials') {
|
|
||||||
setError(
|
|
||||||
result.error ?? 'Authentication failed. Please check your credentials and try again.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the workspace from the cloned directory (or subdirectory)
|
|
||||||
await openWorkspaceFromSyncDir.mutateAsync(workspaceDirectory);
|
|
||||||
|
|
||||||
hide();
|
|
||||||
} catch (err) {
|
|
||||||
setError(String(err));
|
|
||||||
showErrorToast({
|
|
||||||
id: 'git-clone-error',
|
|
||||||
title: 'Clone Failed',
|
|
||||||
message: String(err),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsCloning(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VStack as="form" space={3} alignItems="start" className="pb-3" onSubmit={handleClone}>
|
|
||||||
{error && (
|
|
||||||
<Banner color="danger" className="w-full">
|
|
||||||
{error}
|
|
||||||
</Banner>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<PlainInput
|
|
||||||
required
|
|
||||||
label="Repository URL"
|
|
||||||
placeholder="https://github.com/user/repo.git"
|
|
||||||
defaultValue={url}
|
|
||||||
onChange={setUrl}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PlainInput
|
|
||||||
label="Directory"
|
|
||||||
placeholder={appInfo.defaultProjectDir}
|
|
||||||
defaultValue={directory}
|
|
||||||
onChange={setDirectoryOverride}
|
|
||||||
rightSlot={
|
|
||||||
<IconButton
|
|
||||||
size="xs"
|
|
||||||
className="mr-0.5 !h-auto my-0.5"
|
|
||||||
icon="folder"
|
|
||||||
title="Browse"
|
|
||||||
onClick={handleSelectDirectory}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
checked={hasSubdirectory}
|
|
||||||
onChange={setHasSubdirectory}
|
|
||||||
title="Workspace is in a subdirectory"
|
|
||||||
help="Enable if the Yaak workspace files are not at the root of the repository"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{hasSubdirectory && (
|
|
||||||
<PlainInput
|
|
||||||
label="Subdirectory"
|
|
||||||
placeholder="path/to/workspace"
|
|
||||||
defaultValue={subdirectory}
|
|
||||||
onChange={setSubdirectory}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
color="primary"
|
|
||||||
className="w-full mt-3"
|
|
||||||
disabled={!url || !directory || isCloning}
|
|
||||||
isLoading={isCloning}
|
|
||||||
>
|
|
||||||
{isCloning ? 'Cloning...' : 'Clone Repository'}
|
|
||||||
</Button>
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractRepoName(url: string): string {
|
|
||||||
// Handle various Git URL formats:
|
|
||||||
// https://github.com/user/repo.git
|
|
||||||
// git@github.com:user/repo.git
|
|
||||||
// https://github.com/user/repo
|
|
||||||
const match = url.match(/\/([^/]+?)(\.git)?$/);
|
|
||||||
if (match?.[1]) {
|
|
||||||
return match[1];
|
|
||||||
}
|
|
||||||
// Fallback for SSH-style URLs
|
|
||||||
const sshMatch = url.match(/:([^/]+?)(\.git)?$/);
|
|
||||||
if (sshMatch?.[1]) {
|
|
||||||
return sshMatch[1];
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import type { DnsOverride, Workspace } from '@yaakapp-internal/models';
|
|
||||||
import { patchModel } from '@yaakapp-internal/models';
|
|
||||||
import { useCallback, useId, useMemo } from 'react';
|
|
||||||
import { Button } from './core/Button';
|
|
||||||
import { Checkbox } from './core/Checkbox';
|
|
||||||
import { IconButton } from './core/IconButton';
|
|
||||||
import { PlainInput } from './core/PlainInput';
|
|
||||||
import { HStack, VStack } from './core/Stacks';
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from './core/Table';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
workspace: Workspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DnsOverrideWithId extends DnsOverride {
|
|
||||||
_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DnsOverridesEditor({ workspace }: Props) {
|
|
||||||
const reactId = useId();
|
|
||||||
|
|
||||||
// Ensure each override has an internal ID for React keys
|
|
||||||
const overridesWithIds = useMemo<DnsOverrideWithId[]>(() => {
|
|
||||||
return workspace.settingDnsOverrides.map((override, index) => ({
|
|
||||||
...override,
|
|
||||||
_id: `${reactId}-${index}`,
|
|
||||||
}));
|
|
||||||
}, [workspace.settingDnsOverrides, reactId]);
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
|
||||||
(overrides: DnsOverride[]) => {
|
|
||||||
patchModel(workspace, { settingDnsOverrides: overrides });
|
|
||||||
},
|
|
||||||
[workspace],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAdd = useCallback(() => {
|
|
||||||
const newOverride: DnsOverride = {
|
|
||||||
hostname: '',
|
|
||||||
ipv4: [''],
|
|
||||||
ipv6: [],
|
|
||||||
enabled: true,
|
|
||||||
};
|
|
||||||
handleChange([...workspace.settingDnsOverrides, newOverride]);
|
|
||||||
}, [workspace.settingDnsOverrides, handleChange]);
|
|
||||||
|
|
||||||
const handleUpdate = useCallback(
|
|
||||||
(index: number, update: Partial<DnsOverride>) => {
|
|
||||||
const updated = workspace.settingDnsOverrides.map((o, i) =>
|
|
||||||
i === index ? { ...o, ...update } : o,
|
|
||||||
);
|
|
||||||
handleChange(updated);
|
|
||||||
},
|
|
||||||
[workspace.settingDnsOverrides, handleChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
const updated = workspace.settingDnsOverrides.filter((_, i) => i !== index);
|
|
||||||
handleChange(updated);
|
|
||||||
},
|
|
||||||
[workspace.settingDnsOverrides, handleChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VStack space={3} className="pb-3">
|
|
||||||
<div className="text-text-subtle text-sm">
|
|
||||||
Override DNS resolution for specific hostnames. This works like{' '}
|
|
||||||
<code className="text-text-subtlest bg-surface-highlight px-1 rounded">/etc/hosts</code>{' '}
|
|
||||||
but only for requests made from this workspace.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{overridesWithIds.length > 0 && (
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableHeaderCell className="w-8" />
|
|
||||||
<TableHeaderCell>Hostname</TableHeaderCell>
|
|
||||||
<TableHeaderCell>IPv4 Address</TableHeaderCell>
|
|
||||||
<TableHeaderCell>IPv6 Address</TableHeaderCell>
|
|
||||||
<TableHeaderCell className="w-10" />
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{overridesWithIds.map((override, index) => (
|
|
||||||
<DnsOverrideRow
|
|
||||||
key={override._id}
|
|
||||||
override={override}
|
|
||||||
onUpdate={(update) => handleUpdate(index, update)}
|
|
||||||
onDelete={() => handleDelete(index)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<HStack>
|
|
||||||
<Button size="xs" color="secondary" variant="border" onClick={handleAdd}>
|
|
||||||
Add DNS Override
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DnsOverrideRowProps {
|
|
||||||
override: DnsOverride;
|
|
||||||
onUpdate: (update: Partial<DnsOverride>) => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DnsOverrideRow({ override, onUpdate, onDelete }: DnsOverrideRowProps) {
|
|
||||||
const ipv4Value = override.ipv4.join(', ');
|
|
||||||
const ipv6Value = override.ipv6.join(', ');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<Checkbox
|
|
||||||
hideLabel
|
|
||||||
title={override.enabled ? 'Disable override' : 'Enable override'}
|
|
||||||
checked={override.enabled ?? true}
|
|
||||||
onChange={(enabled) => onUpdate({ enabled })}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<PlainInput
|
|
||||||
size="sm"
|
|
||||||
hideLabel
|
|
||||||
label="Hostname"
|
|
||||||
placeholder="api.example.com"
|
|
||||||
defaultValue={override.hostname}
|
|
||||||
onChange={(hostname) => onUpdate({ hostname })}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<PlainInput
|
|
||||||
size="sm"
|
|
||||||
hideLabel
|
|
||||||
label="IPv4 addresses"
|
|
||||||
placeholder="127.0.0.1"
|
|
||||||
defaultValue={ipv4Value}
|
|
||||||
onChange={(value) =>
|
|
||||||
onUpdate({
|
|
||||||
ipv4: value
|
|
||||||
.split(',')
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<PlainInput
|
|
||||||
size="sm"
|
|
||||||
hideLabel
|
|
||||||
label="IPv6 addresses"
|
|
||||||
placeholder="::1"
|
|
||||||
defaultValue={ipv6Value}
|
|
||||||
onChange={(value) =>
|
|
||||||
onUpdate({
|
|
||||||
ipv6: value
|
|
||||||
.split(',')
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<IconButton
|
|
||||||
size="xs"
|
|
||||||
iconSize="sm"
|
|
||||||
icon="trash"
|
|
||||||
title="Delete override"
|
|
||||||
onClick={onDelete}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -83,7 +83,7 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
|
|||||||
function FormInputsStack<T extends Record<string, JsonPrimitive>>({
|
function FormInputsStack<T extends Record<string, JsonPrimitive>>({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: FormInputsProps<T> & { className?: string }) {
|
}: FormInputsProps<T> & { className?: string}) {
|
||||||
return (
|
return (
|
||||||
<VStack
|
<VStack
|
||||||
space={3}
|
space={3}
|
||||||
@@ -198,9 +198,6 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'accordion':
|
case 'accordion':
|
||||||
if (!hasVisibleInputs(input.inputs)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div key={i + stateKey}>
|
<div key={i + stateKey}>
|
||||||
<DetailsBanner
|
<DetailsBanner
|
||||||
@@ -222,9 +219,6 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'h_stack':
|
case 'h_stack':
|
||||||
if (!hasVisibleInputs(input.inputs)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap sm:flex-nowrap gap-3 items-end" key={i + stateKey}>
|
<div className="flex flex-wrap sm:flex-nowrap gap-3 items-end" key={i + stateKey}>
|
||||||
<FormInputs
|
<FormInputs
|
||||||
@@ -239,9 +233,6 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'banner':
|
case 'banner':
|
||||||
if (!hasVisibleInputs(input.inputs)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Banner
|
<Banner
|
||||||
key={i + stateKey}
|
key={i + stateKey}
|
||||||
@@ -612,8 +603,3 @@ function KeyValueArg({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasVisibleInputs(inputs: FormInput[] | undefined): boolean {
|
|
||||||
if (!inputs) return false;
|
|
||||||
return inputs.some((i) => !i.hidden);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import {
|
import { createWorkspaceModel, foldersAtom, patchModel } from '@yaakapp-internal/models';
|
||||||
createWorkspaceModel,
|
|
||||||
foldersAtom,
|
|
||||||
patchModel,
|
|
||||||
} from '@yaakapp-internal/models';
|
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { Fragment, useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useAuthTab } from '../hooks/useAuthTab';
|
import { useAuthTab } from '../hooks/useAuthTab';
|
||||||
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||||
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
||||||
import { useModelAncestors } from '../hooks/useModelAncestors';
|
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { CountBadge } from './core/CountBadge';
|
import { CountBadge } from './core/CountBadge';
|
||||||
import { Icon } from './core/Icon';
|
|
||||||
import { Input } from './core/Input';
|
import { Input } from './core/Input';
|
||||||
import { Link } from './core/Link';
|
import { Link } from './core/Link';
|
||||||
import { VStack } from './core/Stacks';
|
import { VStack } from './core/Stacks';
|
||||||
@@ -43,8 +37,7 @@ export type FolderSettingsTab =
|
|||||||
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||||
const folders = useAtomValue(foldersAtom);
|
const folders = useAtomValue(foldersAtom);
|
||||||
const folder = folders.find((f) => f.id === folderId) ?? null;
|
const folder = folders.find((f) => f.id === folderId) ?? null;
|
||||||
const ancestors = useModelAncestors(folder);
|
const [activeTab, setActiveTab] = useState<string>(tab ?? TAB_GENERAL);
|
||||||
const breadcrumbs = useMemo(() => ancestors.toReversed(), [ancestors]);
|
|
||||||
const authTab = useAuthTab(TAB_AUTH, folder);
|
const authTab = useAuthTab(TAB_AUTH, folder);
|
||||||
const headersTab = useHeadersTab(TAB_HEADERS, folder);
|
const headersTab = useHeadersTab(TAB_HEADERS, folder);
|
||||||
const inheritedHeaders = useInheritedHeaders(folder);
|
const inheritedHeaders = useInheritedHeaders(folder);
|
||||||
@@ -75,107 +68,77 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
if (folder == null) return null;
|
if (folder == null) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<Tabs
|
||||||
<div className="flex items-center gap-3 px-6 pr-10 mt-4 mb-2 min-w-0 text-xl">
|
value={activeTab}
|
||||||
<Icon icon="folder_cog" size="lg" color="secondary" className="flex-shrink-0" />
|
onChangeValue={setActiveTab}
|
||||||
<div className="flex items-center gap-1.5 font-semibold text-text min-w-0 overflow-hidden flex-1">
|
label="Folder Settings"
|
||||||
{breadcrumbs.map((item, index) => (
|
className="pt-2 pb-2 pl-3 pr-1"
|
||||||
<Fragment key={item.id}>
|
layout="horizontal"
|
||||||
{index > 0 && (
|
addBorders
|
||||||
<Icon
|
tabs={tabs}
|
||||||
icon="chevron_right"
|
>
|
||||||
size="lg"
|
<TabContent value={TAB_AUTH} className="overflow-y-auto h-full px-4">
|
||||||
className="opacity-50 flex-shrink-0"
|
<HttpAuthenticationEditor model={folder} />
|
||||||
/>
|
</TabContent>
|
||||||
)}
|
<TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-4">
|
||||||
<span className="text-text-subtle truncate min-w-0" title={item.name}>
|
<VStack space={3} className="pb-3 h-full">
|
||||||
{item.name}
|
<Input
|
||||||
</span>
|
label="Folder Name"
|
||||||
</Fragment>
|
defaultValue={folder.name}
|
||||||
))}
|
onChange={(name) => patchModel(folder, { name })}
|
||||||
{breadcrumbs.length > 0 && (
|
stateKey={`name.${folder.id}`}
|
||||||
<Icon icon="chevron_right" size="lg" className="opacity-50 flex-shrink-0" />
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
title={folder.name}
|
|
||||||
>
|
|
||||||
{folder.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs
|
|
||||||
defaultValue={tab ?? TAB_GENERAL}
|
|
||||||
label="Folder Settings"
|
|
||||||
className="pt-2 pb-2 pl-3 pr-1 flex-1"
|
|
||||||
layout="horizontal"
|
|
||||||
addBorders
|
|
||||||
tabs={tabs}
|
|
||||||
>
|
|
||||||
<TabContent value={TAB_AUTH} className="overflow-y-auto h-full px-4">
|
|
||||||
<HttpAuthenticationEditor model={folder} />
|
|
||||||
</TabContent>
|
|
||||||
<TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-4">
|
|
||||||
<VStack space={3} className="pb-3 h-full">
|
|
||||||
<Input
|
|
||||||
label="Folder Name"
|
|
||||||
defaultValue={folder.name}
|
|
||||||
onChange={(name) => patchModel(folder, { name })}
|
|
||||||
stateKey={`name.${folder.id}`}
|
|
||||||
/>
|
|
||||||
<MarkdownEditor
|
|
||||||
name="folder-description"
|
|
||||||
placeholder="Folder description"
|
|
||||||
className="border border-border px-2"
|
|
||||||
defaultValue={folder.description}
|
|
||||||
stateKey={`description.${folder.id}`}
|
|
||||||
onChange={(description) => patchModel(folder, { description })}
|
|
||||||
/>
|
|
||||||
</VStack>
|
|
||||||
</TabContent>
|
|
||||||
<TabContent value={TAB_HEADERS} className="overflow-y-auto h-full px-4">
|
|
||||||
<HeadersEditor
|
|
||||||
inheritedHeaders={inheritedHeaders}
|
|
||||||
forceUpdateKey={folder.id}
|
|
||||||
headers={folder.headers}
|
|
||||||
onChange={(headers) => patchModel(folder, { headers })}
|
|
||||||
stateKey={`headers.${folder.id}`}
|
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
<MarkdownEditor
|
||||||
<TabContent value={TAB_VARIABLES} className="overflow-y-auto h-full px-4">
|
name="folder-description"
|
||||||
{folderEnvironment == null ? (
|
placeholder="Folder description"
|
||||||
<EmptyStateText>
|
className="border border-border px-2"
|
||||||
<VStack alignItems="center" space={1.5}>
|
defaultValue={folder.description}
|
||||||
<p>
|
stateKey={`description.${folder.id}`}
|
||||||
Override{' '}
|
onChange={(description) => patchModel(folder, { description })}
|
||||||
<Link href="https://yaak.app/docs/using-yaak/environments-and-variables">
|
/>
|
||||||
Variables
|
</VStack>
|
||||||
</Link>{' '}
|
</TabContent>
|
||||||
for requests within this folder.
|
<TabContent value={TAB_HEADERS} className="overflow-y-auto h-full px-4">
|
||||||
</p>
|
<HeadersEditor
|
||||||
<Button
|
inheritedHeaders={inheritedHeaders}
|
||||||
variant="border"
|
forceUpdateKey={folder.id}
|
||||||
size="sm"
|
headers={folder.headers}
|
||||||
onClick={async () => {
|
onChange={(headers) => patchModel(folder, { headers })}
|
||||||
await createWorkspaceModel({
|
stateKey={`headers.${folder.id}`}
|
||||||
workspaceId: folder.workspaceId,
|
/>
|
||||||
parentModel: 'folder',
|
</TabContent>
|
||||||
parentId: folder.id,
|
<TabContent value={TAB_VARIABLES} className="overflow-y-auto h-full px-4">
|
||||||
model: 'environment',
|
{folderEnvironment == null ? (
|
||||||
name: 'Folder Environment',
|
<EmptyStateText>
|
||||||
});
|
<VStack alignItems="center" space={1.5}>
|
||||||
}}
|
<p>
|
||||||
>
|
Override{' '}
|
||||||
Create Folder Environment
|
<Link href="https://feedback.yaak.app/help/articles/3284139-environments-and-variables">
|
||||||
</Button>
|
Variables
|
||||||
</VStack>
|
</Link>{' '}
|
||||||
</EmptyStateText>
|
for requests within this folder.
|
||||||
) : (
|
</p>
|
||||||
<EnvironmentEditor hideName environment={folderEnvironment} />
|
<Button
|
||||||
)}
|
variant="border"
|
||||||
</TabContent>
|
size="sm"
|
||||||
</Tabs>
|
onClick={async () => {
|
||||||
</div>
|
await createWorkspaceModel({
|
||||||
|
workspaceId: folder.workspaceId,
|
||||||
|
parentModel: 'folder',
|
||||||
|
parentId: folder.id,
|
||||||
|
model: 'environment',
|
||||||
|
name: 'Folder Environment',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create Folder Environment
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</EmptyStateText>
|
||||||
|
) : (
|
||||||
|
<EnvironmentEditor hideName environment={folderEnvironment} />
|
||||||
|
)}
|
||||||
|
</TabContent>
|
||||||
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user