mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-14 04:59:41 +02:00
Compare commits
79 Commits
omnara/pre
...
mcp-client
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c415e7f471 | ||
|
|
8023603ebe | ||
|
|
66942eaf2c | ||
|
|
38796b1833 | ||
|
|
49ffa6fc45 | ||
|
|
1f56ba2eb6 | ||
|
|
f98a70ecb4 | ||
|
|
2984eb40c9 | ||
|
|
cc5d4742f0 | ||
|
|
5b8e4b98a0 | ||
|
|
8637c90a21 | ||
|
|
b88c5e71a0 | ||
|
|
1899d512ab | ||
|
|
7c31718f5e | ||
|
|
8f1463e5d0 | ||
|
|
0dc8807808 | ||
|
|
f24a159b8a | ||
|
|
0b91d3aaff | ||
|
|
431dc1c896 | ||
|
|
bc8277b56b | ||
|
|
0afed185d9 | ||
|
|
55cee00601 | ||
|
|
b41a8e04cb | ||
|
|
eff4519d91 | ||
|
|
c4ce458f79 | ||
|
|
f02ae35634 | ||
|
|
c2f068970b | ||
|
|
eec2d6bc38 | ||
|
|
efa22e470e | ||
|
|
c00d2e981f | ||
|
|
9c45254952 | ||
|
|
d031ff231a | ||
|
|
f056894ddb | ||
|
|
1b0315165f | ||
|
|
bd7e840a57 | ||
|
|
8969748c3c | ||
|
|
4e15ac10a6 | ||
|
|
47a3d44888 | ||
|
|
eb10910d20 | ||
|
|
6ba83d424d | ||
|
|
beb47a6b6a | ||
|
|
1893b8f8dd | ||
|
|
7a5bca7aae | ||
|
|
9a75bc2ae7 | ||
|
|
65514e3882 | ||
|
|
9ddaafb79f | ||
|
|
de47ee19ec | ||
|
|
ea730d0184 | ||
|
|
fe706998d4 | ||
|
|
99209e088f | ||
|
|
3eb29ff2fe | ||
|
|
b759003c83 | ||
|
|
6cba38ac89 | ||
|
|
ba8f85baaf | ||
|
|
9970d5fa6f | ||
|
|
d550b42ca3 | ||
|
|
2e1f0cb53f | ||
|
|
eead422ada | ||
|
|
b5753da3b7 | ||
|
|
ae2f2459e9 | ||
|
|
306e6f358a | ||
|
|
822d52a57e | ||
|
|
e665ce04df | ||
|
|
e4828e1b17 | ||
|
|
42143249a2 | ||
|
|
72a7e6963d | ||
|
|
494e9efb64 | ||
|
|
9fe077f598 | ||
|
|
a6eca1cf2e | ||
|
|
31edd1013f | ||
|
|
28e9657ea5 | ||
|
|
ff084a224a | ||
|
|
bbcae34575 | ||
|
|
2a5587c128 | ||
|
|
c41e173a63 | ||
|
|
2b43407ddf | ||
|
|
4d75b8ef06 | ||
|
|
aa79fb05f9 | ||
|
|
fe01796536 |
@@ -37,3 +37,11 @@ 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>'
|
||||||
|
```
|
||||||
|
|||||||
96
.github/workflows/release.yml
vendored
96
.github/workflows/release.yml
vendored
@@ -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,6 +88,9 @@ 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
|
||||||
@@ -99,6 +102,29 @@ 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 }}
|
||||||
@@ -121,9 +147,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"
|
||||||
|
|||||||
40
Cargo.lock
generated
40
Cargo.lock
generated
@@ -689,9 +689,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.10.1"
|
version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -1316,12 +1316,12 @@ checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.4.0"
|
version = "0.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2136,9 +2136,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "git2"
|
name = "git2"
|
||||||
version = "0.20.2"
|
version = "0.20.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110"
|
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -3036,9 +3036,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libgit2-sys"
|
name = "libgit2-sys"
|
||||||
version = "0.18.1+1.9.0"
|
version = "0.18.3+1.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e"
|
checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -3446,9 +3446,9 @@ checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
@@ -6341,9 +6341,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.41"
|
version = "0.3.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
@@ -6351,22 +6351,22 @@ dependencies = [
|
|||||||
"num-conv",
|
"num-conv",
|
||||||
"num_threads",
|
"num_threads",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
"serde_core",
|
||||||
"time-core",
|
"time-core",
|
||||||
"time-macros",
|
"time-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-core"
|
name = "time-core"
|
||||||
version = "0.1.4"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.22"
|
version = "0.2.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"time-core",
|
"time-core",
|
||||||
@@ -8075,6 +8075,7 @@ name = "yaak-common"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8121,8 +8122,10 @@ 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",
|
||||||
]
|
]
|
||||||
@@ -8149,6 +8152,7 @@ dependencies = [
|
|||||||
"tonic",
|
"tonic",
|
||||||
"tonic-reflection",
|
"tonic-reflection",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"yaak-common",
|
||||||
"yaak-tls",
|
"yaak-tls",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
27
MCP_CLIENT_PLAN.md
Normal file
27
MCP_CLIENT_PLAN.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# MCP Client Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Add an MCP client mode to Yaak so users can connect to and debug MCP servers.
|
||||||
|
|
||||||
|
## Core Design
|
||||||
|
- **Protocol layer:** Implement JSON‑RPC framing, message IDs, and notifications as the common core.
|
||||||
|
- **Transport interface:** Define an async trait with `connect`, `send`, `receive`, and `close` methods.
|
||||||
|
- **Transports:**
|
||||||
|
- Start with **Standard I/O** for local development.
|
||||||
|
- Reuse the existing HTTP stack for **HTTP streaming** next.
|
||||||
|
- Leave hooks for **WebSocket** support later.
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
- Register MCP as a new request type alongside REST, GraphQL, gRPC, and WebSocket.
|
||||||
|
- Allow per‑request transport selection (stdio or HTTP).
|
||||||
|
- Map inbound messages into a new MCP response model that feeds existing timeline and debug views.
|
||||||
|
|
||||||
|
## Testing and Dog‑fooding
|
||||||
|
- Convert Yaak's own MCP server to Standard I/O for local testing.
|
||||||
|
- Use it internally to validate protocol behavior and message flow.
|
||||||
|
- Add unit and integration tests for JSON‑RPC messaging and transport abstractions.
|
||||||
|
|
||||||
|
## Future Refinements
|
||||||
|
- Add WebSocket transport support once core paths are stable.
|
||||||
|
- Extend timelines for protocol‑level visualization layered over raw transport events.
|
||||||
|
- Implement version and capability negotiation between client and server.
|
||||||
@@ -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/src-tauri/icons/icon.png">
|
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/crates-tauri/yaak-app/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> <!-- 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> <a href="https://github.com/flashblaze"><img src="https://github.com/flashblaze.png" width="50px" alt="User avatar: flashblaze" /></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://feedback.yaak.app/help)
|
- [Documentation](https://yaak.app/docs)
|
||||||
- [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)
|
||||||
|
|||||||
198
crates-cli/yaak-cli/PLAN.md
Normal file
198
crates-cli/yaak-cli/PLAN.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# CLI Command Architecture Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Redesign the yaak-cli command structure to use a resource-oriented `<resource> <action>`
|
||||||
|
pattern that scales well, is discoverable, and supports both human and LLM workflows.
|
||||||
|
|
||||||
|
## Command Architecture
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
|
||||||
|
- **Resource-oriented**: top-level commands are nouns, subcommands are verbs
|
||||||
|
- **Polymorphic requests**: `request` covers HTTP, gRPC, and WebSocket — the CLI
|
||||||
|
resolves the type via `get_any_request` and adapts behavior accordingly
|
||||||
|
- **Simple creation, full-fidelity via JSON**: human-friendly flags for basic creation,
|
||||||
|
`--json` for full control (targeted at LLM and scripting workflows)
|
||||||
|
- **Runtime schema introspection**: `request schema` outputs JSON Schema for the request
|
||||||
|
models, with dynamic auth fields populated from loaded plugins at runtime
|
||||||
|
- **Destructive actions require confirmation**: `delete` commands prompt for user
|
||||||
|
confirmation before proceeding. Can be bypassed with `--yes` / `-y` for scripting
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
# Top-level shortcut
|
||||||
|
yaakcli send <id> [-e <env_id>] # id can be a request, folder, or workspace
|
||||||
|
|
||||||
|
# Resource commands
|
||||||
|
yaakcli workspace list
|
||||||
|
yaakcli workspace show <id>
|
||||||
|
yaakcli workspace create --name <name>
|
||||||
|
yaakcli workspace create --json '{"name": "My Workspace"}'
|
||||||
|
yaakcli workspace create '{"name": "My Workspace"}' # positional JSON shorthand
|
||||||
|
yaakcli workspace update --json '{"id": "wk_abc", "name": "New Name"}'
|
||||||
|
yaakcli workspace delete <id>
|
||||||
|
|
||||||
|
yaakcli request list <workspace_id>
|
||||||
|
yaakcli request show <id>
|
||||||
|
yaakcli request create <workspace_id> --name <name> --url <url> [--method GET]
|
||||||
|
yaakcli request create --json '{"workspaceId": "wk_abc", "url": "..."}'
|
||||||
|
yaakcli request update --json '{"id": "rq_abc", "url": "https://new.com"}'
|
||||||
|
yaakcli request send <id> [-e <env_id>]
|
||||||
|
yaakcli request delete <id>
|
||||||
|
yaakcli request schema <http|grpc|websocket>
|
||||||
|
|
||||||
|
yaakcli folder list <workspace_id>
|
||||||
|
yaakcli folder show <id>
|
||||||
|
yaakcli folder create <workspace_id> --name <name>
|
||||||
|
yaakcli folder create --json '{"workspaceId": "wk_abc", "name": "Auth"}'
|
||||||
|
yaakcli folder update --json '{"id": "fl_abc", "name": "New Name"}'
|
||||||
|
yaakcli folder delete <id>
|
||||||
|
|
||||||
|
yaakcli environment list <workspace_id>
|
||||||
|
yaakcli environment show <id>
|
||||||
|
yaakcli environment create <workspace_id> --name <name>
|
||||||
|
yaakcli environment create --json '{"workspaceId": "wk_abc", "name": "Production"}'
|
||||||
|
yaakcli environment update --json '{"id": "ev_abc", ...}'
|
||||||
|
yaakcli environment delete <id>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### `send` — Top-Level Shortcut
|
||||||
|
|
||||||
|
`yaakcli send <id>` is a convenience alias that accepts any sendable ID. It tries
|
||||||
|
each type in order via DB lookups (short-circuiting on first match):
|
||||||
|
|
||||||
|
1. Request (HTTP, gRPC, or WebSocket via `get_any_request`)
|
||||||
|
2. Folder (sends all requests in the folder)
|
||||||
|
3. Workspace (sends all requests in the workspace)
|
||||||
|
|
||||||
|
ID prefixes exist (e.g. `rq_`, `fl_`, `wk_`) but are not relied upon — resolution
|
||||||
|
is purely by DB lookup.
|
||||||
|
|
||||||
|
`request send <id>` is the same but restricted to request IDs only.
|
||||||
|
|
||||||
|
### Request Send — Polymorphic Behavior
|
||||||
|
|
||||||
|
`send` means "execute this request" regardless of protocol:
|
||||||
|
|
||||||
|
- **HTTP**: send request, print response, exit
|
||||||
|
- **gRPC**: invoke the method; for streaming, stream output to stdout until done/Ctrl+C
|
||||||
|
- **WebSocket**: connect, stream messages to stdout until closed/Ctrl+C
|
||||||
|
|
||||||
|
### `request schema` — Runtime JSON Schema
|
||||||
|
|
||||||
|
Outputs a JSON Schema describing the full request shape, including dynamic fields:
|
||||||
|
|
||||||
|
1. Generate base schema from `schemars::JsonSchema` derive on the Rust model structs
|
||||||
|
2. Load plugins, collect auth strategy definitions and their form inputs
|
||||||
|
3. Merge plugin-defined auth fields into the `authentication` property as a `oneOf`
|
||||||
|
4. Output the combined schema as JSON
|
||||||
|
|
||||||
|
This lets an LLM call `schema`, read the shape, and construct valid JSON for
|
||||||
|
`create --json` or `update --json`.
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### Phase 1: Restructure commands (no new functionality)
|
||||||
|
|
||||||
|
Refactor `main.rs` into the new resource/action pattern using clap subcommand nesting.
|
||||||
|
Existing behavior stays the same, just reorganized. Remove the `get` command.
|
||||||
|
|
||||||
|
1. Create module structure: `commands/workspace.rs`, `commands/request.rs`, etc.
|
||||||
|
2. Define nested clap enums:
|
||||||
|
```rust
|
||||||
|
enum Commands {
|
||||||
|
Send(SendArgs),
|
||||||
|
Workspace(WorkspaceArgs),
|
||||||
|
Request(RequestArgs),
|
||||||
|
Folder(FolderArgs),
|
||||||
|
Environment(EnvironmentArgs),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. Move existing `Workspaces` logic into `workspace list`
|
||||||
|
4. Move existing `Requests` logic into `request list`
|
||||||
|
5. Move existing `Send` logic into `request send`
|
||||||
|
6. Move existing `Create` logic into `request create`
|
||||||
|
7. Delete the `Get` command entirely
|
||||||
|
8. Extract shared setup (DB init, plugin init, encryption) into a reusable context struct
|
||||||
|
|
||||||
|
### Phase 2: Add missing CRUD commands
|
||||||
|
|
||||||
|
1. `workspace show <id>`
|
||||||
|
2. `workspace create --name <name>` (and `--json`)
|
||||||
|
3. `workspace update --json`
|
||||||
|
4. `workspace delete <id>`
|
||||||
|
5. `request show <id>` (JSON output of the full request model)
|
||||||
|
6. `request delete <id>`
|
||||||
|
7. `folder list <workspace_id>`
|
||||||
|
8. `folder show <id>`
|
||||||
|
9. `folder create <workspace_id> --name <name>` (and `--json`)
|
||||||
|
10. `folder update --json`
|
||||||
|
11. `folder delete <id>`
|
||||||
|
12. `environment list <workspace_id>`
|
||||||
|
13. `environment show <id>`
|
||||||
|
14. `environment create <workspace_id> --name <name>` (and `--json`)
|
||||||
|
15. `environment update --json`
|
||||||
|
16. `environment delete <id>`
|
||||||
|
|
||||||
|
### Phase 3: JSON input for create/update
|
||||||
|
|
||||||
|
Both commands accept JSON via `--json <string>` or as a positional argument (detected
|
||||||
|
by leading `{`). They follow the same upsert pattern as the plugin API.
|
||||||
|
|
||||||
|
- **`create --json`**: JSON must include `workspaceId`. Must NOT include `id` (or
|
||||||
|
use empty string `""`). Deserializes into the model with defaults for missing fields,
|
||||||
|
then upserts (insert).
|
||||||
|
- **`update --json`**: JSON must include `id`. Performs a fetch-merge-upsert:
|
||||||
|
1. Fetch the existing model from DB
|
||||||
|
2. Serialize it to `serde_json::Value`
|
||||||
|
3. Deep-merge the user's partial JSON on top (JSON Merge Patch / RFC 7386 semantics)
|
||||||
|
4. Deserialize back into the typed model
|
||||||
|
5. Upsert (update)
|
||||||
|
|
||||||
|
This matches how the MCP server plugin already does it (fetch existing, spread, override),
|
||||||
|
but the CLI handles the merge server-side so callers don't have to.
|
||||||
|
|
||||||
|
Setting a field to `null` removes it (for `Option<T>` fields), per RFC 7386.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
1. Add `--json` flag and positional JSON detection to `create` commands
|
||||||
|
2. Add `update` commands with required `--json` flag
|
||||||
|
3. Implement JSON merge utility (or use `json-patch` crate)
|
||||||
|
|
||||||
|
### Phase 4: Runtime schema generation
|
||||||
|
|
||||||
|
1. Add `schemars` dependency to `yaak-models`
|
||||||
|
2. Derive `JsonSchema` on `HttpRequest`, `GrpcRequest`, `WebsocketRequest`, and their
|
||||||
|
nested types (`HttpRequestHeader`, `HttpUrlParameter`, etc.)
|
||||||
|
3. Implement `request schema` command:
|
||||||
|
- Generate base schema from schemars
|
||||||
|
- Query plugins for auth strategy form inputs
|
||||||
|
- Convert plugin form inputs into JSON Schema properties
|
||||||
|
- Merge into the `authentication` field
|
||||||
|
- Print to stdout
|
||||||
|
|
||||||
|
### Phase 5: Polymorphic send
|
||||||
|
|
||||||
|
1. Update `request send` to use `get_any_request` to resolve the request type
|
||||||
|
2. Match on `AnyRequest` variant and dispatch to the appropriate sender:
|
||||||
|
- `AnyRequest::HttpRequest` — existing HTTP send logic
|
||||||
|
- `AnyRequest::GrpcRequest` — gRPC invoke (future implementation)
|
||||||
|
- `AnyRequest::WebsocketRequest` — WebSocket connect (future implementation)
|
||||||
|
3. gRPC and WebSocket send can initially return "not yet implemented" errors
|
||||||
|
|
||||||
|
### Phase 6: Top-level `send` and folder/workspace send
|
||||||
|
|
||||||
|
1. Add top-level `yaakcli send <id>` command
|
||||||
|
2. Resolve ID by trying DB lookups in order: any_request → folder → workspace
|
||||||
|
3. For folder: list all requests in folder, send each
|
||||||
|
4. For workspace: list all requests in workspace, send each
|
||||||
|
5. Add execution options: `--sequential` (default), `--parallel`, `--fail-fast`
|
||||||
|
|
||||||
|
## Crate Changes
|
||||||
|
|
||||||
|
- **yaak-cli**: restructure into modules, new clap hierarchy
|
||||||
|
- **yaak-models**: add `schemars` dependency, derive `JsonSchema` on model structs
|
||||||
|
(current derives: `Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS`)
|
||||||
@@ -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::{parse_and_render, render_json_value_raw, RenderOptions};
|
use yaak_templates::{RenderOptions, parse_and_render, render_json_value_raw};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "yaakcli")]
|
#[command(name = "yaakcli")]
|
||||||
@@ -149,14 +149,7 @@ 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 {
|
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..r.to_owned() })
|
||||||
url,
|
|
||||||
url_parameters,
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
authentication,
|
|
||||||
..r.to_owned()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -169,16 +162,10 @@ 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) {
|
let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" };
|
||||||
"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()
|
dirs::data_dir().expect("Could not determine data directory").join(app_id)
|
||||||
.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");
|
||||||
@@ -191,9 +178,7 @@ 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(
|
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
||||||
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");
|
||||||
@@ -203,9 +188,8 @@ 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 = std::env::var("YAAK_PLUGIN_RUNTIME")
|
let plugin_runtime_main =
|
||||||
.map(PathBuf::from)
|
std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| {
|
||||||
.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")
|
||||||
@@ -226,14 +210,10 @@ 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 = plugin_manager
|
let errors =
|
||||||
.initialize_all_plugins(plugins, &PluginContext::new_empty())
|
plugin_manager.initialize_all_plugins(plugins, &PluginContext::new_empty()).await;
|
||||||
.await;
|
|
||||||
for (plugin_dir, error_msg) in errors {
|
for (plugin_dir, error_msg) in errors {
|
||||||
eprintln!(
|
eprintln!("Warning: Failed to initialize plugin '{}': {}", plugin_dir, error_msg);
|
||||||
"Warning: Failed to initialize plugin '{}': {}",
|
|
||||||
plugin_dir, error_msg
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,9 +229,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Requests { workspace_id } => {
|
Commands::Requests { workspace_id } => {
|
||||||
let requests = db
|
let requests = db.list_http_requests(&workspace_id).expect("Failed to list requests");
|
||||||
.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 {
|
||||||
@@ -261,9 +239,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Send { request_id } => {
|
Commands::Send { request_id } => {
|
||||||
let request = db
|
let request = db.get_http_request(&request_id).expect("Failed to get request");
|
||||||
.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
|
||||||
@@ -318,18 +294,13 @@ async fn main() {
|
|||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
// Drain events silently
|
// Drain events silently
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
||||||
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
|
let response = sender.send(sendable, event_tx).await.expect("Failed to send request");
|
||||||
.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 {
|
||||||
@@ -383,18 +354,13 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
||||||
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
|
let response = sender.send(sendable, event_tx).await.expect("Failed to send request");
|
||||||
.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;
|
||||||
@@ -421,12 +387,7 @@ 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 {
|
Commands::Create { workspace_id, name, method, url } => {
|
||||||
workspace_id,
|
|
||||||
name,
|
|
||||||
method,
|
|
||||||
url,
|
|
||||||
} => {
|
|
||||||
let request = HttpRequest {
|
let request = HttpRequest {
|
||||||
workspace_id,
|
workspace_id,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -2,14 +2,6 @@
|
|||||||
<!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/>-->
|
||||||
|
|||||||
13
crates-tauri/yaak-app/macos/entitlements.yaaknode.plist
Normal file
13
crates-tauri/yaak-app/macos/entitlements.yaaknode.plist
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?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,9 +1,10 @@
|
|||||||
use crate::error::Result;
|
|
||||||
use crate::PluginContextExt;
|
use crate::PluginContextExt;
|
||||||
|
use crate::error::Result;
|
||||||
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::{
|
||||||
@@ -21,20 +22,6 @@ 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>,
|
||||||
@@ -54,7 +41,12 @@ 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(plugin_manager, encryption_manager, &plugin_context, template)?)
|
Ok(encrypt_secure_template_function(
|
||||||
|
plugin_manager,
|
||||||
|
encryption_manager,
|
||||||
|
&plugin_context,
|
||||||
|
template,
|
||||||
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -92,3 +84,17 @@ 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,33 +6,47 @@ 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::{
|
||||||
GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult,
|
BranchDeleteResult, CloneResult, GitCommit, GitRemote, GitStatusSummary, PullResult,
|
||||||
git_add, git_add_credential, git_add_remote, git_checkout_branch, git_commit,
|
PushResult, git_add, git_add_credential, git_add_remote, git_checkout_branch, git_clone,
|
||||||
git_create_branch, git_delete_branch, git_fetch_all, git_init, git_log,
|
git_commit, git_create_branch, git_delete_branch, git_delete_remote_branch, git_fetch_all,
|
||||||
git_merge_branch, git_pull, git_push, git_remotes, git_rm_remote, git_status,
|
git_init, git_log, git_merge_branch, git_pull, git_pull_force_reset, git_pull_merge, git_push,
|
||||||
git_unstage,
|
git_remotes, git_rename_branch, git_reset_changes, git_rm_remote, git_status, 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)?)
|
Ok(git_checkout_branch(dir, branch, force).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_git_branch(dir: &Path, branch: &str) -> Result<()> {
|
pub async fn cmd_git_branch(dir: &Path, branch: &str, base: Option<&str>) -> Result<()> {
|
||||||
Ok(git_create_branch(dir, branch)?)
|
Ok(git_create_branch(dir, branch, base).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_git_delete_branch(dir: &Path, branch: &str) -> Result<()> {
|
pub async fn cmd_git_delete_branch(
|
||||||
Ok(git_delete_branch(dir, branch)?)
|
dir: &Path,
|
||||||
|
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_merge_branch(dir: &Path, branch: &str, force: bool) -> Result<()> {
|
pub async fn cmd_git_delete_remote_branch(dir: &Path, branch: &str) -> Result<()> {
|
||||||
Ok(git_merge_branch(dir, branch, force)?)
|
Ok(git_delete_remote_branch(dir, branch).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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]
|
||||||
@@ -50,24 +64,43 @@ 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)?)
|
Ok(git_commit(dir, message).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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)?)
|
Ok(git_fetch_all(dir).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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)?)
|
Ok(git_push(dir).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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)?)
|
Ok(git_pull(dir).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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]
|
||||||
@@ -86,14 +119,18 @@ 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(dir, remote_url, username, password).await?)
|
Ok(git_add_credential(remote_url, username, password).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crate::error::Result;
|
|
||||||
use crate::PluginContextExt;
|
use crate::PluginContextExt;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
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,9 +1,13 @@
|
|||||||
|
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};
|
||||||
@@ -15,22 +19,19 @@ use yaak_http::client::{
|
|||||||
HttpConnectionOptions, HttpConnectionProxySetting, HttpConnectionProxySettingAuth,
|
HttpConnectionOptions, HttpConnectionProxySetting, HttpConnectionProxySettingAuth,
|
||||||
};
|
};
|
||||||
use yaak_http::cookies::CookieStore;
|
use yaak_http::cookies::CookieStore;
|
||||||
use yaak_http::manager::HttpConnectionManager;
|
use yaak_http::manager::{CachedClient, 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,
|
||||||
};
|
};
|
||||||
@@ -173,19 +174,28 @@ 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(plugin_manager.clone(), encryption_manager.clone(), &plugin_context, RenderPurpose::Send);
|
let cb = PluginTemplateCallback::new(
|
||||||
|
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 request = render_http_request(&resolved, env_chain, &cb, &RenderOptions::throw()).await?;
|
let mut cancel_rx = cancelled_rx.clone();
|
||||||
|
let render_options = RenderOptions::throw();
|
||||||
// Resolve inherited settings for this request
|
let request = tokio::select! {
|
||||||
let resolved_settings = window.db().resolve_settings_for_http_request(&resolved)?;
|
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 {
|
||||||
follow_redirects: resolved_settings.follow_redirects,
|
follow_redirects: workspace.setting_follow_redirects,
|
||||||
timeout: if resolved_settings.request_timeout > 0 {
|
timeout: if workspace.setting_request_timeout > 0 {
|
||||||
Some(Duration::from_millis(resolved_settings.request_timeout.unsigned_abs() as u64))
|
Some(Duration::from_millis(workspace.setting_request_timeout.unsigned_abs() as u64))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
@@ -231,29 +241,36 @@ async fn send_http_request_inner<R: Runtime>(
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = connection_manager
|
let cached_client = connection_manager
|
||||||
.get_client(&HttpConnectionOptions {
|
.get_client(&HttpConnectionOptions {
|
||||||
id: plugin_context.id.clone(),
|
id: plugin_context.id.clone(),
|
||||||
validate_certificates: resolved_settings.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
|
// Apply authentication to the request, racing against cancellation since
|
||||||
apply_authentication(
|
// auth plugins (e.g. OAuth2) can block indefinitely waiting for user action.
|
||||||
&window,
|
let mut cancel_rx = cancelled_rx.clone();
|
||||||
&mut sendable_request,
|
tokio::select! {
|
||||||
&request,
|
result = apply_authentication(
|
||||||
auth_context_id,
|
&window,
|
||||||
&plugin_manager,
|
&mut sendable_request,
|
||||||
plugin_context,
|
&request,
|
||||||
)
|
auth_context_id,
|
||||||
.await?;
|
&plugin_manager,
|
||||||
|
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(
|
||||||
client,
|
cached_client,
|
||||||
sendable_request,
|
sendable_request,
|
||||||
response_ctx,
|
response_ctx,
|
||||||
cancelled_rx.clone(),
|
cancelled_rx.clone(),
|
||||||
@@ -313,7 +330,7 @@ pub fn resolve_http_request<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_transaction<R: Runtime>(
|
async fn execute_transaction<R: Runtime>(
|
||||||
client: reqwest::Client,
|
cached_client: CachedClient,
|
||||||
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>,
|
||||||
@@ -324,7 +341,10 @@ 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();
|
||||||
|
|
||||||
let sender = ReqwestSender::with_client(client);
|
// Keep a reference to the resolver for DNS timing events
|
||||||
|
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),
|
||||||
@@ -349,21 +369,39 @@ 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
|
// For ephemeral responses, just drain the events but still capture DNS timing
|
||||||
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
let dns_elapsed = dns_elapsed.clone();
|
||||||
|
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)
|
||||||
@@ -531,10 +569,14 @@ 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_template};
|
use crate::render::{render_grpc_request, render_json_value, 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, GrpcRequest, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState,
|
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Plugin,
|
||||||
Plugin, Workspace, WorkspaceMeta,
|
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,6 +101,7 @@ 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,
|
||||||
}
|
}
|
||||||
@@ -111,6 +112,7 @@ 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(),
|
||||||
@@ -118,6 +120,7 @@ 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"),
|
||||||
})
|
})
|
||||||
@@ -189,7 +192,6 @@ 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>>,
|
||||||
@@ -224,18 +226,21 @@ 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();
|
||||||
|
|
||||||
Ok(grpc_handle
|
// Always invalidate cached pool when this command is called, to force re-reflection
|
||||||
.lock()
|
let mut handle = grpc_handle.lock().await;
|
||||||
.await
|
handle.invalidate_pool(&req.id, &uri, &proto_files);
|
||||||
|
|
||||||
|
Ok(handle
|
||||||
.services(
|
.services(
|
||||||
&req.id,
|
&req.id,
|
||||||
&uri,
|
&uri,
|
||||||
&proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(),
|
&proto_files,
|
||||||
&metadata,
|
&metadata,
|
||||||
workspace.setting_validate_certificates.unwrap_or(true),
|
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()))?)
|
||||||
@@ -327,7 +332,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
uri.as_str(),
|
uri.as_str(),
|
||||||
&proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(),
|
&proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(),
|
||||||
&metadata,
|
&metadata,
|
||||||
workspace.setting_validate_certificates.unwrap_or(true),
|
workspace.setting_validate_certificates,
|
||||||
client_cert.clone(),
|
client_cert.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -360,10 +365,8 @@ 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();
|
||||||
|
|
||||||
@@ -385,14 +388,12 @@ 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 {
|
||||||
render_template(
|
let result = render_template(
|
||||||
msg.as_str(),
|
msg.as_str(),
|
||||||
environment_chain,
|
environment_chain,
|
||||||
&PluginTemplateCallback::new(
|
&PluginTemplateCallback::new(
|
||||||
@@ -406,24 +407,11 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
),
|
),
|
||||||
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
|
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
.expect("Failed to render template")
|
result.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();
|
||||||
@@ -470,12 +458,48 @@ 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(&service, &method, in_msg_stream, &metadata, client_cert)
|
.streaming(
|
||||||
|
&service,
|
||||||
|
&method,
|
||||||
|
in_msg_stream,
|
||||||
|
&metadata,
|
||||||
|
client_cert,
|
||||||
|
on_message.clone(),
|
||||||
|
)
|
||||||
.await,
|
.await,
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
@@ -490,6 +514,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
in_msg_stream,
|
in_msg_stream,
|
||||||
&metadata,
|
&metadata,
|
||||||
client_cert,
|
client_cert,
|
||||||
|
on_message.clone(),
|
||||||
)
|
)
|
||||||
.await,
|
.await,
|
||||||
),
|
),
|
||||||
@@ -1035,14 +1060,54 @@ 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(&window.plugin_context(), auth_name, values, model.id())
|
.get_http_authentication_config(
|
||||||
|
&window.plugin_context(),
|
||||||
|
auth_name,
|
||||||
|
rendered_values,
|
||||||
|
model.id(),
|
||||||
|
)
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1089,19 +1154,54 @@ 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,
|
||||||
values,
|
rendered_values,
|
||||||
&model.id(),
|
&model.id(),
|
||||||
)
|
)
|
||||||
.await?)
|
.await?)
|
||||||
@@ -1171,35 +1271,6 @@ 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>,
|
||||||
@@ -1296,27 +1367,6 @@ 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>,
|
||||||
@@ -1579,7 +1629,6 @@ 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,
|
||||||
@@ -1613,7 +1662,6 @@ 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,
|
||||||
@@ -1621,12 +1669,13 @@ 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,
|
||||||
@@ -1649,30 +1698,36 @@ 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,5 +1,6 @@
|
|||||||
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;
|
||||||
@@ -8,9 +9,8 @@ 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_tauri_utils::api_client::yaak_api_client;
|
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
use yaak_tauri_utils::api_client::yaak_api_client;
|
||||||
|
|
||||||
// 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,5 +1,7 @@
|
|||||||
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::{
|
||||||
@@ -10,15 +12,12 @@ use chrono::Utc;
|
|||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
use log::error;
|
use log::error;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Emitter, Manager, Runtime};
|
use tauri::{AppHandle, Emitter, Listener, 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::{
|
||||||
@@ -32,6 +31,7 @@ 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,6 +57,58 @@ 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)?;
|
||||||
|
if event.reply_id.is_some() {
|
||||||
|
// Follow-up update from plugin runtime with resolved inputs — forward to frontend
|
||||||
|
window.emit_to(window.label(), "plugin_event", event.clone())?;
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
// Initial request — set up bidirectional communication
|
||||||
|
window.emit_to(window.label(), "plugin_event", event.clone()).unwrap();
|
||||||
|
|
||||||
|
let event_id = event.id.clone();
|
||||||
|
let plugin_handle = plugin_handle.clone();
|
||||||
|
let plugin_context = plugin_context.clone();
|
||||||
|
let window = window.clone();
|
||||||
|
|
||||||
|
// Spawn async task to handle bidirectional form communication
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let (tx, mut rx) = tokio::sync::mpsc::channel::<InternalEvent>(128);
|
||||||
|
|
||||||
|
// Listen for replies from the frontend
|
||||||
|
let listener_id = window.listen(event_id, move |ev: tauri::Event| {
|
||||||
|
let resp: InternalEvent = serde_json::from_str(ev.payload()).unwrap();
|
||||||
|
let _ = tx.try_send(resp);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Forward each reply to the plugin runtime
|
||||||
|
while let Some(resp) = rx.recv().await {
|
||||||
|
let is_done = matches!(
|
||||||
|
&resp.payload,
|
||||||
|
InternalEventPayload::PromptFormResponse(r) if r.done.unwrap_or(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
let event_to_send = plugin_handle.build_event_to_send(
|
||||||
|
&plugin_context,
|
||||||
|
&resp.payload,
|
||||||
|
Some(resp.reply_id.unwrap_or_default()),
|
||||||
|
);
|
||||||
|
if let Err(e) = plugin_handle.send(&event_to_send).await {
|
||||||
|
log::warn!("Failed to forward form response to plugin: {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_done {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.unlisten(listener_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
InternalEventPayload::FindHttpResponsesRequest(req) => {
|
InternalEventPayload::FindHttpResponsesRequest(req) => {
|
||||||
let http_responses = app_handle
|
let http_responses = app_handle
|
||||||
.db()
|
.db()
|
||||||
@@ -166,7 +218,12 @@ 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(plugin_manager, encryption_manager, &plugin_context, req.purpose);
|
let cb = PluginTemplateCallback::new(
|
||||||
|
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?;
|
||||||
@@ -187,7 +244,12 @@ 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(plugin_manager, encryption_manager, &plugin_context, req.purpose);
|
let cb = PluginTemplateCallback::new(
|
||||||
|
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?;
|
||||||
@@ -218,7 +280,12 @@ 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(plugin_manager, encryption_manager, &plugin_context, req.purpose);
|
let cb = PluginTemplateCallback::new(
|
||||||
|
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,
|
||||||
generate_handler, is_dev,
|
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(crate) async fn cmd_plugins_search<R: Runtime>(
|
pub 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(crate) async fn cmd_plugins_search<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub(crate) async fn cmd_plugins_install<R: Runtime>(
|
pub 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(crate) async fn cmd_plugins_install<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub(crate) async fn cmd_plugins_uninstall<R: Runtime>(
|
pub 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(crate) async fn cmd_plugins_uninstall<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub(crate) async fn cmd_plugins_updates<R: Runtime>(
|
pub 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(crate) async fn cmd_plugins_updates<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub(crate) async fn cmd_plugins_update_all<R: Runtime>(
|
pub 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,13 +233,6 @@ pub(crate) 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,6 +3,7 @@ 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};
|
||||||
@@ -11,7 +12,6 @@ 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,7 +55,8 @@ 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,4 +1,5 @@
|
|||||||
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;
|
||||||
@@ -8,7 +9,6 @@ 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,11 +162,16 @@ 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_record" => {
|
"dev.reset_size_16x9" => {
|
||||||
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,8 +154,13 @@ 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_record".to_string(),
|
"dev.reset_size_16x9".to_string(),
|
||||||
"Reset Size 16x9",
|
"Resize to 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,53 +28,6 @@ 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,
|
||||||
@@ -87,30 +40,6 @@ 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,
|
||||||
@@ -296,8 +225,10 @@ 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 (http::HeaderName::from_str(&header.name), HeaderValue::from_str(&header.value))
|
match (
|
||||||
{
|
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);
|
||||||
}
|
}
|
||||||
@@ -355,7 +286,7 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
|||||||
url.as_str(),
|
url.as_str(),
|
||||||
headers,
|
headers,
|
||||||
receive_tx,
|
receive_tx,
|
||||||
workspace.setting_validate_certificates.unwrap_or(true),
|
workspace.setting_validate_certificates,
|
||||||
client_cert,
|
client_cert,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -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,3 +6,4 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["process"] }
|
||||||
|
|||||||
16
crates/yaak-common/src/command.rs
Normal file
16
crates/yaak-common/src/command.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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,2 +1,3 @@
|
|||||||
|
pub mod command;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub mod serde;
|
pub mod serde;
|
||||||
|
|||||||
@@ -11,3 +11,7 @@ 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,6 +115,35 @@ 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();
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { workspace = true, features = ["serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
git2 = { version = "0.20.0", features = ["vendored-libgit2", "vendored-openssl"] }
|
git2 = { version = "0.20.4", features = ["vendored-libgit2", "vendored-openssl"] }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
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 }
|
||||||
|
|||||||
8
crates/yaak-git/bindings/gen_git.ts
generated
8
crates/yaak-git/bindings/gen_git.ts
generated
@@ -1,6 +1,10 @@
|
|||||||
// 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, };
|
||||||
@@ -11,8 +15,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>, };
|
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 PullResult = { "type": "success", message: string, } | { "type": "up_to_date" } | { "type": "needs_credentials", url: string, error: string | null, };
|
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 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, };
|
||||||
|
|||||||
4
crates/yaak-git/bindings/gen_models.ts
generated
4
crates/yaak-git/bindings/gen_models.ts
generated
@@ -1,5 +1,7 @@
|
|||||||
// 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, };
|
||||||
@@ -18,4 +20,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, };
|
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>, };
|
||||||
|
|||||||
@@ -3,20 +3,31 @@ 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 { GitCommit, GitRemote, GitStatusSummary, PullResult, PushResult } from './bindings/gen_git';
|
import { BranchDeleteResult, CloneResult, 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'] });
|
||||||
@@ -59,7 +70,6 @@ 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,
|
||||||
@@ -69,6 +79,15 @@ 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'],
|
||||||
@@ -90,21 +109,31 @@ 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,
|
||||||
}),
|
}),
|
||||||
branch: createFastMutation<void, string, { branch: string }>({
|
createBranch: createFastMutation<void, string, { branch: string; base?: 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; force: boolean }>({
|
mergeBranch: createFastMutation<void, string, { branch: string }>({
|
||||||
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<void, string, { branch: string }>({
|
deleteBranch: createFastMutation<BranchDeleteResult, string, { branch: string, force?: boolean }>({
|
||||||
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 }),
|
||||||
@@ -123,10 +152,9 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => {
|
|||||||
},
|
},
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}),
|
}),
|
||||||
fetchAll: createFastMutation<string, string, void>({
|
fetchAll: createFastMutation<void, string, void>({
|
||||||
mutationKey: ['git', 'checkout', dir],
|
mutationKey: ['git', 'fetch_all', 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],
|
||||||
@@ -137,21 +165,51 @@ 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;
|
|
||||||
|
|
||||||
// Needs credentials, prompt for them
|
if (result.type === 'needs_credentials') {
|
||||||
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', {
|
||||||
dir,
|
remoteUrl: result.url,
|
||||||
remoteUrl: result.url,
|
username: creds.username,
|
||||||
username: creds.username,
|
password: creds.password,
|
||||||
password: creds.password,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Pull again
|
// Pull again after credentials
|
||||||
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,
|
||||||
}),
|
}),
|
||||||
@@ -160,9 +218,39 @@ 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,38 +1,30 @@
|
|||||||
|
use crate::error::Error::GitNotFound;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::Stdio;
|
||||||
|
use tokio::process::Command;
|
||||||
|
use yaak_common::command::new_xplatform_command;
|
||||||
|
|
||||||
use crate::error::Error::GitNotFound;
|
/// Create a git command that runs in the specified directory
|
||||||
#[cfg(target_os = "windows")]
|
pub(crate) async fn new_binary_command(dir: &Path) -> Result<Command> {
|
||||||
use std::os::windows::process::CommandExt;
|
let mut cmd = new_binary_command_global().await?;
|
||||||
|
cmd.arg("-C").arg(dir);
|
||||||
|
Ok(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
/// Create a git command without a specific directory (for global operations)
|
||||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
pub(crate) async fn new_binary_command_global() -> Result<Command> {
|
||||||
|
|
||||||
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 = Command::new("git");
|
let mut probe = new_xplatform_command("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());
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
let status = probe.status().await.map_err(|_| GitNotFound)?;
|
||||||
{
|
|
||||||
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 mut cmd = Command::new("git");
|
let cmd = new_xplatform_command("git");
|
||||||
cmd.arg("-C").arg(dir);
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,99 +1,153 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
pub fn git_checkout_branch(dir: &Path, branch_name: &str, force: bool) -> Result<String> {
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||||
if branch_name.starts_with("origin/") {
|
#[serde(rename_all = "snake_case", tag = "type")]
|
||||||
return git_checkout_remote_branch(dir, branch_name, force);
|
#[ts(export, export_to = "gen_git.ts")]
|
||||||
}
|
pub enum BranchDeleteResult {
|
||||||
|
Success { message: String },
|
||||||
|
NotFullyMerged,
|
||||||
|
}
|
||||||
|
|
||||||
let repo = open_repo(dir)?;
|
pub async fn git_checkout_branch(dir: &Path, branch_name: &str, force: bool) -> Result<String> {
|
||||||
let branch = get_branch_by_name(&repo, branch_name)?;
|
let branch_name = branch_name.trim_start_matches("origin/");
|
||||||
let branch_ref = branch.into_reference();
|
|
||||||
let branch_tree = branch_ref.peel_to_tree()?;
|
|
||||||
|
|
||||||
let mut options = CheckoutBuilder::default();
|
let mut args = vec!["checkout"];
|
||||||
if force {
|
if force {
|
||||||
options.force();
|
args.push("--force");
|
||||||
}
|
}
|
||||||
|
args.push(branch_name);
|
||||||
|
|
||||||
repo.checkout_tree(branch_tree.as_object(), Some(&mut options))?;
|
let out = new_binary_command(dir)
|
||||||
repo.set_head(branch_ref.name().unwrap())?;
|
.await?
|
||||||
|
.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(crate) fn git_checkout_remote_branch(
|
pub async fn git_create_branch(dir: &Path, name: &str, base: Option<&str>) -> Result<()> {
|
||||||
dir: &Path,
|
let mut cmd = new_binary_command(dir).await?;
|
||||||
branch_name: &str,
|
cmd.arg("branch").arg(name);
|
||||||
force: bool,
|
if let Some(base_branch) = base {
|
||||||
) -> Result<String> {
|
cmd.arg(base_branch);
|
||||||
let branch_name = branch_name.trim_start_matches("origin/");
|
|
||||||
let repo = open_repo(dir)?;
|
|
||||||
|
|
||||||
let refname = format!("refs/remotes/origin/{}", branch_name);
|
|
||||||
let remote_ref = repo.find_reference(&refname)?;
|
|
||||||
let commit = remote_ref.peel_to_commit()?;
|
|
||||||
|
|
||||||
let mut new_branch = repo.branch(branch_name, &commit, false)?;
|
|
||||||
let upstream_name = format!("origin/{}", branch_name);
|
|
||||||
new_branch.set_upstream(Some(&upstream_name))?;
|
|
||||||
|
|
||||||
git_checkout_branch(dir, branch_name, force)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn git_create_branch(dir: &Path, name: &str) -> Result<()> {
|
|
||||||
let repo = open_repo(dir)?;
|
|
||||||
let head = match repo.head() {
|
|
||||||
Ok(h) => h,
|
|
||||||
Err(e) if e.code() == git2::ErrorCode::UnbornBranch => {
|
|
||||||
let msg = "Cannot create branch when there are no commits";
|
|
||||||
return Err(GenericError(msg.into()));
|
|
||||||
}
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
};
|
|
||||||
let head = head.peel_to_commit()?;
|
|
||||||
|
|
||||||
repo.branch(name, &head, false)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn git_delete_branch(dir: &Path, name: &str) -> Result<()> {
|
|
||||||
let repo = open_repo(dir)?;
|
|
||||||
let mut branch = get_branch_by_name(&repo, name)?;
|
|
||||||
|
|
||||||
if branch.is_head() {
|
|
||||||
info!("Deleting head branch");
|
|
||||||
let branches = repo.branches(Some(BranchType::Local))?;
|
|
||||||
let other_branch = branches.into_iter().filter_map(|b| b.ok()).find(|b| !b.0.is_head());
|
|
||||||
let other_branch = match other_branch {
|
|
||||||
None => return Err(GenericError("Cannot delete only branch".into())),
|
|
||||||
Some(b) => bytes_to_string(b.0.name_bytes()?)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
git_checkout_branch(dir, &other_branch, true)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
branch.delete()?;
|
let out =
|
||||||
|
cmd.output().await.map_err(|e| GenericError(format!("failed to run git branch: {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 create branch: {}", combined.trim())));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn git_merge_branch(dir: &Path, name: &str, _force: bool) -> Result<()> {
|
pub async fn git_delete_branch(dir: &Path, name: &str, force: bool) -> Result<BranchDeleteResult> {
|
||||||
let repo = open_repo(dir)?;
|
let mut cmd = new_binary_command(dir).await?;
|
||||||
let local_branch = get_current_branch(&repo)?.unwrap();
|
|
||||||
|
|
||||||
let commit_to_merge = get_branch_by_name(&repo, name)?.into_reference();
|
let out =
|
||||||
let commit_to_merge = repo.reference_to_annotated_commit(&commit_to_merge)?;
|
if force { cmd.args(["branch", "-D", name]) } else { cmd.args(["branch", "-d", name]) }
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map_err(|e| GenericError(format!("failed to run git branch -d: {e}")))?;
|
||||||
|
|
||||||
do_merge(&repo, &local_branch, &commit_to_merge)?;
|
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())));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn git_delete_remote_branch(dir: &Path, name: &str) -> Result<()> {
|
||||||
|
// Remote branch names come in as "origin/branch-name", extract the branch name
|
||||||
|
let branch_name = name.trim_start_matches("origin/");
|
||||||
|
|
||||||
|
let out = new_binary_command(dir)
|
||||||
|
.await?
|
||||||
|
.args(["push", "origin", "--delete", branch_name])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map_err(|e| GenericError(format!("failed to run git push --delete: {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 delete remote branch: {}", combined.trim())));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn git_rename_branch(dir: &Path, old_name: &str, new_name: &str) -> Result<()> {
|
||||||
|
let out = new_binary_command(dir)
|
||||||
|
.await?
|
||||||
|
.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 stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
let combined = format!("{}{}", stdout, stderr);
|
||||||
|
|
||||||
|
if !out.status.success() {
|
||||||
|
return Err(GenericError(format!("Failed to rename branch: {}", combined.trim())));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
53
crates/yaak-git/src/clone.rs
Normal file
53
crates/yaak-git/src/clone.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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,8 +3,9 @@ use crate::error::Error::GenericError;
|
|||||||
use log::info;
|
use log::info;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub fn git_commit(dir: &Path, message: &str) -> crate::error::Result<()> {
|
pub async fn git_commit(dir: &Path, message: &str) -> crate::error::Result<()> {
|
||||||
let out = new_binary_command(dir)?.args(["commit", "--message", message]).output()?;
|
let out =
|
||||||
|
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,24 +1,19 @@
|
|||||||
use crate::binary::new_binary_command;
|
use crate::binary::new_binary_command_global;
|
||||||
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(
|
pub async fn git_add_credential(remote_url: &str, username: &str, password: &str) -> Result<()> {
|
||||||
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(dir)?
|
let mut child = new_binary_command_global()
|
||||||
|
.await?
|
||||||
.args(["credential", "approve"])
|
.args(["credential", "approve"])
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
@@ -26,19 +21,21 @@ pub async fn git_add_credential(
|
|||||||
|
|
||||||
{
|
{
|
||||||
let stdin = child.stdin.as_mut().unwrap();
|
let stdin = child.stdin.as_mut().unwrap();
|
||||||
writeln!(stdin, "protocol={}", protocol)?;
|
stdin.write_all(format!("protocol={}\n", protocol).as_bytes()).await?;
|
||||||
writeln!(stdin, "host={}", host)?;
|
stdin.write_all(format!("host={}\n", host).as_bytes()).await?;
|
||||||
if let Some(path) = path {
|
if let Some(path) = path {
|
||||||
if !path.is_empty() {
|
if !path.is_empty() {
|
||||||
writeln!(stdin, "path={}", path.trim_start_matches('/'))?;
|
stdin
|
||||||
|
.write_all(format!("path={}\n", path.trim_start_matches('/')).as_bytes())
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writeln!(stdin, "username={}", username)?;
|
stdin.write_all(format!("username={}\n", username).as_bytes()).await?;
|
||||||
writeln!(stdin, "password={}", password)?;
|
stdin.write_all(format!("password={}\n", password).as_bytes()).await?;
|
||||||
writeln!(stdin)?; // blank line terminator
|
stdin.write_all(b"\n").await?; // blank line terminator
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = child.wait()?;
|
let status = child.wait().await?;
|
||||||
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,10 +3,12 @@ use crate::error::Error::GenericError;
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub fn git_fetch_all(dir: &Path) -> Result<()> {
|
pub async 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,31 +1,38 @@
|
|||||||
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::{git_checkout_branch, git_create_branch, git_delete_branch, git_merge_branch};
|
pub use 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};
|
pub use pull::{PullResult, git_pull, git_pull_force_reset, git_pull_merge};
|
||||||
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;
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
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,19 +15,41 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn git_pull(dir: &Path) -> Result<PullResult> {
|
fn has_uncommitted_changes(dir: &Path) -> Result<bool> {
|
||||||
let repo = open_repo(dir)?;
|
let repo = open_repo(dir)?;
|
||||||
let branch_name = get_current_branch_name(&repo)?;
|
let mut opts = git2::StatusOptions::new();
|
||||||
let remote = get_default_remote_in_repo(&repo)?;
|
opts.include_ignored(false).include_untracked(false);
|
||||||
let remote_name = remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?;
|
let statuses = repo.statuses(Some(&mut opts))?;
|
||||||
let remote_url = remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?;
|
Ok(statuses.iter().any(|e| e.status() != git2::Status::CURRENT))
|
||||||
|
}
|
||||||
|
|
||||||
let out = new_binary_command(dir)?
|
pub async fn git_pull(dir: &Path) -> Result<PullResult> {
|
||||||
|
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);
|
||||||
@@ -48,6 +70,13 @@ pub 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}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +87,65 @@ pub 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,17 +17,25 @@ pub enum PushResult {
|
|||||||
NeedsCredentials { url: String, error: Option<String> },
|
NeedsCredentials { url: String, error: Option<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn git_push(dir: &Path) -> Result<PushResult> {
|
pub async fn git_push(dir: &Path) -> Result<PushResult> {
|
||||||
let repo = open_repo(dir)?;
|
// Extract all git2 data before any await points (git2 types are not Send)
|
||||||
let branch_name = get_current_branch_name(&repo)?;
|
let (branch_name, remote_name, remote_url) = {
|
||||||
let remote = get_default_remote_for_push_in_repo(&repo)?;
|
let repo = open_repo(dir)?;
|
||||||
let remote_name = remote.name().ok_or(GenericError("Failed to get remote name".to_string()))?;
|
let branch_name = get_current_branch_name(&repo)?;
|
||||||
let remote_url = remote.url().ok_or(GenericError("Failed to get remote url".to_string()))?;
|
let remote = get_default_remote_for_push_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)?
|
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);
|
||||||
|
|||||||
20
crates/yaak-git/src/reset.rs
Normal file
20
crates/yaak-git/src/reset.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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,6 +18,8 @@ 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)]
|
||||||
@@ -160,6 +162,18 @@ 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,
|
||||||
@@ -168,5 +182,7 @@ 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,10 +47,6 @@ 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,5 +22,6 @@ 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,14 +115,18 @@ impl GrpcConnection {
|
|||||||
Ok(client.unary(req, path, codec).await?)
|
Ok(client.unary(req, path, codec).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn streaming(
|
pub async fn streaming<F>(
|
||||||
&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>,
|
||||||
) -> Result<Response<Streaming<DynamicMessage>>> {
|
on_message: F,
|
||||||
|
) -> 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();
|
||||||
@@ -131,31 +135,39 @@ 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.filter_map(move |json| {
|
stream
|
||||||
let pool = pool.clone();
|
.then(move |json| {
|
||||||
let uri = uri.clone();
|
let pool = pool.clone();
|
||||||
let input_message = input_message.clone();
|
let uri = uri.clone();
|
||||||
let md = md.clone();
|
let input_message = input_message.clone();
|
||||||
let use_reflection = use_reflection.clone();
|
let md = md.clone();
|
||||||
let client_cert = client_cert.clone();
|
let use_reflection = use_reflection.clone();
|
||||||
tokio::runtime::Handle::current().block_on(async move {
|
let client_cert = client_cert.clone();
|
||||||
if use_reflection {
|
let on_message = on_message.clone();
|
||||||
if let Err(e) =
|
let json_clone = json.clone();
|
||||||
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
async move {
|
||||||
{
|
if use_reflection {
|
||||||
warn!("Failed to resolve Any types: {e}");
|
if let Err(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);
|
||||||
let mut de = Deserializer::from_str(&json);
|
match DynamicMessage::deserialize(input_message, &mut de) {
|
||||||
match DynamicMessage::deserialize(input_message, &mut de) {
|
Ok(m) => {
|
||||||
Ok(m) => Some(m),
|
on_message(Ok(json_clone));
|
||||||
Err(e) => {
|
Some(m)
|
||||||
warn!("Failed to deserialize message: {e}");
|
}
|
||||||
None
|
Err(e) => {
|
||||||
|
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());
|
||||||
@@ -169,14 +181,18 @@ impl GrpcConnection {
|
|||||||
Ok(client.streaming(req, path, codec).await?)
|
Ok(client.streaming(req, path, codec).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn client_streaming(
|
pub async fn client_streaming<F>(
|
||||||
&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>,
|
||||||
) -> Result<Response<DynamicMessage>> {
|
on_message: F,
|
||||||
|
) -> 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();
|
||||||
@@ -185,31 +201,39 @@ 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.filter_map(move |json| {
|
stream
|
||||||
let pool = pool.clone();
|
.then(move |json| {
|
||||||
let uri = uri.clone();
|
let pool = pool.clone();
|
||||||
let input_message = input_message.clone();
|
let uri = uri.clone();
|
||||||
let md = md.clone();
|
let input_message = input_message.clone();
|
||||||
let use_reflection = use_reflection.clone();
|
let md = md.clone();
|
||||||
let client_cert = client_cert.clone();
|
let use_reflection = use_reflection.clone();
|
||||||
tokio::runtime::Handle::current().block_on(async move {
|
let client_cert = client_cert.clone();
|
||||||
if use_reflection {
|
let on_message = on_message.clone();
|
||||||
if let Err(e) =
|
let json_clone = json.clone();
|
||||||
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
async move {
|
||||||
{
|
if use_reflection {
|
||||||
warn!("Failed to resolve Any types: {e}");
|
if let Err(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);
|
||||||
let mut de = Deserializer::from_str(&json);
|
match DynamicMessage::deserialize(input_message, &mut de) {
|
||||||
match DynamicMessage::deserialize(input_message, &mut de) {
|
Ok(m) => {
|
||||||
Ok(m) => Some(m),
|
on_message(Ok(json_clone));
|
||||||
Err(e) => {
|
Some(m)
|
||||||
warn!("Failed to deserialize message: {e}");
|
}
|
||||||
None
|
Err(e) => {
|
||||||
|
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());
|
||||||
@@ -316,10 +340,9 @@ 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 skip_cache || self.get_pool(id, uri, proto_files).is_none() {
|
if 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 out = Command::new(&config.protoc_bin_path)
|
let mut cmd = new_xplatform_command(&config.protoc_bin_path);
|
||||||
.args(&args)
|
cmd.args(&args);
|
||||||
.output()
|
|
||||||
.await
|
let out =
|
||||||
.map_err(|e| GenericError(format!("Failed to run protoc: {}", e)))?;
|
cmd.output().await.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!(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ publish = false
|
|||||||
async-compression = { version = "0.4", features = ["tokio", "gzip", "deflate", "brotli", "zstd"] }
|
async-compression = { version = "0.4", features = ["tokio", "gzip", "deflate", "brotli", "zstd"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
brotli = "7"
|
brotli = "7"
|
||||||
bytes = "1.5.0"
|
bytes = "1.11.1"
|
||||||
cookie = "0.18.1"
|
cookie = "0.18.1"
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ 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)]
|
||||||
@@ -28,10 +30,14 @@ 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 {
|
||||||
pub(crate) fn build_client(&self) -> Result<Client> {
|
/// Build a reqwest Client and return it along with the DNS resolver.
|
||||||
|
/// 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())
|
||||||
@@ -40,15 +46,19 @@ 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
|
// Configure DNS resolver - keep a reference to configure per-request
|
||||||
client = client.dns_resolver(LocalhostResolver::new());
|
let resolver = LocalhostResolver::new(self.dns_overrides.clone());
|
||||||
|
client = client.dns_resolver(resolver.clone());
|
||||||
|
|
||||||
// Configure proxy
|
// Configure proxy
|
||||||
match self.proxy.clone() {
|
match self.proxy.clone() {
|
||||||
@@ -69,7 +79,7 @@ impl HttpConnectionOptions {
|
|||||||
self.client_certificate.is_some()
|
self.client_certificate.is_some()
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(client.build()?)
|
Ok((client.build()?, resolver))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +1,185 @@
|
|||||||
|
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() -> Arc<Self> {
|
pub fn new(dns_overrides: Vec<DnsOverride>) -> 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 {
|
||||||
match HyperName::from_str(&name_str) {
|
let start = Instant::now();
|
||||||
Ok(n) => fallback
|
|
||||||
.call(n)
|
let result = match HyperName::from_str(&name_str) {
|
||||||
.await
|
Ok(n) => fallback.call(n).await,
|
||||||
.map(|addrs| Box::new(addrs) as Addrs)
|
Err(e) => return Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>),
|
||||||
.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,4 +1,5 @@
|
|||||||
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;
|
||||||
@@ -7,8 +8,15 @@ 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, (Client, Instant)>>>,
|
connections: Arc<RwLock<BTreeMap<String, (CachedClient, Instant)>>>,
|
||||||
ttl: Duration,
|
ttl: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,21 +28,26 @@ impl HttpConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_client(&self, opt: &HttpConnectionOptions) -> Result<Client> {
|
pub async fn get_client(&self, opt: &HttpConnectionOptions) -> Result<CachedClient> {
|
||||||
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((c, last_used)) = connections.get_mut(&id) {
|
if let Some((cached, 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(c.clone());
|
return Ok(CachedClient {
|
||||||
|
client: cached.client.clone(),
|
||||||
|
resolver: cached.resolver.clone(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let c = opt.build_client()?;
|
let (client, resolver) = opt.build_client()?;
|
||||||
connections.insert(id.into(), (c.clone(), Instant::now()));
|
let cached = CachedClient { client: client.clone(), resolver: resolver.clone() };
|
||||||
Ok(c)
|
connections.insert(id.into(), (cached, Instant::now()));
|
||||||
|
|
||||||
|
Ok(CachedClient { client, resolver })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,14 @@ 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,
|
||||||
@@ -45,6 +52,12 @@ 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 {
|
||||||
@@ -59,7 +72,16 @@ impl Display for HttpResponseEvent {
|
|||||||
};
|
};
|
||||||
write!(f, "* Redirect {} -> {} ({})", status, url, behavior_str)
|
write!(f, "* Redirect {} -> {} ({})", status, url, behavior_str)
|
||||||
}
|
}
|
||||||
HttpResponseEvent::SendUrl { method, path } => write!(f, "> {} {}", method, path),
|
HttpResponseEvent::SendUrl { method, scheme, username, password, host, port, path, query, fragment } => {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -67,6 +89,19 @@ 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +120,9 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
|
|||||||
RedirectBehavior::DropBody => "drop_body".to_string(),
|
RedirectBehavior::DropBody => "drop_body".to_string(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
HttpResponseEvent::SendUrl { method, path } => D::SendUrl { method, path },
|
HttpResponseEvent::SendUrl { method, scheme, username, password, host, port, path, query, fragment } => {
|
||||||
|
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 }
|
||||||
}
|
}
|
||||||
@@ -93,6 +130,9 @@ 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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,6 +394,9 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,8 +433,15 @@ impl HttpSender for ReqwestSender {
|
|||||||
));
|
));
|
||||||
|
|
||||||
send_event(HttpResponseEvent::SendUrl {
|
send_event(HttpResponseEvent::SendUrl {
|
||||||
path: sendable_req.url().path().to_string(),
|
|
||||||
method: sendable_req.method().to_string(),
|
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(),
|
||||||
|
query: sendable_req.url().query().unwrap_or_default().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,6 +168,7 @@ 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
|
||||||
@@ -181,6 +182,8 @@ 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
|
||||||
@@ -220,6 +223,33 @@ 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)
|
||||||
@@ -269,9 +299,20 @@ 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 {
|
||||||
@@ -282,7 +323,10 @@ mod tests {
|
|||||||
|
|
||||||
impl MockSender {
|
impl MockSender {
|
||||||
fn new(responses: Vec<MockResponse>) -> Self {
|
fn new(responses: Vec<MockResponse>) -> Self {
|
||||||
Self { responses: Arc::new(Mutex::new(responses)) }
|
Self {
|
||||||
|
responses: Arc::new(Mutex::new(responses)),
|
||||||
|
captured_requests: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,9 +334,16 @@ 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()))
|
||||||
@@ -342,7 +393,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_transaction_single_redirect() {
|
async fn test_transaction_single_redirect() {
|
||||||
let redirect_headers = vec![("Location".to_string(), "https://example.com/new".to_string())];
|
let redirect_headers =
|
||||||
|
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![] },
|
||||||
@@ -373,7 +425,8 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_transaction_max_redirects_exceeded() {
|
async fn test_transaction_max_redirects_exceeded() {
|
||||||
let redirect_headers = vec![("Location".to_string(), "https://example.com/loop".to_string())];
|
let redirect_headers =
|
||||||
|
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)
|
||||||
@@ -525,7 +578,8 @@ mod tests {
|
|||||||
_request: SendableHttpRequest,
|
_request: SendableHttpRequest,
|
||||||
_event_tx: mpsc::Sender<HttpResponseEvent>,
|
_event_tx: mpsc::Sender<HttpResponseEvent>,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
let headers = vec![("set-cookie".to_string(), "session=xyz789; Path=/".to_string())];
|
let headers =
|
||||||
|
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![]));
|
||||||
@@ -584,7 +638,10 @@ 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>> =
|
||||||
@@ -720,4 +777,116 @@ 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
crates/yaak-models/bindings/gen_models.ts
generated
8
crates/yaak-models/bindings/gen_models.ts
generated
@@ -12,6 +12,8 @@ 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, };
|
||||||
@@ -38,7 +40,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, 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, 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 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, };
|
||||||
|
|
||||||
@@ -47,7 +49,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, };
|
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 HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
@@ -91,6 +93,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, };
|
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 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,6 +206,34 @@ 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,9 +0,0 @@
|
|||||||
-- Add nullable settings columns to folders (NULL = inherit from parent)
|
|
||||||
ALTER TABLE folders ADD COLUMN setting_request_timeout INTEGER DEFAULT NULL;
|
|
||||||
ALTER TABLE folders ADD COLUMN setting_validate_certificates BOOLEAN DEFAULT NULL;
|
|
||||||
ALTER TABLE folders ADD COLUMN setting_follow_redirects BOOLEAN DEFAULT NULL;
|
|
||||||
|
|
||||||
-- Add nullable settings columns to http_requests (NULL = inherit from parent)
|
|
||||||
ALTER TABLE http_requests ADD COLUMN setting_request_timeout INTEGER DEFAULT NULL;
|
|
||||||
ALTER TABLE http_requests ADD COLUMN setting_validate_certificates BOOLEAN DEFAULT NULL;
|
|
||||||
ALTER TABLE http_requests ADD COLUMN setting_follow_redirects BOOLEAN DEFAULT NULL;
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add DNS resolution timing to http_responses
|
||||||
|
ALTER TABLE http_responses ADD COLUMN elapsed_dns INTEGER DEFAULT 0 NOT NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add DNS overrides setting to workspaces
|
||||||
|
ALTER TABLE workspaces ADD COLUMN setting_dns_overrides TEXT DEFAULT '[]' NOT NULL;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- 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;
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use crate::models::HttpRequestIden::{
|
||||||
|
Authentication, AuthenticationType, Body, BodyType, CreatedAt, Description, FolderId, Headers,
|
||||||
|
Method, Name, SortPriority, UpdatedAt, Url, UrlParameters, WorkspaceId,
|
||||||
|
};
|
||||||
use crate::util::{UpdateSource, generate_prefixed_id};
|
use crate::util::{UpdateSource, generate_prefixed_id};
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use rusqlite::Row;
|
use rusqlite::Row;
|
||||||
@@ -69,6 +73,20 @@ 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")]
|
||||||
@@ -111,36 +129,6 @@ impl Default for EditorKeymap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Settings that can be inherited at workspace → folder → request level.
|
|
||||||
/// All fields optional - None means "inherit from parent" (or use default if at root).
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
|
||||||
#[serde(default, rename_all = "camelCase")]
|
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
|
||||||
pub struct HttpRequestSettingsOverride {
|
|
||||||
pub setting_validate_certificates: Option<bool>,
|
|
||||||
pub setting_follow_redirects: Option<bool>,
|
|
||||||
pub setting_request_timeout: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolved settings with concrete values (after inheritance + defaults applied)
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct ResolvedHttpRequestSettings {
|
|
||||||
pub validate_certificates: bool,
|
|
||||||
pub follow_redirects: bool,
|
|
||||||
pub request_timeout: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResolvedHttpRequestSettings {
|
|
||||||
/// Default values when nothing is set in the inheritance chain
|
|
||||||
pub fn defaults() -> Self {
|
|
||||||
Self {
|
|
||||||
validate_certificates: true,
|
|
||||||
follow_redirects: true,
|
|
||||||
request_timeout: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
@@ -323,10 +311,14 @@ pub struct Workspace {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub encryption_key_challenge: Option<String>,
|
pub encryption_key_challenge: Option<String>,
|
||||||
|
|
||||||
// Inheritable settings (Option = can be null, defaults applied at resolution time)
|
// Settings
|
||||||
pub setting_validate_certificates: Option<bool>,
|
#[serde(default = "default_true")]
|
||||||
pub setting_follow_redirects: Option<bool>,
|
pub setting_validate_certificates: bool,
|
||||||
pub setting_request_timeout: Option<i32>,
|
#[serde(default = "default_true")]
|
||||||
|
pub setting_follow_redirects: bool,
|
||||||
|
pub setting_request_timeout: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub setting_dns_overrides: Vec<DnsOverride>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for Workspace {
|
impl UpsertModelInfo for Workspace {
|
||||||
@@ -367,6 +359,7 @@ 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()),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,6 +376,7 @@ impl UpsertModelInfo for Workspace {
|
|||||||
WorkspaceIden::SettingFollowRedirects,
|
WorkspaceIden::SettingFollowRedirects,
|
||||||
WorkspaceIden::SettingRequestTimeout,
|
WorkspaceIden::SettingRequestTimeout,
|
||||||
WorkspaceIden::SettingValidateCertificates,
|
WorkspaceIden::SettingValidateCertificates,
|
||||||
|
WorkspaceIden::SettingDnsOverrides,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,6 +386,7 @@ 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")?,
|
||||||
@@ -406,6 +401,7 @@ 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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -750,11 +746,6 @@ pub struct Folder {
|
|||||||
pub headers: Vec<HttpRequestHeader>,
|
pub headers: Vec<HttpRequestHeader>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub sort_priority: f64,
|
pub sort_priority: f64,
|
||||||
|
|
||||||
// Inheritable settings (Option = null means inherit from parent)
|
|
||||||
pub setting_validate_certificates: Option<bool>,
|
|
||||||
pub setting_follow_redirects: Option<bool>,
|
|
||||||
pub setting_request_timeout: Option<i32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for Folder {
|
impl UpsertModelInfo for Folder {
|
||||||
@@ -794,9 +785,6 @@ impl UpsertModelInfo for Folder {
|
|||||||
(Description, self.description.into()),
|
(Description, self.description.into()),
|
||||||
(Name, self.name.trim().into()),
|
(Name, self.name.trim().into()),
|
||||||
(SortPriority, self.sort_priority.into()),
|
(SortPriority, self.sort_priority.into()),
|
||||||
(SettingValidateCertificates, self.setting_validate_certificates.into()),
|
|
||||||
(SettingFollowRedirects, self.setting_follow_redirects.into()),
|
|
||||||
(SettingRequestTimeout, self.setting_request_timeout.into()),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,9 +798,6 @@ impl UpsertModelInfo for Folder {
|
|||||||
FolderIden::Description,
|
FolderIden::Description,
|
||||||
FolderIden::FolderId,
|
FolderIden::FolderId,
|
||||||
FolderIden::SortPriority,
|
FolderIden::SortPriority,
|
||||||
FolderIden::SettingValidateCertificates,
|
|
||||||
FolderIden::SettingFollowRedirects,
|
|
||||||
FolderIden::SettingRequestTimeout,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,9 +820,6 @@ impl UpsertModelInfo for Folder {
|
|||||||
headers: serde_json::from_str(&headers).unwrap_or_default(),
|
headers: serde_json::from_str(&headers).unwrap_or_default(),
|
||||||
authentication_type: row.get("authentication_type")?,
|
authentication_type: row.get("authentication_type")?,
|
||||||
authentication: serde_json::from_str(&authentication).unwrap_or_default(),
|
authentication: serde_json::from_str(&authentication).unwrap_or_default(),
|
||||||
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
|
||||||
setting_follow_redirects: row.get("setting_follow_redirects")?,
|
|
||||||
setting_request_timeout: row.get("setting_request_timeout")?,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -895,11 +877,6 @@ pub struct HttpRequest {
|
|||||||
pub sort_priority: f64,
|
pub sort_priority: f64,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub url_parameters: Vec<HttpUrlParameter>,
|
pub url_parameters: Vec<HttpUrlParameter>,
|
||||||
|
|
||||||
// Inheritable settings (Option = null means inherit from parent)
|
|
||||||
pub setting_validate_certificates: Option<bool>,
|
|
||||||
pub setting_follow_redirects: Option<bool>,
|
|
||||||
pub setting_request_timeout: Option<i32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for HttpRequest {
|
impl UpsertModelInfo for HttpRequest {
|
||||||
@@ -927,7 +904,6 @@ impl UpsertModelInfo for HttpRequest {
|
|||||||
self,
|
self,
|
||||||
source: &UpdateSource,
|
source: &UpdateSource,
|
||||||
) -> Result<Vec<(impl IntoIden + Eq, impl Into<SimpleExpr>)>> {
|
) -> Result<Vec<(impl IntoIden + Eq, impl Into<SimpleExpr>)>> {
|
||||||
use HttpRequestIden::*;
|
|
||||||
Ok(vec![
|
Ok(vec![
|
||||||
(CreatedAt, upsert_date(source, self.created_at)),
|
(CreatedAt, upsert_date(source, self.created_at)),
|
||||||
(UpdatedAt, upsert_date(source, self.updated_at)),
|
(UpdatedAt, upsert_date(source, self.updated_at)),
|
||||||
@@ -944,14 +920,10 @@ impl UpsertModelInfo for HttpRequest {
|
|||||||
(AuthenticationType, self.authentication_type.into()),
|
(AuthenticationType, self.authentication_type.into()),
|
||||||
(Headers, serde_json::to_string(&self.headers)?.into()),
|
(Headers, serde_json::to_string(&self.headers)?.into()),
|
||||||
(SortPriority, self.sort_priority.into()),
|
(SortPriority, self.sort_priority.into()),
|
||||||
(SettingValidateCertificates, self.setting_validate_certificates.into()),
|
|
||||||
(SettingFollowRedirects, self.setting_follow_redirects.into()),
|
|
||||||
(SettingRequestTimeout, self.setting_request_timeout.into()),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_columns() -> Vec<impl IntoIden> {
|
fn update_columns() -> Vec<impl IntoIden> {
|
||||||
use HttpRequestIden::*;
|
|
||||||
vec![
|
vec![
|
||||||
UpdatedAt,
|
UpdatedAt,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
@@ -967,9 +939,6 @@ impl UpsertModelInfo for HttpRequest {
|
|||||||
Url,
|
Url,
|
||||||
UrlParameters,
|
UrlParameters,
|
||||||
SortPriority,
|
SortPriority,
|
||||||
SettingValidateCertificates,
|
|
||||||
SettingFollowRedirects,
|
|
||||||
SettingRequestTimeout,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -996,9 +965,6 @@ impl UpsertModelInfo for HttpRequest {
|
|||||||
sort_priority: row.get("sort_priority")?,
|
sort_priority: row.get("sort_priority")?,
|
||||||
url: row.get("url")?,
|
url: row.get("url")?,
|
||||||
url_parameters: serde_json::from_str(url_parameters.as_str()).unwrap_or_default(),
|
url_parameters: serde_json::from_str(url_parameters.as_str()).unwrap_or_default(),
|
||||||
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
|
||||||
setting_follow_redirects: row.get("setting_follow_redirects")?,
|
|
||||||
setting_request_timeout: row.get("setting_request_timeout")?,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1387,6 +1353,7 @@ 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>,
|
||||||
@@ -1435,6 +1402,7 @@ 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()),
|
||||||
@@ -1456,6 +1424,7 @@ 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,
|
||||||
@@ -1489,6 +1458,7 @@ 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")?,
|
||||||
@@ -1525,7 +1495,21 @@ 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,
|
||||||
@@ -1545,6 +1529,12 @@ 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,3 +1,4 @@
|
|||||||
|
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};
|
||||||
@@ -87,6 +88,6 @@ impl<'a> DbContext<'a> {
|
|||||||
|
|
||||||
metadata.append(&mut grpc_request.metadata.clone());
|
metadata.append(&mut grpc_request.metadata.clone());
|
||||||
|
|
||||||
Ok(metadata)
|
Ok(dedupe_headers(metadata))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
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, ResolvedHttpRequestSettings};
|
use crate::models::{Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden};
|
||||||
use crate::util::UpdateSource;
|
use crate::util::UpdateSource;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -87,7 +88,7 @@ impl<'a> DbContext<'a> {
|
|||||||
|
|
||||||
headers.append(&mut http_request.headers.clone());
|
headers.append(&mut http_request.headers.clone());
|
||||||
|
|
||||||
Ok(headers)
|
Ok(dedupe_headers(headers))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_http_requests_for_folder_recursive(
|
pub fn list_http_requests_for_folder_recursive(
|
||||||
@@ -103,79 +104,4 @@ impl<'a> DbContext<'a> {
|
|||||||
}
|
}
|
||||||
Ok(children)
|
Ok(children)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve settings for an HTTP request by walking the inheritance chain:
|
|
||||||
/// Workspace → Folder(s) → Request
|
|
||||||
/// Last non-None value wins, then defaults are applied.
|
|
||||||
pub fn resolve_settings_for_http_request(
|
|
||||||
&self,
|
|
||||||
http_request: &HttpRequest,
|
|
||||||
) -> Result<ResolvedHttpRequestSettings> {
|
|
||||||
let workspace = self.get_workspace(&http_request.workspace_id)?;
|
|
||||||
|
|
||||||
// Start with None for all settings
|
|
||||||
let mut validate_certs: Option<bool> = None;
|
|
||||||
let mut follow_redirects: Option<bool> = None;
|
|
||||||
let mut timeout: Option<i32> = None;
|
|
||||||
|
|
||||||
// Apply workspace settings
|
|
||||||
if workspace.setting_validate_certificates.is_some() {
|
|
||||||
validate_certs = workspace.setting_validate_certificates;
|
|
||||||
}
|
|
||||||
if workspace.setting_follow_redirects.is_some() {
|
|
||||||
follow_redirects = workspace.setting_follow_redirects;
|
|
||||||
}
|
|
||||||
if workspace.setting_request_timeout.is_some() {
|
|
||||||
timeout = workspace.setting_request_timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply folder chain settings (root first, immediate parent last)
|
|
||||||
if let Some(folder_id) = &http_request.folder_id {
|
|
||||||
let folders = self.get_folder_ancestors(folder_id)?;
|
|
||||||
for folder in folders {
|
|
||||||
if folder.setting_validate_certificates.is_some() {
|
|
||||||
validate_certs = folder.setting_validate_certificates;
|
|
||||||
}
|
|
||||||
if folder.setting_follow_redirects.is_some() {
|
|
||||||
follow_redirects = folder.setting_follow_redirects;
|
|
||||||
}
|
|
||||||
if folder.setting_request_timeout.is_some() {
|
|
||||||
timeout = folder.setting_request_timeout;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply request-level settings (highest priority)
|
|
||||||
if http_request.setting_validate_certificates.is_some() {
|
|
||||||
validate_certs = http_request.setting_validate_certificates;
|
|
||||||
}
|
|
||||||
if http_request.setting_follow_redirects.is_some() {
|
|
||||||
follow_redirects = http_request.setting_follow_redirects;
|
|
||||||
}
|
|
||||||
if http_request.setting_request_timeout.is_some() {
|
|
||||||
timeout = http_request.setting_request_timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply defaults for anything still None
|
|
||||||
Ok(ResolvedHttpRequestSettings {
|
|
||||||
validate_certificates: validate_certs.unwrap_or(true),
|
|
||||||
follow_redirects: follow_redirects.unwrap_or(true),
|
|
||||||
request_timeout: timeout.unwrap_or(0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get folder ancestors in order from root to immediate parent
|
|
||||||
fn get_folder_ancestors(&self, folder_id: &str) -> Result<Vec<Folder>> {
|
|
||||||
let mut ancestors = Vec::new();
|
|
||||||
let mut current_id = Some(folder_id.to_string());
|
|
||||||
|
|
||||||
while let Some(id) = current_id {
|
|
||||||
let folder = self.get_folder(&id)?;
|
|
||||||
current_id = folder.folder_id.clone();
|
|
||||||
ancestors.push(folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
ancestors.reverse(); // Root first, immediate parent last
|
|
||||||
Ok(ancestors)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,26 @@ mod websocket_connections;
|
|||||||
mod websocket_events;
|
mod websocket_events;
|
||||||
mod websocket_requests;
|
mod websocket_requests;
|
||||||
mod workspace_metas;
|
mod workspace_metas;
|
||||||
mod workspaces;
|
pub 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,3 +1,4 @@
|
|||||||
|
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};
|
||||||
@@ -95,6 +96,6 @@ impl<'a> DbContext<'a> {
|
|||||||
|
|
||||||
headers.append(&mut websocket_request.headers.clone());
|
headers.append(&mut websocket_request.headers.clone());
|
||||||
|
|
||||||
Ok(headers)
|
Ok(dedupe_headers(headers))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ impl<'a> DbContext<'a> {
|
|||||||
workspaces.push(self.upsert_workspace(
|
workspaces.push(self.upsert_workspace(
|
||||||
&Workspace {
|
&Workspace {
|
||||||
name: "Yaak".to_string(),
|
name: "Yaak".to_string(),
|
||||||
setting_follow_redirects: Some(true),
|
setting_follow_redirects: true,
|
||||||
setting_validate_certificates: Some(true),
|
setting_validate_certificates: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
&UpdateSource::Background,
|
&UpdateSource::Background,
|
||||||
@@ -65,28 +65,7 @@ impl<'a> DbContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn upsert_workspace(&self, w: &Workspace, source: &UpdateSource) -> Result<Workspace> {
|
pub fn upsert_workspace(&self, w: &Workspace, source: &UpdateSource) -> Result<Workspace> {
|
||||||
let mut workspace = w.clone();
|
self.upsert(w, source)
|
||||||
|
|
||||||
// Add default headers only for NEW workspaces (empty ID means insert, not update)
|
|
||||||
// This prevents re-adding headers if a user intentionally removes all headers
|
|
||||||
if workspace.id.is_empty() && workspace.headers.is_empty() {
|
|
||||||
workspace.headers = 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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
self.upsert(&workspace, source)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_auth_for_workspace(
|
pub fn resolve_auth_for_workspace(
|
||||||
@@ -101,6 +80,28 @@ 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> {
|
||||||
workspace.headers.clone()
|
let mut headers = default_headers();
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
16
crates/yaak-plugins/bindings/gen_events.ts
generated
16
crates/yaak-plugins/bindings/gen_events.ts
generated
File diff suppressed because one or more lines are too long
8
crates/yaak-plugins/bindings/gen_models.ts
generated
8
crates/yaak-plugins/bindings/gen_models.ts
generated
@@ -12,6 +12,8 @@ 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, };
|
||||||
@@ -38,7 +40,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, 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, 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 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, };
|
||||||
|
|
||||||
@@ -47,7 +49,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, };
|
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 HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
@@ -77,6 +79,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, };
|
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 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,10 +80,7 @@ pub async fn check_plugin_updates(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Search for plugins in the registry.
|
/// Search for plugins in the registry.
|
||||||
pub async fn search_plugins(
|
pub async fn search_plugins(http_client: &Client, query: &str) -> Result<PluginSearchResponse> {
|
||||||
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();
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ pub enum InternalEventPayload {
|
|||||||
PromptTextRequest(PromptTextRequest),
|
PromptTextRequest(PromptTextRequest),
|
||||||
PromptTextResponse(PromptTextResponse),
|
PromptTextResponse(PromptTextResponse),
|
||||||
|
|
||||||
|
PromptFormRequest(PromptFormRequest),
|
||||||
|
PromptFormResponse(PromptFormResponse),
|
||||||
|
|
||||||
WindowInfoRequest(WindowInfoRequest),
|
WindowInfoRequest(WindowInfoRequest),
|
||||||
WindowInfoResponse(WindowInfoResponse),
|
WindowInfoResponse(WindowInfoResponse),
|
||||||
|
|
||||||
@@ -571,6 +574,43 @@ pub struct PromptTextResponse {
|
|||||||
pub value: Option<String>,
|
pub value: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(default, rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
|
pub struct PromptFormRequest {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
#[ts(optional)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub inputs: Vec<FormInput>,
|
||||||
|
#[ts(optional)]
|
||||||
|
pub confirm_text: Option<String>,
|
||||||
|
#[ts(optional)]
|
||||||
|
pub cancel_text: Option<String>,
|
||||||
|
#[ts(optional)]
|
||||||
|
pub size: Option<DialogSize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
|
pub enum DialogSize {
|
||||||
|
Sm,
|
||||||
|
Md,
|
||||||
|
Lg,
|
||||||
|
Full,
|
||||||
|
Dynamic,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(default, rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
|
pub struct PromptFormResponse {
|
||||||
|
pub values: Option<HashMap<String, JsonPrimitive>>,
|
||||||
|
#[ts(optional)]
|
||||||
|
pub done: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_events.ts")]
|
#[ts(export, export_to = "gen_events.ts")]
|
||||||
@@ -911,6 +951,22 @@ pub enum EditorLanguage {
|
|||||||
Xml,
|
Xml,
|
||||||
Graphql,
|
Graphql,
|
||||||
Markdown,
|
Markdown,
|
||||||
|
C,
|
||||||
|
Clojure,
|
||||||
|
Csharp,
|
||||||
|
Go,
|
||||||
|
Http,
|
||||||
|
Java,
|
||||||
|
Kotlin,
|
||||||
|
ObjectiveC,
|
||||||
|
Ocaml,
|
||||||
|
Php,
|
||||||
|
Powershell,
|
||||||
|
Python,
|
||||||
|
R,
|
||||||
|
Ruby,
|
||||||
|
Shell,
|
||||||
|
Swift,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for EditorLanguage {
|
impl Default for EditorLanguage {
|
||||||
@@ -941,6 +997,10 @@ pub struct FormInputEditor {
|
|||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
pub read_only: Option<bool>,
|
pub read_only: Option<bool>,
|
||||||
|
|
||||||
|
/// Fixed number of visible rows
|
||||||
|
#[ts(optional)]
|
||||||
|
pub rows: Option<i32>,
|
||||||
|
|
||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
pub completion_options: Option<Vec<GenericCompletionOption>>,
|
pub completion_options: Option<Vec<GenericCompletionOption>>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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};
|
use tokio::sync::{Mutex, mpsc, oneshot};
|
||||||
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,6 +43,7 @@ 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,
|
||||||
@@ -70,6 +71,7 @@ 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);
|
||||||
@@ -81,6 +83,7 @@ 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,
|
||||||
@@ -141,9 +144,15 @@ impl PluginManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 2. Start Node.js runtime
|
// 2. Start Node.js runtime
|
||||||
start_nodejs_plugin_runtime(&node_bin_path, &plugin_runtime_main, addr, &kill_server_rx)
|
start_nodejs_plugin_runtime(
|
||||||
.await
|
&node_bin_path,
|
||||||
.unwrap();
|
&plugin_runtime_main,
|
||||||
|
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();
|
||||||
|
|
||||||
@@ -296,8 +305,15 @@ impl PluginManager {
|
|||||||
pub async fn terminate(&self) {
|
pub async fn terminate(&self) {
|
||||||
self.kill_tx.send_replace(true);
|
self.kill_tx.send_replace(true);
|
||||||
|
|
||||||
// Give it a bit of time to kill
|
// Wait for the plugin runtime process to actually exit
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
let killed_rx = self.killed_rx.lock().await.take();
|
||||||
|
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(
|
||||||
@@ -378,7 +394,8 @@ impl PluginManager {
|
|||||||
plugins: Vec<PluginHandle>,
|
plugins: Vec<PluginHandle>,
|
||||||
timeout_duration: Duration,
|
timeout_duration: Duration,
|
||||||
) -> Result<Vec<InternalEvent>> {
|
) -> Result<Vec<InternalEvent>> {
|
||||||
let label = format!("wait[{}.{}]", plugins.len(), payload.type_name());
|
let event_type = 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
|
||||||
@@ -412,10 +429,21 @@ 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 plugin responses. Got {}/{} responses",
|
"Timeout ({:?}) waiting for {} 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,7 +196,11 @@ pub fn decrypt_secure_template_function(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
new_tokens.push(Token::Raw {
|
new_tokens.push(Token::Raw {
|
||||||
text: template_function_secure_run(encryption_manager, args_map, plugin_context)?,
|
text: template_function_secure_run(
|
||||||
|
encryption_manager,
|
||||||
|
args_map,
|
||||||
|
plugin_context,
|
||||||
|
)?,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
t => {
|
t => {
|
||||||
@@ -216,7 +220,8 @@ pub fn encrypt_secure_template_function(
|
|||||||
plugin_context: &PluginContext,
|
plugin_context: &PluginContext,
|
||||||
template: &str,
|
template: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let decrypted = decrypt_secure_template_function(&encryption_manager, plugin_context, template)?;
|
let decrypted =
|
||||||
|
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, };
|
||||||
@@ -20,4 +22,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, };
|
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>, };
|
||||||
|
|||||||
@@ -296,11 +296,7 @@ pub fn compute_sync_ops(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_models(
|
fn workspace_models(db: &DbContext, version: &str, workspace_id: &str) -> Result<Vec<SyncModel>> {
|
||||||
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;
|
||||||
|
|||||||
@@ -1,31 +1,5 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { WebsocketConnection, WebsocketEvent, WebsocketRequest } from '@yaakapp-internal/models';
|
import { WebsocketConnection } 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', {
|
||||||
@@ -33,20 +7,6 @@ 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,6 +2,7 @@ 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;
|
||||||
@@ -10,7 +11,6 @@ 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;
|
||||||
|
|||||||
359
package-lock.json
generated
359
package-lock.json
generated
@@ -13,6 +13,7 @@
|
|||||||
"packages/plugin-runtime-types",
|
"packages/plugin-runtime-types",
|
||||||
"plugins-external/mcp-server",
|
"plugins-external/mcp-server",
|
||||||
"plugins-external/template-function-faker",
|
"plugins-external/template-function-faker",
|
||||||
|
"plugins-external/httpsnippet",
|
||||||
"plugins/action-copy-curl",
|
"plugins/action-copy-curl",
|
||||||
"plugins/action-copy-grpcurl",
|
"plugins/action-copy-grpcurl",
|
||||||
"plugins/action-send-folder",
|
"plugins/action-send-folder",
|
||||||
@@ -62,8 +63,15 @@
|
|||||||
"crates/yaak-ws",
|
"crates/yaak-ws",
|
||||||
"src-web"
|
"src-web"
|
||||||
],
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/lang-go": "^6.0.1",
|
||||||
|
"@codemirror/lang-java": "^6.0.2",
|
||||||
|
"@codemirror/lang-php": "^6.0.2",
|
||||||
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@codemirror/legacy-modes": "^6.5.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.10",
|
"@biomejs/biome": "^2.3.13",
|
||||||
"@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 +509,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/biome": {
|
"node_modules/@biomejs/biome": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.13.tgz",
|
||||||
"integrity": "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==",
|
"integrity": "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -517,20 +525,20 @@
|
|||||||
"url": "https://opencollective.com/biome"
|
"url": "https://opencollective.com/biome"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@biomejs/cli-darwin-arm64": "2.3.11",
|
"@biomejs/cli-darwin-arm64": "2.3.13",
|
||||||
"@biomejs/cli-darwin-x64": "2.3.11",
|
"@biomejs/cli-darwin-x64": "2.3.13",
|
||||||
"@biomejs/cli-linux-arm64": "2.3.11",
|
"@biomejs/cli-linux-arm64": "2.3.13",
|
||||||
"@biomejs/cli-linux-arm64-musl": "2.3.11",
|
"@biomejs/cli-linux-arm64-musl": "2.3.13",
|
||||||
"@biomejs/cli-linux-x64": "2.3.11",
|
"@biomejs/cli-linux-x64": "2.3.13",
|
||||||
"@biomejs/cli-linux-x64-musl": "2.3.11",
|
"@biomejs/cli-linux-x64-musl": "2.3.13",
|
||||||
"@biomejs/cli-win32-arm64": "2.3.11",
|
"@biomejs/cli-win32-arm64": "2.3.13",
|
||||||
"@biomejs/cli-win32-x64": "2.3.11"
|
"@biomejs/cli-win32-x64": "2.3.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-arm64": {
|
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.13.tgz",
|
||||||
"integrity": "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==",
|
"integrity": "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -545,9 +553,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-x64": {
|
"node_modules/@biomejs/cli-darwin-x64": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.13.tgz",
|
||||||
"integrity": "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==",
|
"integrity": "sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -562,9 +570,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-arm64": {
|
"node_modules/@biomejs/cli-linux-arm64": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.13.tgz",
|
||||||
"integrity": "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==",
|
"integrity": "sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -579,9 +587,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.13.tgz",
|
||||||
"integrity": "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==",
|
"integrity": "sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -596,9 +604,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64": {
|
"node_modules/@biomejs/cli-linux-x64": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.13.tgz",
|
||||||
"integrity": "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==",
|
"integrity": "sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -613,9 +621,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64-musl": {
|
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.13.tgz",
|
||||||
"integrity": "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==",
|
"integrity": "sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -630,9 +638,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-win32-arm64": {
|
"node_modules/@biomejs/cli-win32-arm64": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.13.tgz",
|
||||||
"integrity": "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==",
|
"integrity": "sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -647,9 +655,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-win32-x64": {
|
"node_modules/@biomejs/cli-win32-x64": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.13.tgz",
|
||||||
"integrity": "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==",
|
"integrity": "sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -736,6 +744,19 @@
|
|||||||
"@lezer/css": "^1.1.7"
|
"@lezer/css": "^1.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/lang-go": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.6.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@lezer/common": "^1.0.0",
|
||||||
|
"@lezer/go": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@codemirror/lang-html": {
|
"node_modules/@codemirror/lang-html": {
|
||||||
"version": "6.4.11",
|
"version": "6.4.11",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
|
||||||
@@ -753,6 +774,16 @@
|
|||||||
"@lezer/html": "^1.3.12"
|
"@lezer/html": "^1.3.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/lang-java": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@lezer/java": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@codemirror/lang-javascript": {
|
"node_modules/@codemirror/lang-javascript": {
|
||||||
"version": "6.2.4",
|
"version": "6.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
|
||||||
@@ -793,6 +824,32 @@
|
|||||||
"@lezer/markdown": "^1.0.0"
|
"@lezer/markdown": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/lang-php": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/lang-html": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@lezer/common": "^1.0.0",
|
||||||
|
"@lezer/php": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-python": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.3.2",
|
||||||
|
"@codemirror/language": "^6.8.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@lezer/common": "^1.2.1",
|
||||||
|
"@lezer/python": "^1.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@codemirror/lang-xml": {
|
"node_modules/@codemirror/lang-xml": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
|
||||||
@@ -807,6 +864,21 @@
|
|||||||
"@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",
|
||||||
@@ -821,6 +893,15 @@
|
|||||||
"style-mod": "^4.0.0"
|
"style-mod": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/legacy-modes": {
|
||||||
|
"version": "6.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz",
|
||||||
|
"integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@codemirror/lint": {
|
"node_modules/@codemirror/lint": {
|
||||||
"version": "6.9.2",
|
"version": "6.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz",
|
||||||
@@ -832,6 +913,19 @@
|
|||||||
"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",
|
||||||
@@ -1386,9 +1480,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@hono/node-server": {
|
"node_modules/@hono/node-server": {
|
||||||
"version": "1.19.8",
|
"version": "1.19.9",
|
||||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
|
||||||
"integrity": "sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==",
|
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.14.1"
|
"node": ">=18.14.1"
|
||||||
@@ -1542,6 +1636,17 @@
|
|||||||
"lezer-generator": "src/lezer-generator.cjs"
|
"lezer-generator": "src/lezer-generator.cjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@lezer/go": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@lezer/highlight": {
|
"node_modules/@lezer/highlight": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||||
@@ -1562,6 +1667,17 @@
|
|||||||
"@lezer/lr": "^1.0.0"
|
"@lezer/lr": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@lezer/java": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@lezer/javascript": {
|
"node_modules/@lezer/javascript": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||||
@@ -1603,6 +1719,28 @@
|
|||||||
"@lezer/highlight": "^1.0.0"
|
"@lezer/highlight": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@lezer/php": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/python": {
|
||||||
|
"version": "1.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz",
|
||||||
|
"integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@lezer/xml": {
|
"node_modules/@lezer/xml": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
|
||||||
@@ -1614,6 +1752,17 @@
|
|||||||
"@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",
|
||||||
@@ -1636,12 +1785,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.25.2",
|
"version": "1.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
|
||||||
"integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==",
|
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/node-server": "^1.19.7",
|
"@hono/node-server": "^1.19.9",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
@@ -1649,14 +1798,15 @@
|
|||||||
"cross-spawn": "^7.0.5",
|
"cross-spawn": "^7.0.5",
|
||||||
"eventsource": "^3.0.2",
|
"eventsource": "^3.0.2",
|
||||||
"eventsource-parser": "^3.0.0",
|
"eventsource-parser": "^3.0.0",
|
||||||
"express": "^5.0.1",
|
"express": "^5.2.1",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^8.2.1",
|
||||||
"jose": "^6.1.1",
|
"hono": "^4.11.4",
|
||||||
|
"jose": "^6.1.3",
|
||||||
"json-schema-typed": "^8.0.2",
|
"json-schema-typed": "^8.0.2",
|
||||||
"pkce-challenge": "^5.0.0",
|
"pkce-challenge": "^5.0.0",
|
||||||
"raw-body": "^3.0.0",
|
"raw-body": "^3.0.0",
|
||||||
"zod": "^3.25 || ^4.0",
|
"zod": "^3.25 || ^4.0",
|
||||||
"zod-to-json-schema": "^3.25.0"
|
"zod-to-json-schema": "^3.25.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -1982,6 +2132,19 @@
|
|||||||
"node": ">=16.9"
|
"node": ">=16.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@readme/httpsnippet": {
|
||||||
|
"version": "11.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@readme/httpsnippet/-/httpsnippet-11.0.0.tgz",
|
||||||
|
"integrity": "sha512-XSyaAsJkZfmMO9R4WDlVJARZgd4wlImftSkMkKclidniXA1h6DTya9iTqJenQo9mHQLh3u6kAC3CDRaIV+LbLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"qs": "^6.11.2",
|
||||||
|
"stringify-object": "^3.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@replit/codemirror-emacs": {
|
"node_modules/@replit/codemirror-emacs": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz",
|
||||||
@@ -4005,6 +4168,10 @@
|
|||||||
"resolved": "plugins/filter-xpath",
|
"resolved": "plugins/filter-xpath",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@yaak/httpsnippet": {
|
||||||
|
"resolved": "plugins-external/httpsnippet",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@yaak/importer-curl": {
|
"node_modules/@yaak/importer-curl": {
|
||||||
"resolved": "plugins/importer-curl",
|
"resolved": "plugins/importer-curl",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -6826,10 +6993,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-rate-limit": {
|
"node_modules/express-rate-limit": {
|
||||||
"version": "7.5.1",
|
"version": "8.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
||||||
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
|
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "10.0.1"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
},
|
},
|
||||||
@@ -7368,6 +7538,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-own-enumerable-property-symbols": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/get-proto": {
|
"node_modules/get-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
@@ -7811,9 +7987,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hono": {
|
"node_modules/hono": {
|
||||||
"version": "4.11.3",
|
"version": "4.11.7",
|
||||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
|
||||||
"integrity": "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==",
|
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.9.0"
|
"node": ">=16.9.0"
|
||||||
@@ -8096,6 +8272,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||||
|
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ip-bigint": {
|
"node_modules/ip-bigint": {
|
||||||
"version": "7.3.0",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ip-bigint/-/ip-bigint-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ip-bigint/-/ip-bigint-7.3.0.tgz",
|
||||||
@@ -8477,6 +8662,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-obj": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-plain-obj": {
|
"node_modules/is-plain-obj": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||||
@@ -13737,6 +13931,29 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stringify-object": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"get-own-enumerable-property-symbols": "^3.0.0",
|
||||||
|
"is-obj": "^1.0.1",
|
||||||
|
"is-regexp": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stringify-object/node_modules/is-regexp": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
@@ -15721,7 +15938,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin-runtime-types": {
|
"packages/plugin-runtime-types": {
|
||||||
"name": "@yaakapp/api",
|
"name": "@yaakapp/api",
|
||||||
"version": "0.7.1",
|
"version": "0.8.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^24.0.13"
|
"@types/node": "^24.0.13"
|
||||||
},
|
},
|
||||||
@@ -15736,14 +15953,42 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"plugins-external/httpsnippet": {
|
||||||
|
"name": "@yaak/httpsnippet",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@readme/httpsnippet": "^11.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins-external/httpsnippet/node_modules/@types/node": {
|
||||||
|
"version": "22.19.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz",
|
||||||
|
"integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins-external/httpsnippet/node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"plugins-external/mcp-server": {
|
"plugins-external/mcp-server": {
|
||||||
"name": "@yaak/mcp-server",
|
"name": "@yaak/mcp-server",
|
||||||
"version": "0.1.7",
|
"version": "0.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@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.26.0",
|
||||||
"hono": "^4.11.3",
|
"hono": "^4.11.7",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -15984,7 +16229,9 @@
|
|||||||
"@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",
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -12,6 +12,7 @@
|
|||||||
"packages/plugin-runtime-types",
|
"packages/plugin-runtime-types",
|
||||||
"plugins-external/mcp-server",
|
"plugins-external/mcp-server",
|
||||||
"plugins-external/template-function-faker",
|
"plugins-external/template-function-faker",
|
||||||
|
"plugins-external/httpsnippet",
|
||||||
"plugins/action-copy-curl",
|
"plugins/action-copy-curl",
|
||||||
"plugins/action-copy-grpcurl",
|
"plugins/action-copy-grpcurl",
|
||||||
"plugins/action-send-folder",
|
"plugins/action-send-folder",
|
||||||
@@ -95,7 +96,7 @@
|
|||||||
"js-yaml": "^4.1.1"
|
"js-yaml": "^4.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.10",
|
"@biomejs/biome": "^2.3.13",
|
||||||
"@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",
|
||||||
@@ -104,5 +105,12 @@
|
|||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/lang-go": "^6.0.1",
|
||||||
|
"@codemirror/lang-java": "^6.0.2",
|
||||||
|
"@codemirror/lang-php": "^6.0.2",
|
||||||
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@codemirror/legacy-modes": "^6.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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://feedback.yaak.app/help/articles/6911763-plugins-quick-start)
|
the [Quick Start Guide](https://yaak.app/docs/plugin-development/plugins-quick-start)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaakapp/api",
|
"name": "@yaakapp/api",
|
||||||
"version": "0.7.1",
|
"version": "0.8.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"api-client",
|
"api-client",
|
||||||
"insomnia-alternative",
|
"insomnia-alternative",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -12,6 +12,8 @@ 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, };
|
||||||
@@ -38,7 +40,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, 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, 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 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, };
|
||||||
|
|
||||||
@@ -47,7 +49,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, };
|
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 HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
@@ -77,6 +79,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, };
|
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 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, };
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import type {
|
import type {
|
||||||
FindHttpResponsesRequest,
|
FindHttpResponsesRequest,
|
||||||
FindHttpResponsesResponse,
|
FindHttpResponsesResponse,
|
||||||
|
FormInput,
|
||||||
GetCookieValueRequest,
|
GetCookieValueRequest,
|
||||||
GetCookieValueResponse,
|
GetCookieValueResponse,
|
||||||
GetHttpRequestByIdRequest,
|
GetHttpRequestByIdRequest,
|
||||||
GetHttpRequestByIdResponse,
|
GetHttpRequestByIdResponse,
|
||||||
|
JsonPrimitive,
|
||||||
ListCookieNamesResponse,
|
ListCookieNamesResponse,
|
||||||
ListFoldersRequest,
|
ListFoldersRequest,
|
||||||
ListFoldersResponse,
|
ListFoldersResponse,
|
||||||
ListHttpRequestsRequest,
|
ListHttpRequestsRequest,
|
||||||
ListHttpRequestsResponse,
|
ListHttpRequestsResponse,
|
||||||
OpenWindowRequest,
|
OpenWindowRequest,
|
||||||
|
PromptFormRequest,
|
||||||
|
PromptFormResponse,
|
||||||
PromptTextRequest,
|
PromptTextRequest,
|
||||||
PromptTextResponse,
|
PromptTextResponse,
|
||||||
RenderGrpcRequestRequest,
|
RenderGrpcRequestRequest,
|
||||||
@@ -23,8 +27,41 @@ import type {
|
|||||||
TemplateRenderRequest,
|
TemplateRenderRequest,
|
||||||
WorkspaceInfo,
|
WorkspaceInfo,
|
||||||
} from '../bindings/gen_events.ts';
|
} from '../bindings/gen_events.ts';
|
||||||
import type { HttpRequest } from '../bindings/gen_models.ts';
|
import type { Folder, HttpRequest } from '../bindings/gen_models.ts';
|
||||||
import type { JsonValue } from '../bindings/serde_json/JsonValue';
|
import type { JsonValue } from '../bindings/serde_json/JsonValue';
|
||||||
|
import type { MaybePromise } from '../helpers';
|
||||||
|
|
||||||
|
export type CallPromptFormDynamicArgs = {
|
||||||
|
values: { [key in string]?: JsonPrimitive };
|
||||||
|
};
|
||||||
|
|
||||||
|
type AddDynamicMethod<T> = {
|
||||||
|
dynamic?: (
|
||||||
|
ctx: Context,
|
||||||
|
args: CallPromptFormDynamicArgs,
|
||||||
|
) => MaybePromise<Partial<T> | null | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern
|
||||||
|
type AddDynamic<T> = T extends any
|
||||||
|
? T extends { inputs?: FormInput[] }
|
||||||
|
? Omit<T, 'inputs'> & {
|
||||||
|
inputs: Array<AddDynamic<FormInput>>;
|
||||||
|
dynamic?: (
|
||||||
|
ctx: Context,
|
||||||
|
args: CallPromptFormDynamicArgs,
|
||||||
|
) => MaybePromise<
|
||||||
|
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
: T & AddDynamicMethod<T>
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type DynamicPromptFormArg = AddDynamic<FormInput>;
|
||||||
|
|
||||||
|
type DynamicPromptFormRequest = Omit<PromptFormRequest, 'inputs'> & {
|
||||||
|
inputs: DynamicPromptFormArg[];
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkspaceHandle = Pick<WorkspaceInfo, 'id' | 'name'>;
|
export type WorkspaceHandle = Pick<WorkspaceInfo, 'id' | 'name'>;
|
||||||
|
|
||||||
@@ -37,6 +74,7 @@ export interface Context {
|
|||||||
};
|
};
|
||||||
prompt: {
|
prompt: {
|
||||||
text(args: PromptTextRequest): Promise<PromptTextResponse['value']>;
|
text(args: PromptTextRequest): Promise<PromptTextResponse['value']>;
|
||||||
|
form(args: DynamicPromptFormRequest): Promise<PromptFormResponse['values']>;
|
||||||
};
|
};
|
||||||
store: {
|
store: {
|
||||||
set<T>(key: string, value: T): Promise<void>;
|
set<T>(key: string, value: T): Promise<void>;
|
||||||
@@ -79,6 +117,15 @@ 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']>;
|
||||||
|
|||||||
@@ -2,21 +2,22 @@ import type { AuthenticationPlugin } from './AuthenticationPlugin';
|
|||||||
|
|
||||||
import type { Context } from './Context';
|
import type { Context } from './Context';
|
||||||
import type { FilterPlugin } from './FilterPlugin';
|
import type { FilterPlugin } from './FilterPlugin';
|
||||||
|
import type { FolderActionPlugin } from './FolderActionPlugin';
|
||||||
import type { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
|
import type { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
|
||||||
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
|
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
|
||||||
import type { WebsocketRequestActionPlugin } from './WebsocketRequestActionPlugin';
|
|
||||||
import type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
|
|
||||||
import type { FolderActionPlugin } from './FolderActionPlugin';
|
|
||||||
import type { ImporterPlugin } from './ImporterPlugin';
|
import type { ImporterPlugin } from './ImporterPlugin';
|
||||||
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
|
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
|
||||||
import type { ThemePlugin } from './ThemePlugin';
|
import type { ThemePlugin } from './ThemePlugin';
|
||||||
|
import type { WebsocketRequestActionPlugin } from './WebsocketRequestActionPlugin';
|
||||||
|
import type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
|
||||||
|
|
||||||
export type { Context };
|
export type { Context };
|
||||||
export type { DynamicTemplateFunctionArg } from './TemplateFunctionPlugin';
|
|
||||||
export type { DynamicAuthenticationArg } from './AuthenticationPlugin';
|
export type { DynamicAuthenticationArg } from './AuthenticationPlugin';
|
||||||
|
export type { CallPromptFormDynamicArgs, DynamicPromptFormArg } from './Context';
|
||||||
|
export type { DynamicTemplateFunctionArg } from './TemplateFunctionPlugin';
|
||||||
export type { TemplateFunctionPlugin };
|
export type { TemplateFunctionPlugin };
|
||||||
export type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
|
|
||||||
export type { FolderActionPlugin } from './FolderActionPlugin';
|
export type { FolderActionPlugin } from './FolderActionPlugin';
|
||||||
|
export type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The global structure of a Yaak plugin
|
* The global structure of a Yaak plugin
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import console from 'node:console';
|
import console from 'node:console';
|
||||||
import { type Stats, statSync, watch } from 'node:fs';
|
import { type Stats, statSync, watch } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import type { Context, PluginDefinition } from '@yaakapp/api';
|
import type {
|
||||||
|
CallPromptFormDynamicArgs,
|
||||||
|
Context,
|
||||||
|
DynamicPromptFormArg,
|
||||||
|
PluginDefinition,
|
||||||
|
} from '@yaakapp/api';
|
||||||
import {
|
import {
|
||||||
applyFormInputDefaults,
|
applyFormInputDefaults,
|
||||||
validateTemplateFunctionArgs,
|
validateTemplateFunctionArgs,
|
||||||
@@ -11,6 +16,8 @@ import type {
|
|||||||
DeleteKeyValueResponse,
|
DeleteKeyValueResponse,
|
||||||
DeleteModelResponse,
|
DeleteModelResponse,
|
||||||
FindHttpResponsesResponse,
|
FindHttpResponsesResponse,
|
||||||
|
Folder,
|
||||||
|
FormInput,
|
||||||
GetCookieValueRequest,
|
GetCookieValueRequest,
|
||||||
GetCookieValueResponse,
|
GetCookieValueResponse,
|
||||||
GetHttpRequestByIdResponse,
|
GetHttpRequestByIdResponse,
|
||||||
@@ -28,6 +35,7 @@ import type {
|
|||||||
ListHttpRequestsResponse,
|
ListHttpRequestsResponse,
|
||||||
ListWorkspacesResponse,
|
ListWorkspacesResponse,
|
||||||
PluginContext,
|
PluginContext,
|
||||||
|
PromptFormResponse,
|
||||||
PromptTextResponse,
|
PromptTextResponse,
|
||||||
RenderGrpcRequestResponse,
|
RenderGrpcRequestResponse,
|
||||||
RenderHttpRequestResponse,
|
RenderHttpRequestResponse,
|
||||||
@@ -53,6 +61,7 @@ export class PluginInstance {
|
|||||||
#mod: PluginDefinition;
|
#mod: PluginDefinition;
|
||||||
#pluginToAppEvents: EventChannel;
|
#pluginToAppEvents: EventChannel;
|
||||||
#appToPluginEvents: EventChannel;
|
#appToPluginEvents: EventChannel;
|
||||||
|
#pendingDynamicForms = new Map<string, DynamicPromptFormArg[]>();
|
||||||
|
|
||||||
constructor(workerData: PluginWorkerData, pluginEvents: EventChannel) {
|
constructor(workerData: PluginWorkerData, pluginEvents: EventChannel) {
|
||||||
this.#workerData = workerData;
|
this.#workerData = workerData;
|
||||||
@@ -104,6 +113,7 @@ export class PluginInstance {
|
|||||||
|
|
||||||
async terminate() {
|
async terminate() {
|
||||||
await this.#mod?.dispose?.();
|
await this.#mod?.dispose?.();
|
||||||
|
this.#pendingDynamicForms.clear();
|
||||||
this.#unimportModule();
|
this.#unimportModule();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +307,7 @@ export class PluginInstance {
|
|||||||
const replyPayload: InternalEventPayload = {
|
const replyPayload: InternalEventPayload = {
|
||||||
type: 'get_template_function_config_response',
|
type: 'get_template_function_config_response',
|
||||||
pluginRefId: this.#workerData.pluginRefId,
|
pluginRefId: this.#workerData.pluginRefId,
|
||||||
function: { ...fn, args: resolvedArgs },
|
function: { ...fn, args: stripDynamicCallbacks(resolvedArgs) },
|
||||||
};
|
};
|
||||||
this.#sendPayload(context, replyPayload, replyId);
|
this.#sendPayload(context, replyPayload, replyId);
|
||||||
return;
|
return;
|
||||||
@@ -324,7 +334,7 @@ export class PluginInstance {
|
|||||||
|
|
||||||
const replyPayload: InternalEventPayload = {
|
const replyPayload: InternalEventPayload = {
|
||||||
type: 'get_http_authentication_config_response',
|
type: 'get_http_authentication_config_response',
|
||||||
args: resolvedArgs,
|
args: stripDynamicCallbacks(resolvedArgs),
|
||||||
actions: resolvedActions,
|
actions: resolvedActions,
|
||||||
pluginRefId: this.#workerData.pluginRefId,
|
pluginRefId: this.#workerData.pluginRefId,
|
||||||
};
|
};
|
||||||
@@ -336,8 +346,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') {
|
||||||
auth.args = await applyDynamicFormInput(ctx, auth.args, payload);
|
const resolvedArgs = await applyDynamicFormInput(ctx, auth.args, payload);
|
||||||
payload.values = applyFormInputDefaults(auth.args, payload.values);
|
payload.values = applyFormInputDefaults(resolvedArgs, payload.values);
|
||||||
this.#sendPayload(
|
this.#sendPayload(
|
||||||
context,
|
context,
|
||||||
{
|
{
|
||||||
@@ -661,6 +671,69 @@ export class PluginInstance {
|
|||||||
});
|
});
|
||||||
return reply.value;
|
return reply.value;
|
||||||
},
|
},
|
||||||
|
form: async (args) => {
|
||||||
|
// Resolve dynamic callbacks on initial inputs using default values
|
||||||
|
const defaults = applyFormInputDefaults(args.inputs, {});
|
||||||
|
const callArgs: CallPromptFormDynamicArgs = { values: defaults };
|
||||||
|
const resolvedInputs = await applyDynamicFormInput(
|
||||||
|
this.#newCtx(context),
|
||||||
|
args.inputs,
|
||||||
|
callArgs,
|
||||||
|
);
|
||||||
|
const strippedInputs = stripDynamicCallbacks(resolvedInputs);
|
||||||
|
|
||||||
|
// Build the event manually so we can get the event ID for keying
|
||||||
|
const eventToSend = this.#buildEventToSend(
|
||||||
|
context,
|
||||||
|
{ type: 'prompt_form_request', ...args, inputs: strippedInputs },
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store original inputs (with dynamic callbacks) for later resolution
|
||||||
|
this.#pendingDynamicForms.set(eventToSend.id, args.inputs);
|
||||||
|
|
||||||
|
const reply = await new Promise<PromptFormResponse>((resolve) => {
|
||||||
|
const cb = (event: InternalEvent) => {
|
||||||
|
if (event.replyId !== eventToSend.id) return;
|
||||||
|
|
||||||
|
if (event.payload.type === 'prompt_form_response') {
|
||||||
|
const { done, values } = event.payload as PromptFormResponse;
|
||||||
|
if (done) {
|
||||||
|
// Final response — resolve the promise and clean up
|
||||||
|
this.#appToPluginEvents.unlisten(cb);
|
||||||
|
this.#pendingDynamicForms.delete(eventToSend.id);
|
||||||
|
resolve({ values } as PromptFormResponse);
|
||||||
|
} else {
|
||||||
|
// Intermediate value change — resolve dynamic inputs and send back
|
||||||
|
// Skip empty values (fired on initial mount before user interaction)
|
||||||
|
const storedInputs = this.#pendingDynamicForms.get(eventToSend.id);
|
||||||
|
if (storedInputs && values && Object.keys(values).length > 0) {
|
||||||
|
const ctx = this.#newCtx(context);
|
||||||
|
const callArgs: CallPromptFormDynamicArgs = { values };
|
||||||
|
applyDynamicFormInput(ctx, storedInputs, callArgs)
|
||||||
|
.then((resolvedInputs) => {
|
||||||
|
const stripped = stripDynamicCallbacks(resolvedInputs);
|
||||||
|
this.#sendPayload(
|
||||||
|
context,
|
||||||
|
{ type: 'prompt_form_request', ...args, inputs: stripped },
|
||||||
|
eventToSend.id,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Failed to resolve dynamic form inputs', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.#appToPluginEvents.listen(cb);
|
||||||
|
|
||||||
|
// Send the initial event after we start listening (to prevent race)
|
||||||
|
this.#sendEvent(eventToSend);
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.values;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
httpResponse: {
|
httpResponse: {
|
||||||
find: async (args) => {
|
find: async (args) => {
|
||||||
@@ -774,6 +847,44 @@ 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) => {
|
||||||
@@ -859,6 +970,17 @@ export class PluginInstance {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripDynamicCallbacks(inputs: { dynamic?: unknown }[]): FormInput[] {
|
||||||
|
return inputs.map((input) => {
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: stripping dynamic from union type
|
||||||
|
const { dynamic, ...rest } = input as any;
|
||||||
|
if ('inputs' in rest && Array.isArray(rest.inputs)) {
|
||||||
|
rest.inputs = stripDynamicCallbacks(rest.inputs);
|
||||||
|
}
|
||||||
|
return rest as FormInput;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function genId(len = 5): string {
|
function genId(len = 5): string {
|
||||||
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
let id = '';
|
let id = '';
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
import type { Context, DynamicAuthenticationArg, DynamicTemplateFunctionArg } from '@yaakapp/api';
|
import type {
|
||||||
|
CallPromptFormDynamicArgs,
|
||||||
|
Context,
|
||||||
|
DynamicAuthenticationArg,
|
||||||
|
DynamicPromptFormArg,
|
||||||
|
DynamicTemplateFunctionArg,
|
||||||
|
} from '@yaakapp/api';
|
||||||
import type {
|
import type {
|
||||||
CallHttpAuthenticationActionArgs,
|
CallHttpAuthenticationActionArgs,
|
||||||
CallTemplateFunctionArgs,
|
CallTemplateFunctionArgs,
|
||||||
} from '@yaakapp-internal/plugins';
|
} from '@yaakapp-internal/plugins';
|
||||||
|
|
||||||
|
type AnyDynamicArg = DynamicTemplateFunctionArg | DynamicAuthenticationArg | DynamicPromptFormArg;
|
||||||
|
type AnyCallArgs =
|
||||||
|
| CallTemplateFunctionArgs
|
||||||
|
| CallHttpAuthenticationActionArgs
|
||||||
|
| CallPromptFormDynamicArgs;
|
||||||
|
|
||||||
export async function applyDynamicFormInput(
|
export async function applyDynamicFormInput(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
args: DynamicTemplateFunctionArg[],
|
args: DynamicTemplateFunctionArg[],
|
||||||
@@ -18,30 +30,40 @@ export async function applyDynamicFormInput(
|
|||||||
|
|
||||||
export async function applyDynamicFormInput(
|
export async function applyDynamicFormInput(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
args: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[],
|
args: DynamicPromptFormArg[],
|
||||||
callArgs: CallTemplateFunctionArgs | CallHttpAuthenticationActionArgs,
|
callArgs: CallPromptFormDynamicArgs,
|
||||||
): Promise<(DynamicTemplateFunctionArg | DynamicAuthenticationArg)[]> {
|
): Promise<DynamicPromptFormArg[]>;
|
||||||
const resolvedArgs: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[] = [];
|
|
||||||
|
export async function applyDynamicFormInput(
|
||||||
|
ctx: Context,
|
||||||
|
args: AnyDynamicArg[],
|
||||||
|
callArgs: AnyCallArgs,
|
||||||
|
): Promise<AnyDynamicArg[]> {
|
||||||
|
const resolvedArgs: AnyDynamicArg[] = [];
|
||||||
for (const { dynamic, ...arg } of args) {
|
for (const { dynamic, ...arg } of args) {
|
||||||
const dynamicResult =
|
const dynamicResult =
|
||||||
typeof dynamic === 'function'
|
typeof dynamic === 'function'
|
||||||
? await dynamic(
|
? await dynamic(
|
||||||
ctx,
|
ctx,
|
||||||
callArgs as CallTemplateFunctionArgs & CallHttpAuthenticationActionArgs,
|
callArgs as CallTemplateFunctionArgs &
|
||||||
|
CallHttpAuthenticationActionArgs &
|
||||||
|
CallPromptFormDynamicArgs,
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const newArg = {
|
const newArg = {
|
||||||
...arg,
|
...arg,
|
||||||
...dynamicResult,
|
...dynamicResult,
|
||||||
} as DynamicTemplateFunctionArg | DynamicAuthenticationArg;
|
} as AnyDynamicArg;
|
||||||
|
|
||||||
if ('inputs' in newArg && Array.isArray(newArg.inputs)) {
|
if ('inputs' in newArg && Array.isArray(newArg.inputs)) {
|
||||||
try {
|
try {
|
||||||
newArg.inputs = await applyDynamicFormInput(
|
newArg.inputs = await applyDynamicFormInput(
|
||||||
ctx,
|
ctx,
|
||||||
newArg.inputs as DynamicTemplateFunctionArg[],
|
newArg.inputs as DynamicTemplateFunctionArg[],
|
||||||
callArgs as CallTemplateFunctionArgs & CallHttpAuthenticationActionArgs,
|
callArgs as CallTemplateFunctionArgs &
|
||||||
|
CallHttpAuthenticationActionArgs &
|
||||||
|
CallPromptFormDynamicArgs,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to apply dynamic form input', e);
|
console.error('Failed to apply dynamic form input', e);
|
||||||
|
|||||||
45
plugins-external/httpsnippet/README.md
Normal file
45
plugins-external/httpsnippet/README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Yaak HTTP Snippet Plugin
|
||||||
|
|
||||||
|
Generate code snippets for HTTP requests in various languages and frameworks,
|
||||||
|
powered by [@readme/httpsnippet](https://github.com/readmeio/httpsnippet).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Right-click any HTTP request (or use the `...` menu) and select **Generate Code Snippet**.
|
||||||
|
A dialog lets you pick a language and library, with a live preview of the generated code.
|
||||||
|
Click **Copy to Clipboard** to copy the snippet. Your language and library selections are
|
||||||
|
remembered for next time.
|
||||||
|
|
||||||
|
## Supported Languages
|
||||||
|
|
||||||
|
Each language supports one or more libraries:
|
||||||
|
|
||||||
|
| Language | Libraries |
|
||||||
|
|---|---|
|
||||||
|
| C | libcurl |
|
||||||
|
| Clojure | clj-http |
|
||||||
|
| C# | HttpClient, RestSharp |
|
||||||
|
| Go | Native |
|
||||||
|
| HTTP | HTTP/1.1 |
|
||||||
|
| Java | AsyncHttp, NetHttp, OkHttp, Unirest |
|
||||||
|
| JavaScript | Axios, fetch, jQuery, XHR |
|
||||||
|
| Kotlin | OkHttp |
|
||||||
|
| Node.js | Axios, fetch, HTTP, Request, Unirest |
|
||||||
|
| Objective-C | NSURLSession |
|
||||||
|
| OCaml | CoHTTP |
|
||||||
|
| PHP | cURL, Guzzle, HTTP v1, HTTP v2 |
|
||||||
|
| PowerShell | Invoke-WebRequest, RestMethod |
|
||||||
|
| Python | http.client, Requests |
|
||||||
|
| R | httr |
|
||||||
|
| Ruby | Native |
|
||||||
|
| Shell | cURL, HTTPie, Wget |
|
||||||
|
| Swift | URLSession |
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Renders template variables before generating snippets, so the output reflects real values
|
||||||
|
- Supports all body types: JSON, form-urlencoded, multipart, GraphQL, and raw text
|
||||||
|
- Includes authentication headers (Basic, Bearer, and API Key)
|
||||||
|
- Includes query parameters and custom headers
|
||||||
24
plugins-external/httpsnippet/package.json
Normal file
24
plugins-external/httpsnippet/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@yaak/httpsnippet",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.3",
|
||||||
|
"displayName": "HTTP Snippet",
|
||||||
|
"description": "Generate code snippets for HTTP requests in various languages and frameworks",
|
||||||
|
"minYaakVersion": "2026.2.0-beta.10",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mountain-loop/yaak.git",
|
||||||
|
"directory": "plugins-external/httpsnippet"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "yaakcli build",
|
||||||
|
"dev": "yaakcli dev"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@readme/httpsnippet": "^11.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
314
plugins-external/httpsnippet/src/index.ts
Normal file
314
plugins-external/httpsnippet/src/index.ts
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
import { availableTargets, type HarRequest, HTTPSnippet } from '@readme/httpsnippet';
|
||||||
|
import type { EditorLanguage, HttpRequest, PluginDefinition } from '@yaakapp/api';
|
||||||
|
|
||||||
|
// Get all available targets and build select options
|
||||||
|
const targets = availableTargets();
|
||||||
|
|
||||||
|
// Targets to exclude from the language list
|
||||||
|
const excludedTargets = new Set(['json']);
|
||||||
|
|
||||||
|
// Build language (target) options
|
||||||
|
const languageOptions = targets
|
||||||
|
.filter((target) => !excludedTargets.has(target.key))
|
||||||
|
.map((target) => ({
|
||||||
|
label: target.title,
|
||||||
|
value: target.key,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Preferred clients per target (shown first in the list)
|
||||||
|
const preferredClients: Record<string, string> = {
|
||||||
|
javascript: 'fetch',
|
||||||
|
node: 'fetch',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get client options for a given target key
|
||||||
|
function getClientOptions(targetKey: string) {
|
||||||
|
const target = targets.find((t) => t.key === targetKey);
|
||||||
|
if (!target) return [];
|
||||||
|
const preferred = preferredClients[targetKey];
|
||||||
|
return target.clients
|
||||||
|
.map((client) => ({
|
||||||
|
label: client.title,
|
||||||
|
value: client.key,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.value === preferred) return -1;
|
||||||
|
if (b.value === preferred) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get default client for a target
|
||||||
|
function getDefaultClient(targetKey: string): string {
|
||||||
|
const options = getClientOptions(targetKey);
|
||||||
|
return options[0]?.value ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
const defaultTarget = 'javascript';
|
||||||
|
|
||||||
|
// Map httpsnippet target key to editor language for syntax highlighting
|
||||||
|
const editorLanguageMap: Record<string, EditorLanguage> = {
|
||||||
|
c: 'c',
|
||||||
|
clojure: 'clojure',
|
||||||
|
csharp: 'csharp',
|
||||||
|
go: 'go',
|
||||||
|
http: 'http',
|
||||||
|
java: 'java',
|
||||||
|
javascript: 'javascript',
|
||||||
|
kotlin: 'kotlin',
|
||||||
|
node: 'javascript',
|
||||||
|
objc: 'objective_c',
|
||||||
|
ocaml: 'ocaml',
|
||||||
|
php: 'php',
|
||||||
|
powershell: 'powershell',
|
||||||
|
python: 'python',
|
||||||
|
r: 'r',
|
||||||
|
ruby: 'ruby',
|
||||||
|
shell: 'shell',
|
||||||
|
swift: 'swift',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getEditorLanguage(targetKey: string): EditorLanguage {
|
||||||
|
return editorLanguageMap[targetKey] ?? 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Yaak HttpRequest to HAR format
|
||||||
|
function toHarRequest(request: Partial<HttpRequest>) {
|
||||||
|
// Build URL with query parameters
|
||||||
|
let finalUrl = request.url || '';
|
||||||
|
const urlParams = (request.urlParameters ?? []).filter((p) => p.enabled !== false && !!p.name);
|
||||||
|
if (urlParams.length > 0) {
|
||||||
|
const [base, hash] = finalUrl.split('#');
|
||||||
|
const separator = base?.includes('?') ? '&' : '?';
|
||||||
|
const queryString = urlParams
|
||||||
|
.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`)
|
||||||
|
.join('&');
|
||||||
|
finalUrl = base + separator + queryString + (hash ? `#${hash}` : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build headers array
|
||||||
|
const headers: Array<{ name: string; value: string }> = (request.headers ?? [])
|
||||||
|
.filter((h) => h.enabled !== false && !!h.name)
|
||||||
|
.map((h) => ({ name: h.name, value: h.value }));
|
||||||
|
|
||||||
|
// Handle authentication
|
||||||
|
if (request.authentication?.disabled !== true) {
|
||||||
|
if (request.authenticationType === 'basic') {
|
||||||
|
const credentials = btoa(
|
||||||
|
`${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`,
|
||||||
|
);
|
||||||
|
headers.push({ name: 'Authorization', value: `Basic ${credentials}` });
|
||||||
|
} else if (request.authenticationType === 'bearer') {
|
||||||
|
const prefix = request.authentication?.prefix ?? 'Bearer';
|
||||||
|
const token = request.authentication?.token ?? '';
|
||||||
|
headers.push({ name: 'Authorization', value: `${prefix} ${token}`.trim() });
|
||||||
|
} else if (request.authenticationType === 'apikey') {
|
||||||
|
if (request.authentication?.location === 'header') {
|
||||||
|
headers.push({
|
||||||
|
name: request.authentication?.key ?? 'X-Api-Key',
|
||||||
|
value: request.authentication?.value ?? '',
|
||||||
|
});
|
||||||
|
} else if (request.authentication?.location === 'query') {
|
||||||
|
const sep = finalUrl.includes('?') ? '&' : '?';
|
||||||
|
finalUrl = [
|
||||||
|
finalUrl,
|
||||||
|
sep,
|
||||||
|
encodeURIComponent(request.authentication?.key ?? 'token'),
|
||||||
|
'=',
|
||||||
|
encodeURIComponent(request.authentication?.value ?? ''),
|
||||||
|
].join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build HAR request object
|
||||||
|
const har: Record<string, unknown> = {
|
||||||
|
method: request.method || 'GET',
|
||||||
|
url: finalUrl,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle request body
|
||||||
|
const bodyType = request.bodyType ?? 'none';
|
||||||
|
if (bodyType !== 'none' && request.body) {
|
||||||
|
if (bodyType === 'application/x-www-form-urlencoded' && Array.isArray(request.body.form)) {
|
||||||
|
const params = request.body.form
|
||||||
|
.filter((p: { enabled?: boolean; name?: string }) => p.enabled !== false && !!p.name)
|
||||||
|
.map((p: { name: string; value: string }) => ({ name: p.name, value: p.value }));
|
||||||
|
har.postData = {
|
||||||
|
mimeType: 'application/x-www-form-urlencoded',
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
} else if (bodyType === 'multipart/form-data' && Array.isArray(request.body.form)) {
|
||||||
|
const params = request.body.form
|
||||||
|
.filter((p: { enabled?: boolean; name?: string }) => p.enabled !== false && !!p.name)
|
||||||
|
.map((p: { name: string; value: string; file?: string; contentType?: string }) => {
|
||||||
|
const param: Record<string, string> = { name: p.name, value: p.value || '' };
|
||||||
|
if (p.file) param.fileName = p.file;
|
||||||
|
if (p.contentType) param.contentType = p.contentType;
|
||||||
|
return param;
|
||||||
|
});
|
||||||
|
har.postData = {
|
||||||
|
mimeType: 'multipart/form-data',
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
} else if (bodyType === 'graphql' && typeof request.body.query === 'string') {
|
||||||
|
const body = {
|
||||||
|
query: request.body.query || '',
|
||||||
|
variables: maybeParseJSON(request.body.variables, undefined),
|
||||||
|
};
|
||||||
|
har.postData = {
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(body),
|
||||||
|
};
|
||||||
|
} else if (typeof request.body.text === 'string') {
|
||||||
|
har.postData = {
|
||||||
|
mimeType: bodyType,
|
||||||
|
text: request.body.text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return har;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeParseJSON<T>(v: unknown, fallback: T): T | unknown {
|
||||||
|
if (typeof v !== 'string') return fallback;
|
||||||
|
try {
|
||||||
|
return JSON.parse(v);
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const plugin: PluginDefinition = {
|
||||||
|
httpRequestActions: [
|
||||||
|
{
|
||||||
|
label: 'Generate Code Snippet',
|
||||||
|
icon: 'copy',
|
||||||
|
async onSelect(ctx, args) {
|
||||||
|
// Render the request with variables resolved
|
||||||
|
const renderedRequest = await ctx.httpRequest.render({
|
||||||
|
httpRequest: args.httpRequest,
|
||||||
|
purpose: 'send',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to HAR format
|
||||||
|
const harRequest = toHarRequest(renderedRequest) as HarRequest;
|
||||||
|
|
||||||
|
// Get previously selected language or use defaults
|
||||||
|
const storedTarget = await ctx.store.get<string>('selectedTarget');
|
||||||
|
const initialTarget = storedTarget || defaultTarget;
|
||||||
|
const storedClient = await ctx.store.get<string>(`selectedClient:${initialTarget}`);
|
||||||
|
const initialClient = storedClient || getDefaultClient(initialTarget);
|
||||||
|
|
||||||
|
// Create snippet generator
|
||||||
|
const snippet = new HTTPSnippet(harRequest);
|
||||||
|
const generateSnippet = (target: string, client: string): string => {
|
||||||
|
const result = snippet.convert(target as any, client);
|
||||||
|
return (Array.isArray(result) ? result.join('\n') : result || '').replace(/\r\n/g, '\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate initial code preview
|
||||||
|
let initialCode = '';
|
||||||
|
try {
|
||||||
|
initialCode = generateSnippet(initialTarget, initialClient);
|
||||||
|
} catch {
|
||||||
|
initialCode = '// Error generating snippet';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show dialog with language/library selectors and code preview
|
||||||
|
const result = await ctx.prompt.form({
|
||||||
|
id: 'httpsnippet',
|
||||||
|
title: 'Generate Code Snippet',
|
||||||
|
confirmText: 'Copy to Clipboard',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
size: 'md',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: 'h_stack',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'target',
|
||||||
|
label: 'Language',
|
||||||
|
defaultValue: initialTarget,
|
||||||
|
options: languageOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: `client-${initialTarget}`,
|
||||||
|
label: 'Library',
|
||||||
|
defaultValue: initialClient,
|
||||||
|
options: getClientOptions(initialTarget),
|
||||||
|
dynamic(_ctx, { values }) {
|
||||||
|
const targetKey = String(values.target || defaultTarget);
|
||||||
|
const options = getClientOptions(targetKey);
|
||||||
|
return {
|
||||||
|
name: `client-${targetKey}`,
|
||||||
|
options,
|
||||||
|
defaultValue: options[0]?.value ?? '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'editor',
|
||||||
|
name: 'code',
|
||||||
|
label: 'Preview',
|
||||||
|
language: getEditorLanguage(initialTarget),
|
||||||
|
defaultValue: initialCode,
|
||||||
|
readOnly: true,
|
||||||
|
rows: 15,
|
||||||
|
dynamic(_ctx, { values }) {
|
||||||
|
const targetKey = String(values.target || defaultTarget);
|
||||||
|
const clientKey = String(
|
||||||
|
values[`client-${targetKey}`] || getDefaultClient(targetKey),
|
||||||
|
);
|
||||||
|
let code: string;
|
||||||
|
try {
|
||||||
|
code = generateSnippet(targetKey, clientKey);
|
||||||
|
} catch {
|
||||||
|
code = '// Error generating snippet';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
defaultValue: code,
|
||||||
|
language: getEditorLanguage(targetKey),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
// Store the selected language and library for next time
|
||||||
|
const selectedTarget = String(result.target || initialTarget);
|
||||||
|
const selectedClient = String(
|
||||||
|
result[`client-${selectedTarget}`] || getDefaultClient(selectedTarget),
|
||||||
|
);
|
||||||
|
await ctx.store.set('selectedTarget', selectedTarget);
|
||||||
|
await ctx.store.set(`selectedClient:${selectedTarget}`, selectedClient);
|
||||||
|
|
||||||
|
// Generate snippet for the selected language
|
||||||
|
try {
|
||||||
|
const codeText = generateSnippet(selectedTarget, selectedClient);
|
||||||
|
await ctx.clipboard.copyText(codeText);
|
||||||
|
await ctx.toast.show({
|
||||||
|
message: 'Code snippet copied to clipboard',
|
||||||
|
icon: 'copy',
|
||||||
|
color: 'success',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
await ctx.toast.show({
|
||||||
|
message: `Failed to generate snippet: ${err}`,
|
||||||
|
icon: 'alert_triangle',
|
||||||
|
color: 'danger',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "@yaak/mcp-server",
|
"name": "@yaak/mcp-server",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.7",
|
"version": "0.2.1",
|
||||||
"displayName": "MCP Server",
|
"displayName": "MCP Server",
|
||||||
"description": "Expose Yaak functionality via Model Context Protocol",
|
"description": "Expose Yaak functionality via Model Context Protocol",
|
||||||
"minYaakVersion": "2025.10.0-beta.6",
|
"minYaakVersion": "2026.1.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/mountain-loop/yaak.git",
|
"url": "https://github.com/mountain-loop/yaak.git",
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@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.26.0",
|
||||||
"hono": "^4.11.3",
|
"hono": "^4.11.7",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ 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(
|
||||||
@@ -10,10 +16,7 @@ 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: z
|
workspaceId: workspaceIdSchema,
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe('Workspace ID (required if multiple workspaces are open)'),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ workspaceId }) => {
|
async ({ workspaceId }) => {
|
||||||
@@ -30,4 +33,116 @@ 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,6 +2,15 @@ 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(
|
||||||
@@ -10,10 +19,7 @@ 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: z
|
workspaceId: workspaceIdSchema,
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe('Workspace ID (required if multiple workspaces are open)'),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ workspaceId }) => {
|
async ({ workspaceId }) => {
|
||||||
@@ -38,10 +44,7 @@ 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: z
|
workspaceId: workspaceIdSchema,
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe('Workspace ID (required if multiple workspaces are open)'),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ id, workspaceId }) => {
|
async ({ id, workspaceId }) => {
|
||||||
@@ -67,10 +70,7 @@ 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: z
|
workspaceId: workspaceIdSchema,
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe('Workspace ID (required if multiple workspaces are open)'),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ id, workspaceId }) => {
|
async ({ id, workspaceId }) => {
|
||||||
@@ -99,10 +99,7 @@ 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: z
|
workspaceId: workspaceIdSchema,
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe('Workspace ID (required if multiple workspaces are open)'),
|
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -111,62 +108,12 @@ 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: z
|
headers: headersSchema.describe('Request headers'),
|
||||||
.array(
|
urlParameters: urlParametersSchema,
|
||||||
z.object({
|
bodyType: bodyTypeSchema,
|
||||||
name: z.string(),
|
body: bodySchema,
|
||||||
value: z.string(),
|
authenticationType: authenticationTypeSchema,
|
||||||
enabled: z.boolean().default(true),
|
authentication: authenticationSchema,
|
||||||
}),
|
|
||||||
)
|
|
||||||
.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 }) => {
|
||||||
@@ -194,68 +141,18 @@ 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: z.string().describe('Workspace ID'),
|
workspaceId: workspaceIdSchema,
|
||||||
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: z
|
headers: headersSchema.describe('Request headers'),
|
||||||
.array(
|
urlParameters: urlParametersSchema,
|
||||||
z.object({
|
bodyType: bodyTypeSchema,
|
||||||
name: z.string(),
|
body: bodySchema,
|
||||||
value: z.string(),
|
authenticationType: authenticationTypeSchema,
|
||||||
enabled: z.boolean().default(true),
|
authentication: authenticationSchema,
|
||||||
}),
|
|
||||||
)
|
|
||||||
.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 }) => {
|
||||||
|
|||||||
67
plugins-external/mcp-server/src/tools/schemas.ts
Normal file
67
plugins-external/mcp-server/src/tools/schemas.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
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": {}',
|
||||||
|
);
|
||||||
@@ -10,7 +10,7 @@ export const plugin: PluginDefinition = {
|
|||||||
async onSelect(ctx, args) {
|
async onSelect(ctx, args) {
|
||||||
const rendered_request = await ctx.httpRequest.render({
|
const rendered_request = await ctx.httpRequest.render({
|
||||||
httpRequest: args.httpRequest,
|
httpRequest: args.httpRequest,
|
||||||
purpose: 'preview',
|
purpose: 'send',
|
||||||
});
|
});
|
||||||
const data = await convertToCurl(rendered_request);
|
const data = await convertToCurl(rendered_request);
|
||||||
await ctx.clipboard.copyText(data);
|
await ctx.clipboard.copyText(data);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user