Compare commits

...

108 Commits

Author SHA1 Message Date
Mikhail Mamontov
ef1ba9b834 fix(gRPC): Cache descriptor pools to avoid re-reflection; add manual “Refresh Schema” to force re-fetch (#317) 2025-12-09 15:35:35 -08:00
Jake Oliver
846f4d9551 Update 1Password template to support the new Desktop authentication method (#316) 2025-12-09 14:50:08 -08:00
Gregory Schier
4780bfe41f Fix curl import: decode Unicode escape sequences in $'...' strings (#318)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-09 14:15:39 -08:00
Gregory Schier
d0d01b3897 Update license check to use status instead of type 2025-12-09 14:12:13 -08:00
Gregory Schier
fc1e8baa23 Catch any 4XX error on refresh token failure
https://feedback.yaak.app/p/folders-oauth2-refresh-token-issue
2025-12-09 14:08:31 -08:00
Gregory Schier
d35116c494 Add license handling for expired licenses 2025-12-09 13:51:02 -08:00
gschier
1d257b365b Deploying to main from @ mountain-loop/yaak@1076d57e8a 🚀 2025-12-09 18:15:05 +00:00
Gregory Schier
1076d57e8a Remove unused funding model entries from FUNDING.yml 2025-12-09 10:14:19 -08:00
Gregory Schier
1c93d5775f Shorter titles when using native titlebar 2025-12-06 06:47:34 -08:00
Gregory Schier
7b78fac24e Fix native titlebar. Get menu ready for native Mac menus 2025-12-05 15:14:13 -08:00
Gregory Schier
6534b3f622 Debug Window 2025-12-05 17:37:54 -08:00
Gregory Schier
daba21fbca Couple small fixes for Windows 2025-12-05 10:18:21 -08:00
Gregory Schier
3b99ea1cad Try fixing titlebar thing for Windows 2025-12-05 10:03:54 -08:00
Gregory Schier
937d7aa72a Fix Git invokation on Windows (#313) 2025-12-05 09:22:34 -08:00
Gregory Schier
5bf7278479 Add setting to use native window titlebar (#312) 2025-12-05 09:15:48 -08:00
Gregory Schier
095af8cf4b Refresh query when plugins reload in useTemplateFunctionConfig hook 2025-12-02 08:07:53 -08:00
Gregory Schier
e1c1ecc34d Try fix quotes for Windows 2025-12-02 05:45:01 -08:00
Gregory Schier
6e4c167bfd Add beta warning banners to OAuth 1.0 and NTLM plugins 2025-12-01 09:26:55 -08:00
Gregory Schier
25d8357471 Merge remote-tracking branch 'origin/main' 2025-12-01 07:56:12 -08:00
Gregory Schier
8e00693af3 Fix JSON lint error location 2025-12-01 07:54:02 -08:00
gschier
079da67889 Deploying to main from @ mountain-loop/yaak@9ed3dacd28 🚀 2025-12-01 15:39:07 +00:00
Gregory Schier
9ed3dacd28 Fix dropdown menu keys
https://feedback.yaak.app/p/response-history-bug
2025-12-01 06:32:04 -08:00
Gregory Schier
ba6e64ef37 Explicit targets 2025-11-28 09:07:01 -08:00
Gregory Schier
d7a68c2d53 Fix Rust version 2025-11-28 09:04:59 -08:00
Gregory Schier
e8e1d9246e Try using window-latest for ARM build 2025-11-28 08:53:44 -08:00
Gregory Schier
a7574f2e5a Fix vendor scripts for arm 2025-11-28 08:16:24 -08:00
Gregory Schier
69f9661813 Upgrade @yaakapp/cli again 2025-11-28 07:49:54 -08:00
Gregory Schier
302b0a4747 Upgrade @yaakapp/cli 2025-11-28 07:10:27 -08:00
Gregory Schier
07f4696a2c Install wasm-pack from cargo for Windows ARM compat 2025-11-28 06:04:48 -08:00
Gregory Schier
2ddb1096df Try building Windows ARM too 2025-11-27 15:41:17 -08:00
Gregory Schier
0149355d66 Try fix xdg-mime missing on ARM 2025-11-27 14:10:13 -08:00
Gregory Schier
2e7749a883 Fix? 2025-11-27 13:53:50 -08:00
Gregory Schier
cd0e8c0bc2 Try arm linux builds 2025-11-27 13:47:58 -08:00
Gregory Schier
64e4e352a0 Fix package order 2025-11-27 13:46:30 -08:00
Gregory Schier
b512365f5a Oops, remove body too 2025-11-27 13:31:16 -08:00
Gregory Schier
13c84e3fb6 Add doNotEncodePath to aws4 plugin 2025-11-27 13:29:59 -08:00
Gregory Schier
8d1b17cac1 Add previewArgs support for template functions and enhance validation logic for form inputs 2025-11-27 12:55:39 -08:00
Gregory Schier
0c7034eefc Fix text cutting off on <Select/> 2025-11-27 06:22:29 -08:00
Gregory Schier
3ec236462f Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src-web/components/MarkdownEditor.tsx
2025-11-26 14:12:46 -08:00
Gregory Schier
1b5ac6fc89 Some fixes 2025-11-26 14:10:02 -08:00
Gregory Schier
d356bac135 Fix icon and segmented control and dev icon 2025-11-26 11:22:10 -08:00
Gregory Schier
8a80e7b833 Add ability to conditionally disable auth (#309) 2025-11-26 11:05:07 -08:00
Gregory Schier
a1ae065d37 PR feedback 2025-11-26 11:01:13 -08:00
Gregory Schier
79dd50474d Conditionally disable auth 2025-11-26 10:30:16 -08:00
Gregory Schier
dfa6f1c5b4 Fix collapse all folders 2025-11-26 07:19:12 -08:00
Gregory Schier
2edd33b6e3 TSC check and set editor key 2025-11-26 06:25:02 -08:00
Gregory Schier
8b851d4685 Fix better 2025-11-25 09:46:02 -08:00
Gregory Schier
20e1b5c00e Fix dialog and invalid variable style 2025-11-25 09:37:19 -08:00
Gregory Schier
c4ab2965f7 Scrollable tables, specify multi-part filename, fix required prop in prompt, better tab padding 2025-11-25 08:45:33 -08:00
Gregory Schier
0cad8f69e2 Fix imports 2025-11-24 08:55:55 -08:00
Zhizhen He
a8402824ed Fix useState (#307) 2025-11-24 08:55:37 -08:00
Gregory Schier
acf9458616 Enable updater artifacts creation in Tauri release configuration 2025-11-23 09:26:34 -08:00
Gregory Schier
0a58f7dfc8 Add back vendor-node 2025-11-23 08:58:57 -08:00
Gregory Schier
6e05d85ae4 Move icon definition to make Windows tests pass 2025-11-23 08:55:47 -08:00
Gregory Schier
a04db485de Run CI on main 2025-11-23 08:46:26 -08:00
Gregory Schier
d7043e75d6 Biome tweaks 2025-11-23 08:45:15 -08:00
Gregory Schier
ec3e2e16a9 Switch to BiomeJS (#306) 2025-11-23 08:38:13 -08:00
Gregory Schier
2bac610efe Official 1Password Template Function (#305) 2025-11-22 06:08:13 -08:00
Gregory Schier
43a7132014 Support Any type for gRPC reflection 2025-11-21 13:15:09 -08:00
Gregory Schier
bddc6e35a0 Bail out if sync directory is deleted 2025-11-19 13:42:48 -08:00
Gregory Schier
0e98a3e498 Fix prompt default value 2025-11-19 10:10:13 -08:00
Gregory Schier
17b6c945e6 Cap max height on template function dialog 2025-11-19 09:39:43 -08:00
Gregory Schier
474e761eb7 Fix prompt 2025-11-19 09:21:59 -08:00
Gregory Schier
1fbf9e50c4 Better keychain function descriptions 2025-11-19 09:15:50 -08:00
Gregory Schier
6863decd8e Fix sidebar scroll into view 2025-11-18 13:30:37 -08:00
Gregory Schier
569e506f32 Try rel imports 2025-11-18 09:09:56 -08:00
Gregory Schier
6d7a08758f Try rel imports 2025-11-18 09:09:39 -08:00
Gregory Schier
20dfd50a7d Try without && 2025-11-18 09:08:16 -08:00
Gregory Schier
d747eb5e45 Try again 2025-11-18 09:02:10 -08:00
Gregory Schier
81fca7c54f Reverse order 2025-11-18 08:55:46 -08:00
Gregory Schier
5465efea84 Don't build in vendor-plugins 2025-11-18 08:53:55 -08:00
Gregory Schier
96a3630725 Fix package build order 2025-11-18 08:48:58 -08:00
Gregory Schier
f1b6c89186 Fix package types? 2025-11-18 08:39:07 -08:00
Gregory Schier
9c52652a5e Move a bunch of git ops to use the git binary (#302) 2025-11-17 15:22:39 -08:00
Gregory Schier
84219571e8 Improved prompt function add add ctx.* functions (#301) 2025-11-15 08:19:58 -08:00
iammordaty
7ced183b11 Change wording from "Show sidebar" to "Toggle sidebar" (#300) 2025-11-13 13:40:51 -08:00
Gregor Majcen
593a7ab7e5 Add an option to allow jsonpath/xpath to return as array (#297)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-11-13 05:57:11 -08:00
Zhizhen He
a4c4663011 Merge pull request #298
* fix: replace unstable from_mins to stable from_secs
2025-11-12 07:01:58 -08:00
jzhangdev
5745a96106 Merge pull request #299
* Fix scroll bar layout in EventStreamViewer
2025-11-12 06:58:35 -08:00
Gregory Schier
5449e3c620 Add sidebar action to select the active request 2025-11-11 14:38:05 -08:00
Gregory Schier
7b6278405c Focus request/folder after creation 2025-11-11 14:11:43 -08:00
goldlinker
8164a61376 chore: make some documents clearer (#276) 2025-11-10 17:25:54 -08:00
Jeroen Van den Berghe
2e9f21f838 Convert Insomnia variables syntax in headers, parameters and form data (#291)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-11-10 17:24:30 -08:00
Gregory Schier
0d725b59bd Verify trusted-signing-cli version 2025-11-10 15:02:10 -08:00
Gregory Schier
632860c29b Try again 2025-11-10 14:58:54 -08:00
Gregory Schier
e1cf16f6e1 Try again 2025-11-10 14:49:15 -08:00
Gregory Schier
47c9cfb295 Fix release? 2025-11-10 14:46:09 -08:00
Gregory Schier
6389fd3b8f Connection re-use for plugin networking and beta NTLM plugin (#295) 2025-11-10 14:41:49 -08:00
Gregory Schier
d318546d0c Back to vertical tabs in workspace settings 2025-11-10 06:21:26 -08:00
Gregory Schier
2f60b7b1f3 Switch trusted-signing-cli install method 2025-11-09 13:55:51 -08:00
Gregory Schier
75dc82570b Rename BadgeButton to PillButton 2025-11-09 08:18:26 -08:00
Gregory Schier
d7a7a64ec4 New "Triangle" theme 2025-11-09 07:55:31 -08:00
Gregory Schier
3aae1b52d1 Update commercial use trial wording 2025-11-09 07:19:05 -08:00
Gregory Schier
9eddf716e1 Update commercial use trial wording 2025-11-09 07:07:18 -08:00
Gregory Schier
554e632c19 Minor license handling tweaks 2025-11-09 06:01:03 -08:00
Gregory Schier
054916b7af JSON linting 2025-11-08 15:24:31 -08:00
Gregory Schier
f2a63087b0 Actually fix GraphQLEditor.tsx properly 2025-11-06 09:33:12 -08:00
Gregory Schier
6f0d4ad5e4 Fix GraphQL editor 2025-11-06 06:31:56 -08:00
Gregory Schier
cd3530f598 Dropdown to setup sync now opens the correct workspace settings tab 2025-11-06 05:13:18 -08:00
Gregory Schier
53aea914ac Don't drag tree item when editing
https://feedback.yaak.app/p/select-text-of-navbar-in-edit-mode
2025-11-06 05:10:23 -08:00
Gregory Schier
dc0c1decee Fix copy-curl with API key
https://feedback.yaak.app/p/copy-as-curl-bug-when-auth-use-api-key-with
2025-11-05 10:21:26 -08:00
Gregory Schier
32d56f2274 OAuth 1 Authentication Plugin (#292) 2025-11-05 10:12:48 -08:00
Gregory Schier
ef86c1d189 Recursively collapse during "coolapse all" 2025-11-05 10:12:10 -08:00
Gregory Schier
e264c50427 Show more resopnse header y height 2025-11-05 10:11:55 -08:00
Gregory Schier
f05ad62301 Fix zoom hotkey
https://feedback.yaak.app/p/zoom-in-not-working-on-linux-mint
2025-11-05 10:11:46 -08:00
Gregory Schier
0a6228bf16 Fix Input ref timing, PairEditor initialization, and environment variable focus 2025-11-04 14:04:12 -08:00
Gregory Schier
fa3a0b57f9 Fix Editor.tsx wonkiness 2025-11-04 13:44:18 -08:00
Gregory Schier
4390c02117 Fix gRPC message editing 2025-11-04 12:35:36 -08:00
480 changed files with 10491 additions and 7266 deletions

12
.github/FUNDING.yml vendored
View File

@@ -1,15 +1,3 @@
# These are supported funding model platforms
github: gschier
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: https://yaak.app/pricing

View File

@@ -1,18 +0,0 @@
on:
pull_request:
branches: [develop]
name: CI (JS)
jobs:
test:
name: Lint/Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run lint
- run: npm test

View File

@@ -1,36 +0,0 @@
on:
pull_request:
branches: [develop]
paths:
- src-tauri/**
- .github/workflows/**
name: CI (Rust)
defaults:
run:
working-directory: src-tauri
jobs:
test:
name: Check/Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev
- uses: dtolnay/rust-toolchain@stable
- uses: actions/cache@v3
continue-on-error: false
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
- run: cargo check --all
- run: cargo test --all

31
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
on:
pull_request:
push:
branches:
- main
name: Lint and Test
permissions:
contents: read
jobs:
test:
name: Lint/Test
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: 'src-tauri'
shared-key: ci
cache-on-failure: true
- run: npm ci
- run: npm run lint
- name: Run JS Tests
run: npm test
- name: Run Rust Tests
run: cargo test --all
working-directory: src-tauri

View File

@@ -16,15 +16,34 @@ jobs:
- platform: 'macos-latest' # for Arm-based Macs (M1 and above).
args: '--target aarch64-apple-darwin'
yaak_arch: 'arm64'
os: 'macos'
targets: 'aarch64-apple-darwin'
- platform: 'macos-latest' # for Intel-based Macs.
args: '--target x86_64-apple-darwin'
yaak_arch: 'x64'
os: 'macos'
targets: 'x86_64-apple-darwin'
- platform: 'ubuntu-22.04'
args: ''
yaak_arch: 'x64'
os: 'ubuntu'
targets: ''
- platform: 'ubuntu-22.04-arm'
args: ''
yaak_arch: 'arm64'
os: 'ubuntu'
targets: ''
- platform: 'windows-latest'
args: ''
yaak_arch: 'x64'
os: 'windows'
targets: ''
# Windows ARM64
- platform: 'windows-latest'
args: '--target aarch64-pc-windows-msvc'
yaak_arch: 'arm64'
os: 'windows'
targets: 'aarch64-pc-windows-msvc'
runs-on: ${{ matrix.platform }}
timeout-minutes: 40
steps:
@@ -33,54 +52,49 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
targets: ${{ matrix.targets }}
- uses: actions/cache@v3
continue-on-error: false
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
src-tauri/target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
workspaces: 'src-tauri'
shared-key: ci
cache-on-failure: true
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
- name: install dependencies (Linux only)
if: matrix.os == 'ubuntu'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: install dependencies (windows only)
if: matrix.platform == 'windows-latest'
run: cargo install --force trusted-signing-cli
- name: Install NPM Dependencies
run: npm ci
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
- name: Install Protoc for plugin-runtime
uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Some things (eg. WASM package) requires building before lint will work
- name: Run bootstrap
run: npm run bootstrap
- name: Install trusted-signing-cli (Windows only)
if: matrix.os == 'windows'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$dir = "$env:USERPROFILE\trusted-signing"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
$url = "https://github.com/Levminer/trusted-signing-cli/releases/download/0.8.0/trusted-signing-cli.exe"
$exe = Join-Path $dir "trusted-signing-cli.exe"
Invoke-WebRequest -Uri $url -OutFile $exe
echo $dir >> $env:GITHUB_PATH
& $exe --version
- name: Run lint
run: npm run lint
- name: Run tests
- run: npm ci
- run: npm run lint
- name: Run JS Tests
run: npm test
- name: Run Rust Tests
run: cargo test --all
working-directory: src-tauri
- name: Set version
run: npm run replace-version
@@ -97,17 +111,17 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
# Apple signing stuff
APPLE_CERTIFICATE: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_SIGNING_IDENTITY }}
APPLE_TEAM_ID: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_TEAM_ID }}
APPLE_CERTIFICATE: ${{ matrix.os == 'macos' && secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ matrix.os == 'macos' && secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ matrix.os == 'macos' && secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ matrix.os == 'macos' && secrets.APPLE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ matrix.os == 'macos' && secrets.APPLE_SIGNING_IDENTITY }}
APPLE_TEAM_ID: ${{ matrix.os == 'macos' && secrets.APPLE_TEAM_ID }}
# Windows signing stuff
AZURE_CLIENT_ID: ${{ matrix.platform == 'windows-latest' && secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ matrix.platform == 'windows-latest' && secrets.AZURE_CLIENT_SECRET }}
AZURE_TENANT_ID: ${{ matrix.platform == 'windows-latest' && secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ matrix.os == 'windows' && secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ matrix.os == 'windows' && secrets.AZURE_CLIENT_SECRET }}
AZURE_TENANT_ID: ${{ matrix.os == 'windows' && secrets.AZURE_TENANT_ID }}
with:
tagName: 'v__VERSION__'
releaseName: 'Release __VERSION__'

2
.gitignore vendored
View File

@@ -15,6 +15,8 @@ dist-ssr
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/launch.json
.idea
.DS_Store
*.suo

View File

@@ -1,4 +0,0 @@
node_modules/
dist/
out/
.prettierrc.cjs

View File

@@ -1,8 +0,0 @@
export default {
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 100,
"bracketSpacing": true
}

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["biomejs.biome", "rust-lang.rust-analyzer", "bradlc.vscode-tailwindcss"]
}

26
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Dev App",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start"]
},
{
"type": "node",
"request": "launch",
"name": "Build App",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start"]
},
{
"type": "node",
"request": "launch",
"name": "Bootstrap",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "bootstrap"]
}
]
}

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"biome.enabled": true,
"biome.lint.format.enable": true
}

View File

@@ -54,9 +54,35 @@ Rerun the app to apply the migrations.
_Note: For safety, development builds use a separate database location from production builds._
## Lezer Grammer Generation
## Lezer Grammar Generation
```sh
# Example
lezer-generator components/core/Editor/<LANG>/<LANG>.grammar > components/core/Editor/<LANG>/<LANG>.ts
```
## Linting & Formatting
This repo uses Biome for linting and formatting (replacing ESLint + Prettier).
- Lint the entire repo:
```sh
npm run lint
```
- Auto-fix lint issues where possible:
```sh
npm run lint:fix
```
- Format code:
```sh
npm run format
```
Notes:
- Many workspace packages also expose the same scripts (`lint`, `lint:fix`, and `format`).
- TypeScript type-checking still runs separately via `tsc --noEmit` in relevant packages.

View File

@@ -19,10 +19,10 @@
<p align="center">
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https:&#x2F;&#x2F;github.com&#x2F;MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a>&nbsp;&nbsp;<a href="https://github.com/dharsanb"><img src="https:&#x2F;&#x2F;github.com&#x2F;dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a>&nbsp;&nbsp;<a href="https://github.com/railwayapp"><img src="https:&#x2F;&#x2F;github.com&#x2F;railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a>&nbsp;&nbsp;<a href="https://github.com/caseyamcl"><img src="https:&#x2F;&#x2F;github.com&#x2F;caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a>&nbsp;&nbsp;<a href="https://github.com/andriyor"><img src="https:&#x2F;&#x2F;github.com&#x2F;andriyor.png" width="80px" alt="User avatar: andriyor" /></a>&nbsp;&nbsp;<a href="https://github.com/"><img src="https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;JamesIves&#x2F;github-sponsors-readme-action&#x2F;dev&#x2F;.github&#x2F;assets&#x2F;placeholder.png" width="80px" alt="User avatar: " /></a>&nbsp;&nbsp;<!-- sponsors-premium -->
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https:&#x2F;&#x2F;github.com&#x2F;MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a>&nbsp;&nbsp;<a href="https://github.com/dharsanb"><img src="https:&#x2F;&#x2F;github.com&#x2F;dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a>&nbsp;&nbsp;<a href="https://github.com/railwayapp"><img src="https:&#x2F;&#x2F;github.com&#x2F;railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a>&nbsp;&nbsp;<a href="https://github.com/caseyamcl"><img src="https:&#x2F;&#x2F;github.com&#x2F;caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a>&nbsp;&nbsp;<a href="https://github.com/"><img src="https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;JamesIves&#x2F;github-sponsors-readme-action&#x2F;dev&#x2F;.github&#x2F;assets&#x2F;placeholder.png" width="80px" alt="User avatar: " /></a>&nbsp;&nbsp;<!-- sponsors-premium -->
</p>
<p align="center">
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanwash.png" width="50px" alt="User avatar: seanwash" /></a>&nbsp;&nbsp;<a href="https://github.com/jerath"><img src="https:&#x2F;&#x2F;github.com&#x2F;jerath.png" width="50px" alt="User avatar: jerath" /></a>&nbsp;&nbsp;<a href="https://github.com/itsa-sh"><img src="https:&#x2F;&#x2F;github.com&#x2F;itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a>&nbsp;&nbsp;<a href="https://github.com/dmmulroy"><img src="https:&#x2F;&#x2F;github.com&#x2F;dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a>&nbsp;&nbsp;<a href="https://github.com/timcole"><img src="https:&#x2F;&#x2F;github.com&#x2F;timcole.png" width="50px" alt="User avatar: timcole" /></a>&nbsp;&nbsp;<a href="https://github.com/VLZH"><img src="https:&#x2F;&#x2F;github.com&#x2F;VLZH.png" width="50px" alt="User avatar: VLZH" /></a>&nbsp;&nbsp;<a href="https://github.com/terasaka2k"><img src="https:&#x2F;&#x2F;github.com&#x2F;terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a>&nbsp;&nbsp;<a href="https://github.com/majudhu"><img src="https:&#x2F;&#x2F;github.com&#x2F;majudhu.png" width="50px" alt="User avatar: majudhu" /></a>&nbsp;&nbsp;<a href="https://github.com/axelrindle"><img src="https:&#x2F;&#x2F;github.com&#x2F;axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a>&nbsp;&nbsp;<a href="https://github.com/jirizverina"><img src="https:&#x2F;&#x2F;github.com&#x2F;jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a>&nbsp;&nbsp;<a href="https://github.com/chip-well"><img src="https:&#x2F;&#x2F;github.com&#x2F;chip-well.png" width="50px" alt="User avatar: chip-well" /></a>&nbsp;&nbsp;<!-- sponsors-base -->
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanwash.png" width="50px" alt="User avatar: seanwash" /></a>&nbsp;&nbsp;<a href="https://github.com/jerath"><img src="https:&#x2F;&#x2F;github.com&#x2F;jerath.png" width="50px" alt="User avatar: jerath" /></a>&nbsp;&nbsp;<a href="https://github.com/itsa-sh"><img src="https:&#x2F;&#x2F;github.com&#x2F;itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a>&nbsp;&nbsp;<a href="https://github.com/dmmulroy"><img src="https:&#x2F;&#x2F;github.com&#x2F;dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a>&nbsp;&nbsp;<a href="https://github.com/timcole"><img src="https:&#x2F;&#x2F;github.com&#x2F;timcole.png" width="50px" alt="User avatar: timcole" /></a>&nbsp;&nbsp;<a href="https://github.com/VLZH"><img src="https:&#x2F;&#x2F;github.com&#x2F;VLZH.png" width="50px" alt="User avatar: VLZH" /></a>&nbsp;&nbsp;<a href="https://github.com/terasaka2k"><img src="https:&#x2F;&#x2F;github.com&#x2F;terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a>&nbsp;&nbsp;<a href="https://github.com/andriyor"><img src="https:&#x2F;&#x2F;github.com&#x2F;andriyor.png" width="50px" alt="User avatar: andriyor" /></a>&nbsp;&nbsp;<a href="https://github.com/majudhu"><img src="https:&#x2F;&#x2F;github.com&#x2F;majudhu.png" width="50px" alt="User avatar: majudhu" /></a>&nbsp;&nbsp;<a href="https://github.com/axelrindle"><img src="https:&#x2F;&#x2F;github.com&#x2F;axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a>&nbsp;&nbsp;<a href="https://github.com/jirizverina"><img src="https:&#x2F;&#x2F;github.com&#x2F;jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a>&nbsp;&nbsp;<a href="https://github.com/chip-well"><img src="https:&#x2F;&#x2F;github.com&#x2F;chip-well.png" width="50px" alt="User avatar: chip-well" /></a>&nbsp;&nbsp;<a href="https://github.com/GRAYAH"><img src="https:&#x2F;&#x2F;github.com&#x2F;GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a>&nbsp;&nbsp;<!-- sponsors-base -->
</p>
![Yaak API Client](https://yaak.app/static/screenshot.png)

51
biome.json Normal file
View File

@@ -0,0 +1,51 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.7/schema.json",
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"useKeyWithClickEvents": "off"
}
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"bracketSpacing": true
},
"css": {
"parser": {
"tailwindDirectives": true
},
"linter": {
"enabled": false
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double",
"trailingCommas": "all",
"semicolons": "always"
}
},
"files": {
"includes": [
"**",
"!**/node_modules",
"!**/dist",
"!**/build",
"!scripts",
"!packages/plugin-runtime",
"!packages/plugin-runtime-types",
"!src-tauri",
"!src-web/tailwind.config.cjs",
"!src-web/postcss.config.cjs",
"!src-web/vite.config.ts",
"!src-web/routeTree.gen.ts"
]
}
}

View File

@@ -1,89 +0,0 @@
const { defineConfig, globalIgnores } = require('eslint/config');
const { fixupConfigRules } = require('@eslint/compat');
const reactRefresh = require('eslint-plugin-react-refresh');
const tsParser = require('@typescript-eslint/parser');
const js = require('@eslint/js');
const { FlatCompat } = require('@eslint/eslintrc');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
module.exports = defineConfig([
{
extends: fixupConfigRules(
compat.extends(
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:import/recommended',
'plugin:jsx-a11y/recommended',
'plugin:@typescript-eslint/recommended',
'eslint-config-prettier',
),
),
plugins: {
'react-refresh': reactRefresh,
},
languageOptions: {
parser: tsParser,
parserOptions: {
project: ['./tsconfig.json'],
},
},
settings: {
react: {
version: 'detect',
},
'import/resolver': {
node: {
paths: ['src-web'],
extensions: ['.ts', '.tsx'],
},
},
},
rules: {
'react-refresh/only-export-components': 'error',
'jsx-a11y/no-autofocus': 'off',
'react/react-in-jsx-scope': 'off',
'import/no-unresolved': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
disallowTypeAnnotations: true,
fixStyle: 'separate-type-imports',
},
],
},
},
globalIgnores([
'scripts/**/*',
'packages/plugin-runtime/**/*',
'packages/plugin-runtime-types/**/*',
'src-tauri/**/*',
'src-web/tailwind.config.cjs',
'src-web/vite.config.ts',
]),
globalIgnores([
'**/node_modules/',
'**/dist/',
'**/build/',
'**/.eslintrc.cjs',
'**/.prettierrc.cjs',
'src-web/postcss.config.cjs',
'src-web/vite.config.ts',
]),
]);

3489
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,9 @@
"plugins/auth-basic",
"plugins/auth-bearer",
"plugins/auth-jwt",
"plugins/auth-ntlm",
"plugins/auth-oauth2",
"plugins/auth-oauth1",
"plugins/filter-jsonpath",
"plugins/filter-xpath",
"plugins/importer-curl",
@@ -26,7 +28,9 @@
"plugins/importer-postman",
"plugins/importer-postman-environment",
"plugins/importer-yaak",
"plugins/template-function-1password",
"plugins/template-function-cookie",
"plugins/template-function-ctx",
"plugins/template-function-encode",
"plugins/template-function-fs",
"plugins/template-function-hash",
@@ -34,11 +38,11 @@
"plugins/template-function-prompt",
"plugins/template-function-random",
"plugins/template-function-regex",
"plugins/template-function-request",
"plugins/template-function-response",
"plugins/template-function-timestamp",
"plugins/template-function-uuid",
"plugins/template-function-xml",
"plugins/template-function-request",
"plugins/template-function-response",
"plugins/themes-yaak",
"src-tauri",
"src-tauri/yaak-crypto",
@@ -65,36 +69,29 @@
"icons": "run-p icons:*",
"icons:dev": "tauri icon src-tauri/icons/icon-dev.png --output src-tauri/icons/dev",
"icons:release": "tauri icon src-tauri/icons/icon.png --output src-tauri/icons/release",
"bootstrap": "run-p bootstrap:* && npm run --workspaces --if-present bootstrap",
"bootstrap:vendor-node": "node scripts/vendor-node.cjs",
"bootstrap:vendor-plugins": "node scripts/vendor-plugins.cjs",
"bootstrap:vendor-protoc": "node scripts/vendor-protoc.cjs",
"lint": "npm run --workspaces --if-present lint",
"bootstrap": "run-s bootstrap:*",
"bootstrap:install-wasm-pack": "node scripts/install-wasm-pack.cjs",
"bootstrap:build": "npm run build",
"bootstrap:vendor": "npm run vendor",
"vendor": "run-p vendor:*",
"vendor:vendor-plugins": "node scripts/vendor-plugins.cjs",
"vendor:vendor-protoc": "node scripts/vendor-protoc.cjs",
"vendor:vendor-node": "node scripts/vendor-node.cjs",
"lint": "run-p lint:*",
"lint:biome": "biome lint",
"lint:extra": "npm run --workspaces --if-present lint",
"format": "biome format --write .",
"replace-version": "node scripts/replace-version.cjs",
"tauri": "tauri",
"tauri-before-build": "npm run bootstrap && npm run --workspaces --if-present build",
"tauri-before-build": "npm run bootstrap",
"tauri-before-dev": "workspaces-run --parallel -- npm run --workspaces --if-present dev"
},
"dependencies": {
"jotai": "^2.12.2"
},
"devDependencies": {
"@eslint/compat": "^1.3.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.29.0",
"@biomejs/biome": "^2.3.7",
"@tauri-apps/cli": "^2.9.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.2.7",
"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"@yaakapp/cli": "^0.3.4",
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.8.3",
"vitest": "^3.2.4",
"workspaces-run": "^1.0.2"

View File

@@ -1,12 +1,12 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
export function debounce(fn: (...args: any[]) => void, delay = 500) {
let timer: ReturnType<typeof setTimeout>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = function (...args: any[]) {
// biome-ignore lint/suspicious/noExplicitAny: none
const result = (...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
result.cancel = function () {
result.cancel = () => {
clearTimeout(timer);
};
return result;

View File

@@ -1,20 +1,20 @@
export function formatSize(bytes: number): string {
let num;
let unit;
let num: number;
let unit: string;
if (bytes > 1000 * 1000 * 1000) {
num = bytes / 1000 / 1000 / 1000;
unit = 'GB';
} else if (bytes > 1000 * 1000) {
num = bytes / 1000 / 1000;
unit = 'MB';
} else if (bytes > 1000) {
num = bytes / 1000;
unit = 'KB';
} else {
num = bytes;
unit = 'B';
}
if (bytes > 1000 * 1000 * 1000) {
num = bytes / 1000 / 1000 / 1000;
unit = 'GB';
} else if (bytes > 1000 * 1000) {
num = bytes / 1000 / 1000;
unit = 'MB';
} else if (bytes > 1000) {
num = bytes / 1000;
unit = 'KB';
} else {
num = bytes;
unit = 'B';
}
return `${Math.round(num * 10) / 10} ${unit}`;
return `${Math.round(num * 10) / 10} ${unit}`;
}

View File

@@ -1 +1,3 @@
export * from './debounce';
export * from './formatSize';
export * from './templateFunction';

View File

@@ -0,0 +1,49 @@
import type {
CallTemplateFunctionArgs,
JsonPrimitive,
TemplateFunctionArg,
} from '@yaakapp-internal/plugins';
export function validateTemplateFunctionArgs(
fnName: string,
args: TemplateFunctionArg[],
values: CallTemplateFunctionArgs['values'],
): string | null {
for (const arg of args) {
if ('inputs' in arg && arg.inputs) {
// Recurse down
const err = validateTemplateFunctionArgs(fnName, arg.inputs, values);
if (err) return err;
}
if (!('name' in arg)) continue;
if (arg.optional) continue;
if (arg.defaultValue != null) continue;
if (arg.hidden) continue;
if (values[arg.name] != null) continue;
return `Missing required argument "${arg.label || arg.name}" for template function ${fnName}()`;
}
return null;
}
/** Recursively apply form input defaults to a set of values */
export function applyFormInputDefaults(
inputs: TemplateFunctionArg[],
values: { [p: string]: JsonPrimitive | undefined },
) {
let newValues: { [p: string]: JsonPrimitive | undefined } = { ...values };
for (const input of inputs) {
if ('defaultValue' in input && values[input.name] === undefined) {
newValues[input.name] = input.defaultValue;
}
if (input.type === 'checkbox' && values[input.name] === undefined) {
newValues[input.name] = false;
}
// Recurse down to all child inputs
if ('inputs' in input) {
newValues = applyFormInputDefaults(input.inputs ?? [], newValues);
}
}
return newValues;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.7.0",
"version": "0.7.1",
"keywords": [
"api-client",
"insomnia-alternative",

View File

@@ -1,5 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PluginVersion } from "./gen_search.js";
import type { PluginVersion } from "./gen_search";
export type PluginNameVersion = { name: string, version: string, };

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models";
import type { JsonValue } from "./serde_json/JsonValue";
export type BootRequest = { dir: string, watch: boolean, };
@@ -30,7 +30,7 @@ export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: JsonValue }, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: JsonPrimitive }, };
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
@@ -74,7 +74,7 @@ export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
@@ -224,6 +224,8 @@ defaultValue?: string, disabled?: boolean,
*/
description?: string, };
export type FormInputHStack = { inputs?: Array<FormInput>, hidden?: boolean, };
export type FormInputHttpRequest = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
@@ -387,9 +389,9 @@ export type ImportResources = { workspaces: Array<Workspace>, environments: Arra
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, context: PluginContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "window_info_request" } & WindowInfoRequest | { "type": "window_info_response" } & WindowInfoResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;
@@ -403,13 +405,13 @@ export type OpenWindowRequest = { url: string,
*/
label: string, title?: string, size?: WindowSize, dataDirKey?: string, };
export type PluginWindowContext = { "type": "none" } | { "type": "label", label: string, workspace_id: string | null, };
export type PluginContext = { id: string, label: string | null, workspaceId: string | null, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
confirmText?: string, password?: boolean,
/**
* Text to add to the cancel button
*/
@@ -443,18 +445,24 @@ export type SetKeyValueResponse = {};
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, timeout?: number, };
export type TemplateFunction = { name: string, description?: string,
export type TemplateFunction = { name: string, previewType?: TemplateFunctionPreviewType, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<TemplateFunctionArg>, };
aliases?: Array<string>, args: Array<TemplateFunctionArg>,
/**
* A list of arg names to show in the inline preview. If not provided, none will be shown (for privacy reasons).
*/
previewArgs?: Array<string>, };
/**
* Similar to FormInput, but contains
*/
export type TemplateFunctionArg = FormInput;
export type TemplateFunctionPreviewType = "live" | "click" | "none";
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
@@ -485,6 +493,10 @@ export type ThemeComponentColors = { surface?: string, surfaceHighlight?: string
export type ThemeComponents = { dialog?: ThemeComponentColors, menu?: ThemeComponentColors, toast?: ThemeComponentColors, sidebar?: ThemeComponentColors, responsePane?: ThemeComponentColors, appHeader?: ThemeComponentColors, button?: ThemeComponentColors, banner?: ThemeComponentColors, templateTag?: ThemeComponentColors, urlBar?: ThemeComponentColors, editor?: ThemeComponentColors, input?: ThemeComponentColors, };
export type WindowInfoRequest = { label: string, };
export type WindowInfoResponse = { requestId: string | null, environmentId: string | null, workspaceId: string | null, label: string, };
export type WindowNavigateEvent = { url: string, };
export type WindowSize = { width: number, height: number, };

View File

@@ -3,22 +3,37 @@ import {
CallHttpAuthenticationRequest,
CallHttpAuthenticationResponse,
FormInput,
GetHttpAuthenticationConfigRequest,
GetHttpAuthenticationSummaryResponse,
HttpAuthenticationAction,
} from '../bindings/gen_events';
import { MaybePromise } from '../helpers';
import { Context } from './Context';
type DynamicFormInput = FormInput & {
dynamic(
type AddDynamicMethod<T> = {
dynamic?: (
ctx: Context,
args: GetHttpAuthenticationConfigRequest,
): MaybePromise<Partial<FormInput> | undefined | null>;
args: CallHttpAuthenticationActionArgs,
) => MaybePromise<Partial<T> | null | undefined>;
};
type AddDynamic<T> = T extends any
? T extends { inputs?: FormInput[] }
? Omit<T, 'inputs'> & {
inputs: Array<AddDynamic<FormInput>>;
dynamic?: (
ctx: Context,
args: CallHttpAuthenticationActionArgs,
) => MaybePromise<
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
>;
}
: T & AddDynamicMethod<T>
: never;
export type DynamicAuthenticationArg = AddDynamic<FormInput>;
export type AuthenticationPlugin = GetHttpAuthenticationSummaryResponse & {
args: (FormInput | DynamicFormInput)[];
args: DynamicAuthenticationArg[];
onApply(
ctx: Context,
args: CallHttpAuthenticationRequest,

View File

@@ -18,7 +18,7 @@ import type {
ShowToastRequest,
TemplateRenderRequest,
} from '../bindings/gen_events.ts';
import { JsonValue } from '../bindings/serde_json/JsonValue';
import type { JsonValue } from '../bindings/serde_json/JsonValue';
export interface Context {
clipboard: {
@@ -36,6 +36,9 @@ export interface Context {
delete(key: string): Promise<boolean>;
};
window: {
requestId(): Promise<string | null>;
workspaceId(): Promise<string | null>;
environmentId(): Promise<string | null>;
openUrl(
args: OpenWindowRequest & {
onNavigate?: (args: { url: string }) => void;

View File

@@ -1,21 +1,31 @@
import {
CallTemplateFunctionArgs,
FormInput,
GetHttpAuthenticationConfigRequest,
TemplateFunction,
TemplateFunctionArg,
} from '../bindings/gen_events';
import { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events';
import { MaybePromise } from '../helpers';
import { Context } from './Context';
export type DynamicTemplateFunctionArg = FormInput & {
dynamic(
type AddDynamicMethod<T> = {
dynamic?: (
ctx: Context,
args: GetHttpAuthenticationConfigRequest,
): MaybePromise<Partial<FormInput> | undefined | null>;
args: CallTemplateFunctionArgs,
) => MaybePromise<Partial<T> | null | undefined>;
};
export type TemplateFunctionPlugin = TemplateFunction & {
args: (TemplateFunctionArg | DynamicTemplateFunctionArg)[];
type AddDynamic<T> = T extends any
? T extends { inputs?: FormInput[] }
? Omit<T, 'inputs'> & {
inputs: Array<AddDynamic<FormInput>>;
dynamic?: (
ctx: Context,
args: CallTemplateFunctionArgs,
) => MaybePromise<
Partial<Omit<T, 'inputs'> & { inputs: Array<AddDynamic<FormInput>> }> | null | undefined
>;
}
: T & AddDynamicMethod<T>
: never;
export type DynamicTemplateFunctionArg = AddDynamic<FormInput>;
export type TemplateFunctionPlugin = Omit<TemplateFunction, 'args'> & {
args: DynamicTemplateFunctionArg[];
onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>;
};

View File

@@ -1,4 +1,6 @@
import { AuthenticationPlugin } from './AuthenticationPlugin';
import type { Context } from './Context';
import type { FilterPlugin } from './FilterPlugin';
import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
@@ -6,9 +8,10 @@ import type { ImporterPlugin } from './ImporterPlugin';
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
import type { ThemePlugin } from './ThemePlugin';
import type { Context } from './Context';
export type { Context };
export type { DynamicTemplateFunctionArg } from './TemplateFunctionPlugin';
export type { DynamicAuthenticationArg } from './AuthenticationPlugin';
export type { TemplateFunctionPlugin };
/**
* The global structure of a Yaak plugin

View File

@@ -1,3 +1,4 @@
import { PluginContext } from '@yaakapp-internal/plugins';
import type { BootRequest, InternalEvent } from '@yaakapp/api';
import type { EventChannel } from './EventChannel';
import { PluginInstance, PluginWorkerData } from './PluginInstance';
@@ -6,14 +7,12 @@ export class PluginHandle {
#instance: PluginInstance;
constructor(
readonly pluginRefId: string,
readonly bootRequest: BootRequest,
readonly pluginToAppEvents: EventChannel,
pluginRefId: string,
context: PluginContext,
bootRequest: BootRequest,
pluginToAppEvents: EventChannel,
) {
const workerData: PluginWorkerData = {
pluginRefId: this.pluginRefId,
bootRequest: this.bootRequest,
};
const workerData: PluginWorkerData = { pluginRefId, context, bootRequest };
this.#instance = new PluginInstance(workerData, pluginToAppEvents);
}

View File

@@ -1,8 +1,8 @@
import { applyFormInputDefaults, validateTemplateFunctionArgs } from '@yaakapp-internal/lib/templateFunction';
import {
BootRequest,
DeleteKeyValueResponse,
FindHttpResponsesResponse,
FormInput,
GetCookieValueRequest,
GetCookieValueResponse,
GetHttpRequestByIdResponse,
@@ -13,32 +13,32 @@ import {
InternalEvent,
InternalEventPayload,
ListCookieNamesResponse,
PluginWindowContext,
PluginContext,
PromptTextResponse,
RenderGrpcRequestResponse,
RenderHttpRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
TemplateFunctionArg,
TemplateRenderResponse,
WindowInfoResponse,
} from '@yaakapp-internal/plugins';
import { Context, PluginDefinition } from '@yaakapp/api';
import { JsonValue } from '@yaakapp/api/lib/bindings/serde_json/JsonValue';
import console from 'node:console';
import { readFileSync, type Stats, statSync, watch } from 'node:fs';
import { type Stats, statSync, watch } from 'node:fs';
import path from 'node:path';
import { applyDynamicFormInput } from './common';
import { EventChannel } from './EventChannel';
import { migrateTemplateFunctionSelectOptions } from './migrations';
export interface PluginWorkerData {
bootRequest: BootRequest;
pluginRefId: string;
context: PluginContext;
}
export class PluginInstance {
#workerData: PluginWorkerData;
#mod: PluginDefinition;
#pkg: { name?: string; version?: string };
#pluginToAppEvents: EventChannel;
#appToPluginEvents: EventChannel;
@@ -52,18 +52,14 @@ export class PluginInstance {
await this.#onMessage(event);
});
// Reload plugin if the JS or package.json changes
const windowContextNone: PluginWindowContext = { type: 'none' };
this.#mod = {} as any;
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
const fileChangeCallback = async () => {
await this.#mod?.dispose?.();
this.#importModule();
await this.#mod?.init?.(this.#newCtx({ type: 'none' }));
await this.#mod?.init?.(this.#newCtx(workerData.context));
return this.#sendPayload(
windowContextNone,
workerData.context,
{
type: 'reload_response',
silent: false,
@@ -90,14 +86,14 @@ export class PluginInstance {
}
async #onMessage(event: InternalEvent) {
const ctx = this.#newCtx(event.windowContext);
const ctx = this.#newCtx(event.context);
const { windowContext, payload, id: replyId } = event;
const { context, payload, id: replyId } = event;
try {
if (payload.type === 'boot_request') {
await this.#mod?.init?.(ctx);
this.#sendPayload(windowContext, { type: 'boot_response' }, replyId);
this.#sendPayload(context, { type: 'boot_response' }, replyId);
return;
}
@@ -106,7 +102,7 @@ export class PluginInstance {
type: 'terminate_response',
};
await this.terminate();
this.#sendPayload(windowContext, payload, replyId);
this.#sendPayload(context, payload, replyId);
return;
}
@@ -123,10 +119,10 @@ export class PluginInstance {
// deno-lint-ignore no-explicit-any
resources: reply.resources as any,
};
this.#sendPayload(windowContext, replyPayload, replyId);
this.#sendPayload(context, replyPayload, replyId);
return;
} else {
// Continue, to send back an empty reply
// Send back an empty reply (below)
}
}
@@ -136,7 +132,7 @@ export class PluginInstance {
payload: payload.content,
mimeType: payload.type,
});
this.#sendPayload(windowContext, { type: 'filter_response', ...reply }, replyId);
this.#sendPayload(context, { type: 'filter_response', ...reply }, replyId);
return;
}
@@ -154,7 +150,7 @@ export class PluginInstance {
pluginRefId: this.#workerData.pluginRefId,
actions: reply,
};
this.#sendPayload(windowContext, replyPayload, replyId);
this.#sendPayload(context, replyPayload, replyId);
return;
}
@@ -172,7 +168,7 @@ export class PluginInstance {
pluginRefId: this.#workerData.pluginRefId,
actions: reply,
};
this.#sendPayload(windowContext, replyPayload, replyId);
this.#sendPayload(context, replyPayload, replyId);
return;
}
@@ -181,7 +177,7 @@ export class PluginInstance {
type: 'get_themes_response',
themes: this.#mod.themes,
};
this.#sendPayload(windowContext, replyPayload, replyId);
this.#sendPayload(context, replyPayload, replyId);
return;
}
@@ -203,7 +199,7 @@ export class PluginInstance {
pluginRefId: this.#workerData.pluginRefId,
functions,
};
this.#sendPayload(windowContext, replyPayload, replyId);
this.#sendPayload(context, replyPayload, replyId);
return;
}
@@ -213,56 +209,42 @@ export class PluginInstance {
) {
let templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name);
if (templateFunction == null) {
this.#sendEmpty(windowContext, replyId);
this.#sendEmpty(context, replyId);
return;
}
templateFunction = migrateTemplateFunctionSelectOptions(templateFunction);
// @ts-ignore
delete templateFunction.onRender;
const resolvedArgs: TemplateFunctionArg[] = [];
for (const arg of templateFunction.args) {
if (arg && 'dynamic' in arg) {
const dynamicAttrs = await arg.dynamic(ctx, payload);
const { dynamic, ...other } = arg;
resolvedArgs.push({ ...other, ...dynamicAttrs } as TemplateFunctionArg);
} else if (arg) {
resolvedArgs.push(arg);
}
templateFunction.args = resolvedArgs;
}
const fn = {
...migrateTemplateFunctionSelectOptions(templateFunction),
onRender: undefined,
};
payload.values = applyFormInputDefaults(fn.args, payload.values);
const p = { ...payload, purpose: 'preview' } as const;
const resolvedArgs = await applyDynamicFormInput(ctx, fn.args, p);
const replyPayload: InternalEventPayload = {
type: 'get_template_function_config_response',
pluginRefId: this.#workerData.pluginRefId,
function: templateFunction,
function: { ...fn, args: resolvedArgs },
};
this.#sendPayload(windowContext, replyPayload, replyId);
this.#sendPayload(context, replyPayload, replyId);
return;
}
if (payload.type === 'get_http_authentication_summary_request' && this.#mod?.authentication) {
const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_summary_response',
...this.#mod.authentication,
};
this.#sendPayload(windowContext, replyPayload, replyId);
this.#sendPayload(context, replyPayload, replyId);
return;
}
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
const { args, actions } = this.#mod.authentication;
const resolvedArgs: FormInput[] = [];
for (const v of args) {
if (v && 'dynamic' in v) {
const dynamicAttrs = await v.dynamic(ctx, payload);
const { dynamic, ...other } = v;
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
} else if (v) {
resolvedArgs.push(v);
}
}
payload.values = applyFormInputDefaults(args, payload.values);
const resolvedArgs = await applyDynamicFormInput(ctx, args, payload);
const resolvedActions: HttpAuthenticationAction[] = [];
for (const { onSelect, ...action } of actions ?? []) {
resolvedActions.push(action);
@@ -275,16 +257,17 @@ export class PluginInstance {
pluginRefId: this.#workerData.pluginRefId,
};
this.#sendPayload(windowContext, replyPayload, replyId);
this.#sendPayload(context, replyPayload, replyId);
return;
}
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) {
const auth = this.#mod.authentication;
if (typeof auth?.onApply === 'function') {
applyFormInputDefaults(auth.args, payload.values);
auth.args = await applyDynamicFormInput(ctx, auth.args, payload);
payload.values = applyFormInputDefaults(auth.args, payload.values);
this.#sendPayload(
windowContext,
context,
{
type: 'call_http_authentication_response',
...(await auth.onApply(ctx, payload)),
@@ -302,7 +285,7 @@ export class PluginInstance {
const action = this.#mod.authentication.actions?.[payload.index];
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
this.#sendEmpty(windowContext, replyId);
this.#sendEmpty(context, replyId);
return;
}
}
@@ -314,7 +297,7 @@ export class PluginInstance {
const action = this.#mod.httpRequestActions[payload.index];
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
this.#sendEmpty(windowContext, replyId);
this.#sendEmpty(context, replyId);
return;
}
}
@@ -326,7 +309,7 @@ export class PluginInstance {
const action = this.#mod.grpcRequestActions[payload.index];
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
this.#sendEmpty(windowContext, replyId);
this.#sendEmpty(context, replyId);
return;
}
}
@@ -336,21 +319,43 @@ export class PluginInstance {
Array.isArray(this.#mod?.templateFunctions)
) {
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
if (typeof fn?.onRender === 'function') {
applyFormInputDefaults(fn.args, payload.args.values);
try {
const result = await fn.onRender(ctx, payload.args);
if (
payload.args.purpose === 'preview' &&
(fn?.previewType === 'click' || fn?.previewType === 'none')
) {
// Send empty render response
this.#sendPayload(
context,
{
type: 'call_template_function_response',
value: null,
error: 'Live preview disabled for this function',
},
replyId,
);
} else if (typeof fn?.onRender === 'function') {
const resolvedArgs = await applyDynamicFormInput(ctx, fn.args, payload.args);
const values = applyFormInputDefaults(resolvedArgs, payload.args.values);
const error = validateTemplateFunctionArgs(fn.name, resolvedArgs, values);
if (error && payload.args.purpose !== 'preview') {
this.#sendPayload(
windowContext,
{
type: 'call_template_function_response',
value: result ?? null,
},
context,
{ type: 'call_template_function_response', value: null, error },
replyId,
);
return;
}
try {
const result = await fn.onRender(ctx, { ...payload.args, values });
this.#sendPayload(
context,
{ type: 'call_template_function_response', value: result ?? null },
replyId,
);
} catch (err) {
this.#sendPayload(
windowContext,
context,
{
type: 'call_template_function_response',
value: null,
@@ -365,12 +370,12 @@ export class PluginInstance {
} catch (err) {
const error = `${err}`.replace(/^Error:\s*/g, '');
console.log('Plugin call threw exception', payload.type, '→', error);
this.#sendPayload(windowContext, { type: 'error_response', error }, replyId);
this.#sendPayload(context, { type: 'error_response', error }, replyId);
return;
}
// No matches, so send back an empty response so the caller doesn't block forever
this.#sendEmpty(windowContext, replyId);
this.#sendEmpty(context, replyId);
}
#pathMod() {
@@ -393,7 +398,7 @@ export class PluginInstance {
}
#buildEventToSend(
windowContext: PluginWindowContext,
context: PluginContext,
payload: InternalEventPayload,
replyId: string | null = null,
): InternalEvent {
@@ -403,16 +408,16 @@ export class PluginInstance {
id: genId(),
replyId,
payload,
windowContext,
context,
};
}
#sendPayload(
windowContext: PluginWindowContext,
context: PluginContext,
payload: InternalEventPayload,
replyId: string | null,
): string {
const event = this.#buildEventToSend(windowContext, payload, replyId);
const event = this.#buildEventToSend(context, payload, replyId);
this.#sendEvent(event);
return event.id;
}
@@ -424,16 +429,16 @@ export class PluginInstance {
this.#pluginToAppEvents.emit(event);
}
#sendEmpty(windowContext: PluginWindowContext, replyId: string | null = null): string {
return this.#sendPayload(windowContext, { type: 'empty_response' }, replyId);
#sendEmpty(context: PluginContext, replyId: string | null = null): string {
return this.#sendPayload(context, { type: 'empty_response' }, replyId);
}
#sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
windowContext: PluginWindowContext,
#sendForReply<T extends Omit<InternalEventPayload, 'type'>>(
context: PluginContext,
payload: InternalEventPayload,
): Promise<T> {
// 1. Build event to send
const eventToSend = this.#buildEventToSend(windowContext, payload, null);
const eventToSend = this.#buildEventToSend(context, payload, null);
// 2. Spawn listener in background
const promise = new Promise<T>((resolve) => {
@@ -455,12 +460,12 @@ export class PluginInstance {
}
#sendAndListenForEvents(
windowContext: PluginWindowContext,
context: PluginContext,
payload: InternalEventPayload,
onEvent: (event: InternalEventPayload) => void,
): void {
// 1. Build event to send
const eventToSend = this.#buildEventToSend(windowContext, payload, null);
const eventToSend = this.#buildEventToSend(context, payload, null);
// 2. Listen for replies in the background
this.#appToPluginEvents.listen((event: InternalEvent) => {
@@ -473,11 +478,23 @@ export class PluginInstance {
this.#sendEvent(eventToSend);
}
#newCtx(windowContext: PluginWindowContext): Context {
#newCtx(context: PluginContext): Context {
const _windowInfo = async () => {
if (context.label == null) {
throw new Error("Can't get window context without an active window");
}
const payload: InternalEventPayload = {
type: 'window_info_request',
label: context.label,
};
return this.#sendForReply<WindowInfoResponse>(context, payload);
};
return {
clipboard: {
copyText: async (text) => {
await this.#sendAndWaitForReply(windowContext, {
await this.#sendForReply(context, {
type: 'copy_text_request',
text,
});
@@ -485,7 +502,7 @@ export class PluginInstance {
},
toast: {
show: async (args) => {
await this.#sendAndWaitForReply(windowContext, {
await this.#sendForReply(context, {
type: 'show_toast_request',
// Handle default here because null/undefined both convert to None in Rust translation
timeout: args.timeout === undefined ? 5000 : args.timeout,
@@ -494,6 +511,15 @@ export class PluginInstance {
},
},
window: {
requestId: async () => {
return (await _windowInfo()).requestId;
},
async workspaceId(): Promise<string | null> {
return (await _windowInfo()).workspaceId;
},
async environmentId(): Promise<string | null> {
return (await _windowInfo()).environmentId;
},
openUrl: async ({ onNavigate, onClose, ...args }) => {
args.label = args.label || `${Math.random()}`;
const payload: InternalEventPayload = { type: 'open_window_request', ...args };
@@ -504,21 +530,21 @@ export class PluginInstance {
onClose?.();
}
};
this.#sendAndListenForEvents(windowContext, payload, onEvent);
this.#sendAndListenForEvents(context, payload, onEvent);
return {
close: () => {
const closePayload: InternalEventPayload = {
type: 'close_window_request',
label: args.label,
};
this.#sendPayload(windowContext, closePayload, null);
this.#sendPayload(context, closePayload, null);
},
};
},
},
prompt: {
text: async (args) => {
const reply: PromptTextResponse = await this.#sendAndWaitForReply(windowContext, {
const reply: PromptTextResponse = await this.#sendForReply(context, {
type: 'prompt_text_request',
...args,
});
@@ -531,8 +557,8 @@ export class PluginInstance {
type: 'find_http_responses_request',
...args,
} as const;
const { httpResponses } = await this.#sendAndWaitForReply<FindHttpResponsesResponse>(
windowContext,
const { httpResponses } = await this.#sendForReply<FindHttpResponsesResponse>(
context,
payload,
);
return httpResponses;
@@ -544,8 +570,8 @@ export class PluginInstance {
type: 'render_grpc_request_request',
...args,
} as const;
const { grpcRequest } = await this.#sendAndWaitForReply<RenderGrpcRequestResponse>(
windowContext,
const { grpcRequest } = await this.#sendForReply<RenderGrpcRequestResponse>(
context,
payload,
);
return grpcRequest;
@@ -557,8 +583,8 @@ export class PluginInstance {
type: 'get_http_request_by_id_request',
...args,
} as const;
const { httpRequest } = await this.#sendAndWaitForReply<GetHttpRequestByIdResponse>(
windowContext,
const { httpRequest } = await this.#sendForReply<GetHttpRequestByIdResponse>(
context,
payload,
);
return httpRequest;
@@ -568,8 +594,8 @@ export class PluginInstance {
type: 'send_http_request_request',
...args,
} as const;
const { httpResponse } = await this.#sendAndWaitForReply<SendHttpRequestResponse>(
windowContext,
const { httpResponse } = await this.#sendForReply<SendHttpRequestResponse>(
context,
payload,
);
return httpResponse;
@@ -579,8 +605,8 @@ export class PluginInstance {
type: 'render_http_request_request',
...args,
} as const;
const { httpRequest } = await this.#sendAndWaitForReply<RenderHttpRequestResponse>(
windowContext,
const { httpRequest } = await this.#sendForReply<RenderHttpRequestResponse>(
context,
payload,
);
return httpRequest;
@@ -592,18 +618,12 @@ export class PluginInstance {
type: 'get_cookie_value_request',
...args,
} as const;
const { value } = await this.#sendAndWaitForReply<GetCookieValueResponse>(
windowContext,
payload,
);
const { value } = await this.#sendForReply<GetCookieValueResponse>(context, payload);
return value;
},
listNames: async () => {
const payload = { type: 'list_cookie_names_request' } as const;
const { names } = await this.#sendAndWaitForReply<ListCookieNamesResponse>(
windowContext,
payload,
);
const { names } = await this.#sendForReply<ListCookieNamesResponse>(context, payload);
return names;
},
},
@@ -614,20 +634,14 @@ export class PluginInstance {
*/
render: async (args) => {
const payload = { type: 'template_render_request', ...args } as const;
const result = await this.#sendAndWaitForReply<TemplateRenderResponse>(
windowContext,
payload,
);
const result = await this.#sendForReply<TemplateRenderResponse>(context, payload);
return result.data as any;
},
},
store: {
get: async <T>(key: string) => {
const payload = { type: 'get_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<GetKeyValueResponse>(
windowContext,
payload,
);
const result = await this.#sendForReply<GetKeyValueResponse>(context, payload);
return result.value ? (JSON.parse(result.value) as T) : undefined;
},
set: async <T>(key: string, value: T) => {
@@ -637,20 +651,17 @@ export class PluginInstance {
key,
value: valueStr,
};
await this.#sendAndWaitForReply<GetKeyValueResponse>(windowContext, payload);
await this.#sendForReply<GetKeyValueResponse>(context, payload);
},
delete: async (key: string) => {
const payload = { type: 'delete_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<DeleteKeyValueResponse>(
windowContext,
payload,
);
const result = await this.#sendForReply<DeleteKeyValueResponse>(context, payload);
return result.deleted;
},
},
plugin: {
reload: () => {
this.#sendPayload({ type: 'none' }, { type: 'reload_response', silent: true }, null);
this.#sendPayload(context, { type: 'reload_response', silent: true }, null);
},
},
};
@@ -666,20 +677,6 @@ function genId(len = 5): string {
return id;
}
/** Recursively apply form input defaults to a set of values */
function applyFormInputDefaults(
inputs: TemplateFunctionArg[],
values: { [p: string]: JsonValue | undefined },
) {
for (const input of inputs) {
if ('inputs' in input) {
applyFormInputDefaults(input.inputs ?? [], values);
} else if ('defaultValue' in input && values[input.name] === undefined) {
values[input.name] = input.defaultValue;
}
}
}
const watchedFiles: Record<string, Stats | null> = {};
/**

View File

@@ -0,0 +1,37 @@
import { CallHttpAuthenticationActionArgs, CallTemplateFunctionArgs } from '@yaakapp-internal/plugins';
import { Context, DynamicAuthenticationArg, DynamicTemplateFunctionArg } from '@yaakapp/api';
export async function applyDynamicFormInput(
ctx: Context,
args: DynamicTemplateFunctionArg[],
callArgs: CallTemplateFunctionArgs,
): Promise<DynamicTemplateFunctionArg[]>;
export async function applyDynamicFormInput(
ctx: Context,
args: DynamicAuthenticationArg[],
callArgs: CallHttpAuthenticationActionArgs,
): Promise<DynamicAuthenticationArg[]>;
export async function applyDynamicFormInput(
ctx: Context,
args: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[],
callArgs: CallTemplateFunctionArgs | CallHttpAuthenticationActionArgs,
): Promise<(DynamicTemplateFunctionArg | DynamicAuthenticationArg)[]> {
const resolvedArgs: any[] = [];
for (const { dynamic, ...arg } of args) {
const newArg: any = {
...arg,
...(typeof dynamic === 'function' ? await dynamic(ctx, callArgs as any) : undefined),
};
if ('inputs' in newArg && Array.isArray(newArg.inputs)) {
try {
newArg.inputs = await applyDynamicFormInput(ctx, newArg.inputs, callArgs as any);
} catch (e) {
console.error('Failed to apply dynamic form input', e);
}
}
resolvedArgs.push(newArg);
}
return resolvedArgs;
}

View File

@@ -39,7 +39,7 @@ async function handleIncoming(msg: string) {
const pluginEvent: InternalEvent = JSON.parse(msg);
// Handle special event to bootstrap plugin
if (pluginEvent.payload.type === 'boot_request') {
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.payload, pluginToAppEvents);
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.context, pluginEvent.payload, pluginToAppEvents);
plugins[pluginEvent.pluginRefId] = plugin;
}

View File

@@ -1,4 +1,4 @@
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
import type { TemplateFunctionPlugin } from '@yaakapp/api';
export function migrateTemplateFunctionSelectOptions(
f: TemplateFunctionPlugin,
@@ -13,8 +13,5 @@ export function migrateTemplateFunctionSelectOptions(
return a;
});
return {
...f,
args: migratedArgs,
};
return { ...f, args: migratedArgs };
}

View File

@@ -0,0 +1,150 @@
import { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins';
import { Context, DynamicTemplateFunctionArg } from '@yaakapp/api';
import { describe, expect, test } from 'vitest';
import { applyDynamicFormInput, applyFormInputDefaults } from '../src/common';
describe('applyFormInputDefaults', () => {
test('Works with top-level select', () => {
const args: DynamicTemplateFunctionArg[] = [
{
type: 'select',
name: 'test',
options: [{ label: 'Option 1', value: 'one' }],
defaultValue: 'one',
},
];
expect(applyFormInputDefaults(args, {})).toEqual({
test: 'one',
});
});
test('Works with existing value', () => {
const args: DynamicTemplateFunctionArg[] = [
{
type: 'select',
name: 'test',
options: [{ label: 'Option 1', value: 'one' }],
defaultValue: 'one',
},
];
expect(applyFormInputDefaults(args, { test: 'explicit' })).toEqual({
test: 'explicit',
});
});
test('Works with recursive select', () => {
const args: DynamicTemplateFunctionArg[] = [
{ type: 'text', name: 'dummy', defaultValue: 'top' },
{
type: 'accordion',
label: 'Test',
inputs: [
{ type: 'text', name: 'name', defaultValue: 'hello' },
{
type: 'select',
name: 'test',
options: [{ label: 'Option 1', value: 'one' }],
defaultValue: 'one',
},
],
},
];
expect(applyFormInputDefaults(args, {})).toEqual({
dummy: 'top',
test: 'one',
name: 'hello',
});
});
test('Works with dynamic options', () => {
const args: DynamicTemplateFunctionArg[] = [
{
type: 'select',
name: 'test',
defaultValue: 'one',
options: [],
dynamic() {
return { options: [{ label: 'Option 1', value: 'one' }] };
},
},
];
expect(applyFormInputDefaults(args, {})).toEqual({
test: 'one',
});
expect(applyFormInputDefaults(args, {})).toEqual({
test: 'one',
});
});
});
describe('applyDynamicFormInput', () => {
test('Works with plain input', async () => {
const ctx = {} as Context;
const args: DynamicTemplateFunctionArg[] = [
{ type: 'text', name: 'name' },
{ type: 'checkbox', name: 'checked' },
];
const callArgs: CallTemplateFunctionArgs = {
values: {},
purpose: 'preview',
};
expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([
{ type: 'text', name: 'name' },
{ type: 'checkbox', name: 'checked' },
]);
});
test('Works with dynamic input', async () => {
const ctx = {} as Context;
const args: DynamicTemplateFunctionArg[] = [
{
type: 'text',
name: 'name',
async dynamic(_ctx, _args) {
return { hidden: true };
},
},
];
const callArgs: CallTemplateFunctionArgs = {
values: {},
purpose: 'preview',
};
expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([
{ type: 'text', name: 'name', hidden: true },
]);
});
test('Works with recursive dynamic input', async () => {
const ctx = {} as Context;
const callArgs: CallTemplateFunctionArgs = {
values: { hello: 'world' },
purpose: 'preview',
};
const args: DynamicTemplateFunctionArg[] = [
{
type: 'banner',
inputs: [
{
type: 'text',
name: 'name',
async dynamic(_ctx, args) {
return { hidden: args.values.hello === 'world' };
},
},
],
},
];
expect(await applyDynamicFormInput(ctx, args, callArgs)).toEqual([
{
type: 'banner',
inputs: [
{
type: 'text',
name: 'name',
hidden: true,
},
],
},
]);
});
});

View File

@@ -12,7 +12,6 @@
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

@@ -36,7 +36,7 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
if (urlParams.length > 0) {
// Build url
const [base, hash] = finalUrl.split('#');
const separator = base!.includes('?') ? '&' : '?';
const separator = base?.includes('?') ? '&' : '?';
const queryString = urlParams
.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`)
.join('&');
@@ -46,7 +46,7 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
// Add API key authentication
if (request.authenticationType === 'apikey') {
if (request.authentication?.location === 'query') {
const sep = request.url?.includes('?') ? '&' : '?';
const sep = finalUrl.includes('?') ? '&' : '?';
finalUrl = [
finalUrl,
sep,

View File

@@ -12,7 +12,7 @@ describe('exporter-curl', () => {
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual([`curl 'https://yaak.app?a=aaa&b=bbb'`].join(` \\n `));
).toEqual([`curl 'https://yaak.app?a=aaa&b=bbb'`].join(' \\n '));
});
test('Exports GET with params and hash', async () => {
@@ -25,7 +25,7 @@ describe('exporter-curl', () => {
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual([`curl 'https://yaak.app/path?a=aaa&b=bbb#section'`].join(` \\n `));
).toEqual([`curl 'https://yaak.app/path?a=aaa&b=bbb#section'`].join(' \\n '));
});
test('Exports POST with url form data', async () => {
@@ -43,7 +43,7 @@ describe('exporter-curl', () => {
},
}),
).toEqual(
[`curl -X POST 'https://yaak.app'`, `--data 'a=aaa'`, `--data 'b=bbb'`].join(` \\\n `),
[`curl -X POST 'https://yaak.app'`, `--data 'a=aaa'`, `--data 'b=bbb'`].join(' \\\n '),
);
});
@@ -62,7 +62,7 @@ describe('exporter-curl', () => {
[
`curl -X POST 'https://yaak.app'`,
`--data '{"query":"{foo,bar}","variables":{"a":"aaa","b":"bbb"}}'`,
].join(` \\\n `),
].join(' \\\n '),
);
});
@@ -77,7 +77,7 @@ describe('exporter-curl', () => {
},
}),
).toEqual(
[`curl -X POST 'https://yaak.app'`, `--data '{"query":"{foo,bar}"}'`].join(` \\\n `),
[`curl -X POST 'https://yaak.app'`, `--data '{"query":"{foo,bar}"}'`].join(' \\\n '),
);
});
@@ -101,8 +101,8 @@ describe('exporter-curl', () => {
`curl -X PUT 'https://yaak.app'`,
`--form 'a=aaa'`,
`--form 'b=bbb'`,
`--form f=@/foo/bar.png;type=image/png`,
].join(` \\\n `),
'--form f=@/foo/bar.png;type=image/png',
].join(' \\\n '),
);
});
@@ -122,7 +122,7 @@ describe('exporter-curl', () => {
`curl -X POST 'https://yaak.app'`,
`--header 'Content-Type: application/json'`,
`--data '{"foo":"bar\\'s"}'`,
].join(` \\\n `),
].join(' \\\n '),
);
});
@@ -142,7 +142,7 @@ describe('exporter-curl', () => {
`curl -X POST 'https://yaak.app'`,
`--header 'Content-Type: application/json'`,
`--data '{"foo":"bar",\n"baz":"qux"}'`,
].join(` \\\n `),
].join(' \\\n '),
);
});
@@ -155,7 +155,7 @@ describe('exporter-curl', () => {
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual([`curl ''`, `--header 'a: aaa'`, `--header 'b: bbb'`].join(` \\\n `));
).toEqual([`curl ''`, `--header 'a: aaa'`, `--header 'b: bbb'`].join(' \\\n '));
});
test('Basic auth', async () => {
@@ -168,7 +168,7 @@ describe('exporter-curl', () => {
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--user 'user:pass'`].join(` \\\n `));
).toEqual([`curl 'https://yaak.app'`, `--user 'user:pass'`].join(' \\\n '));
});
test('Basic auth disabled', async () => {
@@ -182,7 +182,7 @@ describe('exporter-curl', () => {
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`].join(` \\\n `));
).toEqual([`curl 'https://yaak.app'`].join(' \\\n '));
});
test('Broken basic auth', async () => {
@@ -192,7 +192,7 @@ describe('exporter-curl', () => {
authenticationType: 'basic',
authentication: {},
}),
).toEqual([`curl 'https://yaak.app'`, `--user ':'`].join(` \\\n `));
).toEqual([`curl 'https://yaak.app'`, `--user ':'`].join(' \\\n '));
});
test('Digest auth', async () => {
@@ -205,7 +205,7 @@ describe('exporter-curl', () => {
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--digest --user 'user:pass'`].join(` \\\n `));
).toEqual([`curl 'https://yaak.app'`, `--digest --user 'user:pass'`].join(' \\\n '));
});
test('Bearer auth', async () => {
@@ -217,7 +217,7 @@ describe('exporter-curl', () => {
token: 'tok',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer tok'`].join(` \\\n `));
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer tok'`].join(' \\\n '));
});
test('Bearer auth with custom prefix', async () => {
@@ -231,7 +231,7 @@ describe('exporter-curl', () => {
},
}),
).toEqual(
[`curl 'https://yaak.app'`, `--header 'Authorization: Token abc123'`].join(` \\\n `),
[`curl 'https://yaak.app'`, `--header 'Authorization: Token abc123'`].join(' \\\n '),
);
});
@@ -245,7 +245,7 @@ describe('exporter-curl', () => {
prefix: '',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: xyz789'`].join(` \\\n `));
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: xyz789'`].join(' \\\n '));
});
test('Broken bearer auth', async () => {
@@ -258,7 +258,7 @@ describe('exporter-curl', () => {
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer'`].join(` \\\n `));
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer'`].join(' \\\n '));
});
test('AWS v4 auth', async () => {
@@ -275,11 +275,9 @@ describe('exporter-curl', () => {
},
}),
).toEqual(
[
`curl 'https://yaak.app'`,
`--aws-sigv4 aws:amz:us-east-1:s3`,
`--user 'ak:sk'`,
].join(` \\\n `),
[`curl 'https://yaak.app'`, '--aws-sigv4 aws:amz:us-east-1:s3', `--user 'ak:sk'`].join(
' \\\n ',
),
);
});
@@ -299,10 +297,10 @@ describe('exporter-curl', () => {
).toEqual(
[
`curl 'https://yaak.app'`,
`--aws-sigv4 aws:amz:us-east-1:s3`,
'--aws-sigv4 aws:amz:us-east-1:s3',
`--user 'ak:sk'`,
`--header 'X-Amz-Security-Token: st'`,
].join(` \\\n `),
].join(' \\\n '),
);
});
@@ -314,15 +312,40 @@ describe('exporter-curl', () => {
authentication: {
location: 'header',
key: 'X-Header',
value: 'my-token'
value: 'my-token',
},
}),
).toEqual(
[
`curl 'https://yaak.app'`,
`--header 'X-Header: my-token'`,
].join(` \\\n `),
);
).toEqual([`curl 'https://yaak.app'`, `--header 'X-Header: my-token'`].join(' \\\n '));
});
test('API key auth header query', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app?hi=there',
urlParameters: [{ name: 'param', value: 'hi' }],
authenticationType: 'apikey',
authentication: {
location: 'query',
key: 'foo',
value: 'bar',
},
}),
).toEqual([`curl 'https://yaak.app?hi=there&param=hi&foo=bar'`].join(' \\\n '));
});
test('API key auth header query with params', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
urlParameters: [{ name: 'param', value: 'hi' }],
authenticationType: 'apikey',
authentication: {
location: 'query',
key: 'foo',
value: 'bar',
},
}),
).toEqual([`curl 'https://yaak.app?param=hi&foo=bar'`].join(' \\\n '));
});
test('API key auth header default', async () => {
@@ -334,12 +357,7 @@ describe('exporter-curl', () => {
location: 'header',
},
}),
).toEqual(
[
`curl 'https://yaak.app'`,
`--header 'X-Api-Key: '`,
].join(` \\\n `),
);
).toEqual([`curl 'https://yaak.app'`, `--header 'X-Api-Key: '`].join(' \\\n '));
});
test('API key auth query', async () => {
@@ -350,14 +368,10 @@ describe('exporter-curl', () => {
authentication: {
location: 'query',
key: 'foo',
value: 'bar-baz'
value: 'bar-baz',
},
}),
).toEqual(
[
`curl 'https://yaak.app?foo=bar-baz'`,
].join(` \\\n `),
);
).toEqual([`curl 'https://yaak.app?foo=bar-baz'`].join(' \\\n '));
});
test('API key auth query with existing', async () => {
@@ -368,14 +382,10 @@ describe('exporter-curl', () => {
authentication: {
location: 'query',
key: 'hi',
value: 'there'
value: 'there',
},
}),
).toEqual(
[
`curl 'https://yaak.app?foo=bar&baz=qux&hi=there'`,
].join(` \\\n `),
);
).toEqual([`curl 'https://yaak.app?foo=bar&baz=qux&hi=there'`].join(' \\\n '));
});
test('API key auth query default', async () => {
@@ -387,11 +397,7 @@ describe('exporter-curl', () => {
location: 'query',
},
}),
).toEqual(
[
`curl 'https://yaak.app?foo=bar&baz=qux&token='`,
].join(` \\\n `),
);
).toEqual([`curl 'https://yaak.app?foo=bar&baz=qux&token='`].join(' \\\n '));
});
test('Stale body data', async () => {
@@ -403,6 +409,6 @@ describe('exporter-curl', () => {
text: 'ignore me',
},
}),
).toEqual([`curl 'https://yaak.app'`].join(` \\\n `));
).toEqual([`curl 'https://yaak.app'`].join(' \\\n '));
});
});

View File

@@ -12,7 +12,6 @@
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

@@ -1,5 +1,5 @@
import type { GrpcRequest, PluginDefinition } from '@yaakapp/api';
import path from 'node:path';
import type { GrpcRequest, PluginDefinition } from '@yaakapp/api';
const NEWLINE = '\\\n ';

View File

@@ -10,7 +10,7 @@ describe('exporter-curl', () => {
},
[],
),
).toEqual([`grpcurl yaak.app`].join(` \\\n `));
).toEqual(['grpcurl yaak.app'].join(' \\\n '));
});
test('Basic metadata', async () => {
expect(
@@ -25,7 +25,7 @@ describe('exporter-curl', () => {
},
[],
),
).toEqual([`grpcurl -H 'aaa: AAA'`, `-H 'bbb: BBB'`, `yaak.app`].join(` \\\n `));
).toEqual([`grpcurl -H 'aaa: AAA'`, `-H 'bbb: BBB'`, 'yaak.app'].join(' \\\n '));
});
test('Basic auth', async () => {
expect(
@@ -40,7 +40,7 @@ describe('exporter-curl', () => {
},
[],
),
).toEqual([`grpcurl -H 'Authorization: Basic dXNlcjpwYXNz'`, `yaak.app`].join(` \\\n `));
).toEqual([`grpcurl -H 'Authorization: Basic dXNlcjpwYXNz'`, 'yaak.app'].join(' \\\n '));
});
test('API key auth', async () => {
@@ -56,7 +56,7 @@ describe('exporter-curl', () => {
},
[],
),
).toEqual([`grpcurl -H 'X-Token: tok'`, `yaak.app`].join(` \\\n `));
).toEqual([`grpcurl -H 'X-Token: tok'`, 'yaak.app'].join(' \\\n '));
});
test('API key auth', async () => {
@@ -73,7 +73,7 @@ describe('exporter-curl', () => {
},
[],
),
).toEqual([`grpcurl`, `yaak.app?token=tok%201`].join(` \\\n `));
).toEqual(['grpcurl', 'yaak.app?token=tok%201'].join(' \\\n '));
});
test('Single proto file', async () => {
@@ -82,8 +82,8 @@ describe('exporter-curl', () => {
`grpcurl -import-path '/foo/bar'`,
`-import-path '/foo'`,
`-proto '/foo/bar/baz.proto'`,
`yaak.app`,
].join(` \\\n `),
'yaak.app',
].join(' \\\n '),
);
});
test('Multiple proto files, same dir', async () => {
@@ -95,8 +95,8 @@ describe('exporter-curl', () => {
`-import-path '/foo'`,
`-proto '/foo/bar/aaa.proto'`,
`-proto '/foo/bar/bbb.proto'`,
`yaak.app`,
].join(` \\\n `),
'yaak.app',
].join(' \\\n '),
);
});
test('Multiple proto files, different dir', async () => {
@@ -110,18 +110,18 @@ describe('exporter-curl', () => {
`-import-path '/xxx'`,
`-proto '/aaa/bbb/ccc.proto'`,
`-proto '/xxx/yyy/zzz.proto'`,
`yaak.app`,
].join(` \\\n `),
'yaak.app',
].join(' \\\n '),
);
});
test('Single include dir', async () => {
expect(await convert({ url: 'https://yaak.app' }, ['/aaa/bbb'])).toEqual(
[`grpcurl -import-path '/aaa/bbb'`, `yaak.app`].join(` \\\n `),
[`grpcurl -import-path '/aaa/bbb'`, 'yaak.app'].join(' \\\n '),
);
});
test('Multiple include dir', async () => {
expect(await convert({ url: 'https://yaak.app' }, ['/aaa/bbb', '/xxx/yyy'])).toEqual(
[`grpcurl -import-path '/aaa/bbb'`, `-import-path '/xxx/yyy'`, `yaak.app`].join(` \\\n `),
[`grpcurl -import-path '/aaa/bbb'`, `-import-path '/xxx/yyy'`, 'yaak.app'].join(' \\\n '),
);
});
test('Mixed proto and dirs', async () => {
@@ -134,8 +134,8 @@ describe('exporter-curl', () => {
`-import-path '/foo'`,
`-import-path '/'`,
`-proto '/foo/bar.proto'`,
`yaak.app`,
].join(` \\\n `),
'yaak.app',
].join(' \\\n '),
);
});
test('Sends data', async () => {
@@ -152,8 +152,8 @@ describe('exporter-curl', () => {
`grpcurl -import-path '/'`,
`-proto '/foo.proto'`,
`-d '{"foo":"bar","baz":1}'`,
`yaak.app`,
].join(` \\\n `),
'yaak.app',
].join(' \\\n '),
);
});
});

View File

@@ -11,7 +11,6 @@
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
"dev": "yaakcli dev"
}
}

View File

@@ -21,13 +21,15 @@ export const plugin: PluginDefinition = {
name: 'key',
label: 'Key',
dynamic: (_ctx, { values }) => {
return values.location === 'query' ? {
label: 'Parameter Name',
description: 'The name of the query parameter to add to the request',
} : {
label: 'Header Name',
description: 'The name of the header to add to the request',
};
return values.location === 'query'
? {
label: 'Parameter Name',
description: 'The name of the query parameter to add to the request',
}
: {
label: 'Header Name',
description: 'The name of the header to add to the request',
};
},
},
{
@@ -45,9 +47,8 @@ export const plugin: PluginDefinition = {
if (location === 'query') {
return { setQueryParameters: [{ name: key, value }] };
} else {
return { setHeaders: [{ name: key, value }] };
}
return { setHeaders: [{ name: key, value }] };
},
},
};

View File

@@ -11,8 +11,7 @@
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
"dev": "yaakcli dev"
},
"dependencies": {
"aws4": "^1.13.2"

View File

@@ -1,8 +1,8 @@
import type { CallHttpAuthenticationResponse } from '@yaakapp-internal/plugins';
import type { PluginDefinition } from '@yaakapp/api';
import aws4 from 'aws4';
import type { Request } from 'aws4';
import { URL } from 'node:url';
import type { PluginDefinition } from '@yaakapp/api';
import type { CallHttpAuthenticationResponse } from '@yaakapp-internal/plugins';
import type { Request } from 'aws4';
import aws4 from 'aws4';
export const plugin: PluginDefinition = {
authentication: {
@@ -57,10 +57,6 @@ export const plugin: PluginDefinition = {
}
}
if (args.method !== 'GET') {
headers['x-amz-content-sha256'] = 'UNSIGNED-PAYLOAD';
}
const signature = aws4.sign(
{
host: url.host,
@@ -69,6 +65,7 @@ export const plugin: PluginDefinition = {
service: String(values.service || 'sts'),
region: values.region ? String(values.region) : undefined,
headers,
doNotEncodePath: true,
},
{
accessKeyId,
@@ -77,11 +74,6 @@ export const plugin: PluginDefinition = {
},
);
// After signing, aws4 will set:
// - opts.headers["Authorization"]
// - opts.headers["X-Amz-Date"]
// - optionally content sha256 header etc
if (signature.headers == null) {
return {};
}

View File

@@ -11,7 +11,6 @@
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
"dev": "yaakcli dev"
}
}

View File

@@ -5,21 +5,24 @@ export const plugin: PluginDefinition = {
name: 'basic',
label: 'Basic Auth',
shortLabel: 'Basic',
args: [{
type: 'text',
name: 'username',
label: 'Username',
optional: true,
}, {
type: 'text',
name: 'password',
label: 'Password',
optional: true,
password: true,
}],
args: [
{
type: 'text',
name: 'username',
label: 'Username',
optional: true,
},
{
type: 'text',
name: 'password',
label: 'Password',
optional: true,
password: true,
},
],
async onApply(_ctx, { values }) {
const { username, password } = values;
const value = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
const value = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
return { setHeaders: [{ name: 'Authorization', value }] };
},
},

View File

@@ -12,7 +12,6 @@
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

@@ -1,5 +1,5 @@
import type { CallHttpAuthenticationRequest } from '@yaakapp-internal/plugins';
import type { PluginDefinition } from '@yaakapp/api';
import type { CallHttpAuthenticationRequest } from '@yaakapp-internal/plugins';
export const plugin: PluginDefinition = {
authentication: {

View File

@@ -7,7 +7,7 @@ const ctx = {} as Context;
describe('auth-bearer', () => {
test('No values', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
await plugin.authentication?.onApply(ctx, {
values: {},
headers: [],
url: 'https://yaak.app',
@@ -19,7 +19,7 @@ describe('auth-bearer', () => {
test('Only token', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
await plugin.authentication?.onApply(ctx, {
values: { token: 'my-token' },
headers: [],
url: 'https://yaak.app',
@@ -31,7 +31,7 @@ describe('auth-bearer', () => {
test('Only prefix', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
await plugin.authentication?.onApply(ctx, {
values: { prefix: 'Hello' },
headers: [],
url: 'https://yaak.app',
@@ -43,7 +43,7 @@ describe('auth-bearer', () => {
test('Prefix and token', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
await plugin.authentication?.onApply(ctx, {
values: { prefix: 'Hello', token: 'my-token' },
headers: [],
url: 'https://yaak.app',
@@ -55,7 +55,7 @@ describe('auth-bearer', () => {
test('Extra spaces', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
await plugin.authentication?.onApply(ctx, {
values: { prefix: '\t Hello ', token: ' \nmy-token ' },
headers: [],
url: 'https://yaak.app',

View File

@@ -11,8 +11,7 @@
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
"dev": "yaakcli dev"
},
"dependencies": {
"jsonwebtoken": "^9.0.2"

View File

@@ -68,12 +68,11 @@ export const plugin: PluginDefinition = {
label: 'Parameter Name',
description: 'The name of the query parameter to add to the request',
};
} else {
return {
label: 'Header Name',
description: 'The name of the header to add to the request',
};
}
return {
label: 'Header Name',
description: 'The name of the header to add to the request',
};
},
},
{
@@ -110,12 +109,11 @@ export const plugin: PluginDefinition = {
const paramName = String(values.name || 'token');
const paramValue = String(values.value || '');
return { setQueryParameters: [{ name: paramName, value: paramValue }] };
} else {
const headerPrefix = values.headerPrefix != null ? values.headerPrefix : 'Bearer';
const headerName = String(values.name || 'Authorization');
const headerValue = `${headerPrefix} ${token}`.trim();
return { setHeaders: [{ name: headerName, value: headerValue }] };
}
const headerPrefix = values.headerPrefix != null ? values.headerPrefix : 'Bearer';
const headerName = String(values.name || 'Authorization');
const headerValue = `${headerPrefix} ${token}`.trim();
return { setHeaders: [{ name: headerName, value: headerValue }] };
},
},
};

View File

@@ -0,0 +1,19 @@
{
"name": "@yaak/auth-ntlm",
"displayName": "NTLM Authentication",
"description": "Authenticate requests using NTLM authentication",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-ntlm"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev"
},
"dependencies": {
"httpntlm": "^1.8.13"
}
}

View File

@@ -0,0 +1,87 @@
import type { PluginDefinition } from '@yaakapp/api';
import { ntlm } from 'httpntlm';
export const plugin: PluginDefinition = {
authentication: {
name: 'windows',
label: 'NTLM Auth',
shortLabel: 'NTLM',
args: [
{
type: 'banner',
color: 'info',
inputs: [
{
type: 'markdown',
content:
'NTLM is still in beta. Please submit any issues to [Feedback](https://yaak.app/feedback).',
},
],
},
{
type: 'text',
name: 'username',
label: 'Username',
optional: true,
},
{
type: 'text',
name: 'password',
label: 'Password',
optional: true,
password: true,
},
{
type: 'accordion',
label: 'Advanced',
inputs: [
{ name: 'domain', label: 'Domain', type: 'text', optional: true },
{ name: 'workstation', label: 'Workstation', type: 'text', optional: true },
],
},
],
async onApply(ctx, { values, method, url }) {
const username = values.username ? String(values.username) : undefined;
const password = values.password ? String(values.password) : undefined;
const domain = values.domain ? String(values.domain) : undefined;
const workstation = values.workstation ? String(values.workstation) : undefined;
const options = {
url,
username,
password,
workstation,
domain,
};
const type1 = ntlm.createType1Message(options);
const negotiateResponse = await ctx.httpRequest.send({
httpRequest: {
method,
url,
headers: [
{ name: 'Authorization', value: type1 },
{ name: 'Connection', value: 'keep-alive' },
],
},
});
const wwwAuthenticateHeader = negotiateResponse.headers.find(
(h) => h.name.toLowerCase() === 'www-authenticate',
);
if (!wwwAuthenticateHeader?.value) {
throw new Error('Unable to find www-authenticate response header for NTLM');
}
const type2 = ntlm.parseType2Message(wwwAuthenticateHeader.value, (err: Error | null) => {
if (err != null) throw err;
});
const type3 = ntlm.createType3Message(type2, options);
return { setHeaders: [{ name: 'Authorization', value: type3 }] };
},
},
};

1
plugins/auth-ntlm/src/modules.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'httpntlm';

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -0,0 +1,19 @@
{
"name": "@yaak/auth-oauth1",
"displayName": "OAuth 1.0",
"description": "Authenticate requests using OAuth 1.0a",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-oauth1"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev"
},
"dependencies": {
"oauth-1.0a": "^2.2.6"
}
}

View File

@@ -0,0 +1,210 @@
import crypto from 'node:crypto';
import type { Context, GetHttpAuthenticationConfigRequest, PluginDefinition } from '@yaakapp/api';
import OAuth from 'oauth-1.0a';
const signatures = {
HMAC_SHA1: 'HMAC-SHA1',
HMAC_SHA256: 'HMAC-SHA256',
HMAC_SHA512: 'HMAC-SHA512',
RSA_SHA1: 'RSA-SHA1',
RSA_SHA256: 'RSA-SHA256',
RSA_SHA512: 'RSA-SHA512',
PLAINTEXT: 'PLAINTEXT',
} as const;
const defaultSig = signatures.HMAC_SHA1;
const pkSigs = Object.values(signatures).filter((k) => k.startsWith('RSA-'));
const nonPkSigs = Object.values(signatures).filter((k) => !pkSigs.includes(k));
type SigMethod = (typeof signatures)[keyof typeof signatures];
function hiddenIfNot(
sigMethod: SigMethod[],
...other: ((values: GetHttpAuthenticationConfigRequest['values']) => boolean)[]
) {
return (_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) => {
const hasGrantType = sigMethod.find((t) => t === String(values.signatureMethod ?? defaultSig));
const hasOtherBools = other.every((t) => t(values));
const show = hasGrantType && hasOtherBools;
return { hidden: !show };
};
}
export const plugin: PluginDefinition = {
authentication: {
name: 'oauth1',
label: 'OAuth 1.0',
shortLabel: 'OAuth 1',
args: [
{
type: 'banner',
color: 'info',
inputs: [
{
type: 'markdown',
content:
'OAuth 1.0 is still in beta. Please submit any issues to [Feedback](https://yaak.app/feedback).',
},
],
},
{
name: 'signatureMethod',
label: 'Signature Method',
type: 'select',
defaultValue: defaultSig,
options: Object.values(signatures).map((v) => ({ label: v, value: v })),
},
{ name: 'consumerKey', label: 'Consumer Key', type: 'text', password: true, optional: true },
{
name: 'consumerSecret',
label: 'Consumer Secret',
type: 'text',
password: true,
optional: true,
},
{
name: 'tokenKey',
label: 'Access Token',
type: 'text',
password: true,
optional: true,
},
{
name: 'tokenSecret',
label: 'Token Secret',
type: 'text',
password: true,
optional: true,
dynamic: hiddenIfNot(nonPkSigs),
},
{
name: 'privateKey',
label: 'Private Key (RSA-SHA1)',
type: 'text',
multiLine: true,
optional: true,
password: true,
placeholder:
'-----BEGIN RSA PRIVATE KEY-----\nPrivate key in PEM format\n-----END RSA PRIVATE KEY-----',
dynamic: hiddenIfNot(pkSigs),
},
{
type: 'accordion',
label: 'Advanced',
inputs: [
{ name: 'callback', label: 'Callback Url', type: 'text', optional: true },
{ name: 'verifier', label: 'Verifier', type: 'text', optional: true, password: true },
{ name: 'timestamp', label: 'Timestamp', type: 'text', optional: true },
{ name: 'nonce', label: 'Nonce', type: 'text', optional: true },
{
name: 'version',
label: 'OAuth Version',
type: 'text',
optional: true,
defaultValue: '1.0',
},
{ name: 'realm', label: 'Realm', type: 'text', optional: true },
],
},
],
onApply(
_ctx,
{ values, method, url },
): {
setHeaders?: { name: string; value: string }[];
setQueryParameters?: { name: string; value: string }[];
} {
const consumerKey = String(values.consumerKey || '');
const consumerSecret = String(values.consumerSecret || '');
const signatureMethod = String(values.signatureMethod || signatures.HMAC_SHA1) as SigMethod;
const version = String(values.version || '1.0');
const realm = String(values.realm || '') || undefined;
const oauth = new OAuth({
consumer: { key: consumerKey, secret: consumerSecret },
signature_method: signatureMethod,
version,
hash_function: hashFunction(signatureMethod),
realm,
});
if (pkSigs.includes(signatureMethod)) {
oauth.getSigningKey = (tokenSecret?: string) => tokenSecret || '';
}
const requestUrl = new URL(url);
// Base request options passed to oauth-1.0a
const requestData: Omit<OAuth.RequestOptions, 'data'> & {
data: Record<string, string | string[]>;
} = {
method,
url: requestUrl.toString(),
includeBodyHash: false,
data: {},
};
// (1) Include existing query params in signature base string
for (const key of requestUrl.searchParams.keys()) {
if (key.startsWith('oauth_')) continue;
const all = requestUrl.searchParams.getAll(key);
const first = all[0];
if (first == null) continue;
requestData.data[key] = all.length > 1 ? all : first;
}
// (2) Manual oauth_* overrides
if (values.callback) requestData.data.oauth_callback = String(values.callback);
if (values.nonce) requestData.data.oauth_nonce = String(values.nonce);
if (values.timestamp) requestData.data.oauth_timestamp = String(values.timestamp);
if (values.verifier) requestData.data.oauth_verifier = String(values.verifier);
let token: OAuth.Token | { key: string } | undefined;
if (pkSigs.includes(signatureMethod)) {
token = {
key: String(values.tokenKey || ''),
secret: String(values.privateKey || ''),
};
} else if (values.tokenKey && values.tokenSecret) {
token = { key: String(values.tokenKey), secret: String(values.tokenSecret) };
} else if (values.tokenKey) {
token = { key: String(values.tokenKey) };
}
const authParams = oauth.authorize(requestData, token as OAuth.Token | undefined);
const { Authorization } = oauth.toHeader(authParams);
return { setHeaders: [{ name: 'Authorization', value: Authorization }] };
},
},
};
function hashFunction(signatureMethod: SigMethod) {
switch (signatureMethod) {
case signatures.HMAC_SHA1:
return (base: string, key: string) =>
crypto.createHmac('sha1', key).update(base).digest('base64');
case signatures.HMAC_SHA256:
return (base: string, key: string) =>
crypto.createHmac('sha256', key).update(base).digest('base64');
case signatures.HMAC_SHA512:
return (base: string, key: string) =>
crypto.createHmac('sha512', key).update(base).digest('base64');
case signatures.RSA_SHA1:
return (base: string, privateKey: string) =>
crypto.createSign('RSA-SHA1').update(base).sign(privateKey, 'base64');
case signatures.RSA_SHA256:
return (base: string, privateKey: string) =>
crypto.createSign('RSA-SHA256').update(base).sign(privateKey, 'base64');
case signatures.RSA_SHA512:
return (base: string, privateKey: string) =>
crypto.createSign('RSA-SHA512').update(base).sign(privateKey, 'base64');
case signatures.PLAINTEXT:
return (base: string) => base;
default:
return (base: string, key: string) =>
crypto.createHmac('sha1', key).update(base).digest('base64');
}
}

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -12,7 +12,6 @@
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

@@ -1,5 +1,5 @@
import type { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api';
import { readFileSync } from 'node:fs';
import type { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api';
import type { AccessTokenRawResponse } from './store';
export async function fetchAccessToken(
@@ -39,15 +39,15 @@ export async function fetchAccessToken(
],
};
if (scope) httpRequest.body!.form.push({ name: 'scope', value: scope });
if (audience) httpRequest.body!.form.push({ name: 'audience', value: audience });
if (scope) httpRequest.body?.form.push({ name: 'scope', value: scope });
if (audience) httpRequest.body?.form.push({ name: 'audience', value: audience });
if (credentialsInBody) {
httpRequest.body!.form.push({ name: 'client_id', value: clientId });
httpRequest.body!.form.push({ name: 'client_secret', value: clientSecret });
httpRequest.body?.form.push({ name: 'client_id', value: clientId });
httpRequest.body?.form.push({ name: 'client_secret', value: clientSecret });
} else {
const value = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
httpRequest.headers!.push({ name: 'Authorization', value });
const value = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
httpRequest.headers?.push({ name: 'Authorization', value });
}
httpRequest.authenticationType = 'none'; // Don't inherit workspace auth
@@ -58,12 +58,11 @@ export async function fetchAccessToken(
const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : '';
if (resp.status < 200 || resp.status >= 300) {
throw new Error(
'Failed to fetch access token with status=' + resp.status + ' and body=' + body,
);
throw new Error(`Failed to fetch access token with status=${resp.status} and body=${body}`);
}
let response;
// biome-ignore lint/suspicious/noExplicitAny: none
let response: any;
try {
response = JSON.parse(body);
} catch {
@@ -71,7 +70,7 @@ export async function fetchAccessToken(
}
if (response.error) {
throw new Error('Failed to fetch access token with ' + response.error);
throw new Error(`Failed to fetch access token with ${response.error}`);
}
return response;

View File

@@ -1,5 +1,5 @@
import type { Context, HttpRequest } from '@yaakapp/api';
import { readFileSync } from 'node:fs';
import type { Context, HttpRequest } from '@yaakapp/api';
import type { AccessToken, AccessTokenRawResponse, TokenStoreArgs } from './store';
import { deleteToken, getToken, storeToken } from './store';
import { isTokenExpired } from './util';
@@ -58,23 +58,23 @@ export async function getOrRefreshAccessToken(
],
};
if (scope) httpRequest.body!.form.push({ name: 'scope', value: scope });
if (scope) httpRequest.body?.form.push({ name: 'scope', value: scope });
if (credentialsInBody) {
httpRequest.body!.form.push({ name: 'client_id', value: clientId });
httpRequest.body!.form.push({ name: 'client_secret', value: clientSecret });
httpRequest.body?.form.push({ name: 'client_id', value: clientId });
httpRequest.body?.form.push({ name: 'client_secret', value: clientSecret });
} else {
const value = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
httpRequest.headers!.push({ name: 'Authorization', value });
const value = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
httpRequest.headers?.push({ name: 'Authorization', value });
}
httpRequest.authenticationType = 'none'; // Don't inherit workspace auth
const resp = await ctx.httpRequest.send({ httpRequest });
if (resp.status === 401) {
// Bad refresh token, so we'll force it to fetch a fresh access token by deleting
// and returning null;
console.log('[oauth2] Unauthorized refresh_token request');
if (resp.status >= 400 && resp.status < 500) {
// Client errors (4xx) indicate the refresh token is invalid, expired, or revoked
// Delete the token and return null to trigger a fresh authorization flow
console.log('[oauth2] Refresh token request failed with client error, deleting token');
await deleteToken(ctx, tokenArgs);
return null;
}
@@ -84,12 +84,11 @@ export async function getOrRefreshAccessToken(
console.log('[oauth2] Got refresh token response', resp.status);
if (resp.status < 200 || resp.status >= 300) {
throw new Error(
'Failed to refresh access token with status=' + resp.status + ' and body=' + body,
);
throw new Error(`Failed to refresh access token with status=${resp.status} and body=${body}`);
}
let response;
// biome-ignore lint/suspicious/noExplicitAny: none
let response: any;
try {
response = JSON.parse(body);
} catch {

View File

@@ -1,5 +1,5 @@
import type { Context } from '@yaakapp/api';
import { createHash, randomBytes } from 'node:crypto';
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import type { AccessToken, TokenStoreArgs } from '../store';
@@ -84,7 +84,7 @@ export async function getAuthorizationCode(
const authorizationUrlStr = authorizationUrl.toString();
console.log('[oauth2] Authorizing', authorizationUrlStr);
// eslint-disable-next-line no-async-promise-executor
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: none
const code = await new Promise<string>(async (resolve, reject) => {
let foundCode = false;
const { close } = await ctx.window.openUrl({
@@ -97,7 +97,7 @@ export async function getAuthorizationCode(
}
},
async onNavigate({ url: urlStr }) {
let code;
let code: string | null;
try {
code = extractCode(urlStr, redirectUri);
} catch (err) {

View File

@@ -1,6 +1,6 @@
import type { Context } from '@yaakapp/api';
import type { AccessToken, AccessTokenRawResponse} from '../store';
import { getDataDirKey , getToken, storeToken } from '../store';
import type { AccessToken, AccessTokenRawResponse } from '../store';
import { getDataDirKey, getToken, storeToken } from '../store';
import { isTokenExpired } from '../util';
export async function getImplicit(
@@ -56,7 +56,7 @@ export async function getImplicit(
);
}
// eslint-disable-next-line no-async-promise-executor
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: none
const newToken = await new Promise<AccessToken>(async (resolve, reject) => {
let foundAccessToken = false;
const authorizationUrlStr = authorizationUrl.toString();

View File

@@ -27,7 +27,7 @@ const grantTypes: FormInputSelectOption[] = [
{ label: 'Client Credentials', value: 'client_credentials' },
];
const defaultGrantType = grantTypes[0]!.value;
const defaultGrantType = grantTypes[0]?.value;
function hiddenIfNot(
grantTypes: GrantType[],
@@ -131,7 +131,6 @@ export const plugin: PluginDefinition = {
type: 'select',
name: 'grantType',
label: 'Grant Type',
hideLabel: true,
defaultValue: defaultGrantType,
options: grantTypes,
},
@@ -288,6 +287,7 @@ export const plugin: PluginDefinition = {
{
type: 'accordion',
label: 'Access Token Response',
inputs: [],
async dynamic(ctx, { contextId, values }) {
const tokenArgs: TokenStoreArgs = {
contextId,
@@ -304,6 +304,7 @@ export const plugin: PluginDefinition = {
inputs: [
{
type: 'editor',
name: 'response',
defaultValue: JSON.stringify(token.response, null, 2),
hideLabel: true,
readOnly: true,
@@ -389,7 +390,7 @@ export const plugin: PluginDefinition = {
credentialsInBody,
});
} else {
throw new Error('Invalid grant type ' + grantType);
throw new Error(`Invalid grant type ${grantType}`);
}
const headerName = stringArg(values, 'headerName') || 'Authorization';
@@ -404,7 +405,7 @@ function stringArgOrNull(
name: string,
): string | null {
const arg = values[name];
if (arg == null || arg == '') return null;
if (arg == null || arg === '') return null;
return `${arg}`;
}

View File

@@ -1,5 +1,5 @@
import type { Context } from '@yaakapp/api';
import { createHash } from 'node:crypto';
import type { Context } from '@yaakapp/api';
export async function storeToken(
ctx: Context,

View File

@@ -50,7 +50,7 @@ export function extractCode(urlStr: string, redirectUri: string | null): string
export function urlMatchesRedirect(url: URL, redirectUrl: string | null): boolean {
if (!redirectUrl) return true;
let redirect;
let redirect: URL;
try {
redirect = new URL(redirectUrl);
} catch {

View File

@@ -1,4 +1,4 @@
import { describe, test, expect } from 'vitest';
import { describe, expect, test } from 'vitest';
import { extractCode } from '../src/util';
describe('extractCode', () => {

View File

@@ -11,8 +11,7 @@
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
"dev": "yaakcli dev"
},
"dependencies": {
"jsonpath-plus": "^10.3.0"

View File

@@ -6,8 +6,7 @@
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
"dev": "yaakcli dev"
},
"dependencies": {
"@xmldom/xmldom": "^0.9.8",

View File

@@ -7,16 +7,15 @@ export const plugin: PluginDefinition = {
name: 'XPath',
description: 'Filter XPath',
onFilter(_ctx, args) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
const doc: any = new DOMParser().parseFromString(args.payload, 'text/xml');
try {
const result = xpath.select(args.filter, doc, false);
if (Array.isArray(result)) {
return { content: result.map((r) => String(r)).join('\n') };
} else {
// Not sure what cases this happens in (?)
return { content: String(result) };
}
// Not sure what cases this happens in (?)
return { content: String(result) };
} catch (err) {
return { content: '', error: `Invalid filter: ${err}` };
}

View File

@@ -7,7 +7,6 @@
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
},
"dependencies": {

View File

@@ -1,4 +1,12 @@
import type { Context, Environment, Folder, HttpRequest, HttpUrlParameter, PluginDefinition, Workspace } from '@yaakapp/api';
import type {
Context,
Environment,
Folder,
HttpRequest,
HttpUrlParameter,
PluginDefinition,
Workspace,
} from '@yaakapp/api';
import type { ControlOperator, ParseEntry } from 'shell-quote';
import { parse } from 'shell-quote';
@@ -28,7 +36,7 @@ const SUPPORTED_FLAGS = [
['url-query'],
['user', 'u'], // Authentication
DATA_FLAGS,
].flatMap((v) => v);
].flat();
const BOOLEAN_FLAGS = ['G', 'get', 'digest'];
@@ -41,12 +49,40 @@ export const plugin: PluginDefinition = {
name: 'cURL',
description: 'Import cURL commands',
onImport(_ctx: Context, args: { text: string }) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
return convertCurl(args.text) as any;
},
},
};
/**
* Decodes escape sequences in shell $'...' strings
* Handles Unicode escape sequences (\uXXXX) and common escape codes
*/
function decodeShellString(str: string): string {
return str
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\r')
.replace(/\\t/g, '\t')
.replace(/\\'/g, "'")
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\');
}
/**
* Checks if a string might contain escape sequences that need decoding
* If so, decodes them; otherwise returns the string as-is
*/
function maybeDecodeEscapeSequences(str: string): string {
// Check if the string contains escape sequences that shell-quote might not handle
if (str.includes('\\u') || str.includes('\\x')) {
return decodeShellString(str);
}
return str;
}
export function convertCurl(rawData: string) {
if (!rawData.match(/^\s*curl /)) {
return null;
@@ -78,9 +114,11 @@ export function convertCurl(rawData: string) {
for (const parseEntry of normalizedParseEntries) {
if (typeof parseEntry === 'string') {
if (parseEntry.startsWith('$')) {
currentCommand.push(parseEntry.slice(1));
// Handle $'...' strings from shell-quote - decode escape sequences
currentCommand.push(decodeShellString(parseEntry.slice(1)));
} else {
currentCommand.push(parseEntry);
// Decode escape sequences that shell-quote might not handle
currentCommand.push(maybeDecodeEscapeSequences(parseEntry));
}
continue;
}
@@ -100,7 +138,7 @@ export function convertCurl(rawData: string) {
if (op?.startsWith('$')) {
// Handle the case where literal like -H $'Header: \'Some Quoted Thing\''
const str = op.slice(2, op.length - 1).replace(/\\'/g, '\'');
const str = decodeShellString(op.slice(2, op.length - 1));
currentCommand.push(str);
continue;
@@ -153,7 +191,7 @@ function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
continue;
}
let value;
let value: string | boolean;
const nextEntry = parseEntries[i + 1];
const hasValue = !BOOLEAN_FLAGS.includes(name);
if (isSingleDash && name.length > 1) {
@@ -169,7 +207,7 @@ function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
}
flagsByName[name] = flagsByName[name] || [];
flagsByName[name]!.push(value);
flagsByName[name]?.push(value);
} else if (parseEntry) {
singletons.push(parseEntry);
}
@@ -184,7 +222,11 @@ function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
const urlParameters: HttpUrlParameter[] =
search?.split('&').map((p) => {
const v = splitOnce(p, '=');
return { name: decodeURIComponent(v[0] ?? ''), value: decodeURIComponent(v[1] ?? ''), enabled: true };
return {
name: decodeURIComponent(v[0] ?? ''),
value: decodeURIComponent(v[1] ?? ''),
enabled: true,
};
}) ?? [];
const url = baseUrl ?? urlArg;
@@ -209,15 +251,15 @@ function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
const authenticationType = username ? (isDigest ? 'digest' : 'basic') : null;
const authentication = username
? {
username: username.trim(),
password: (password ?? '').trim(),
}
username: username.trim(),
password: (password ?? '').trim(),
}
: {};
// Headers
const headers = [
...((flagsByName['header'] as string[] | undefined) || []),
...((flagsByName['H'] as string[] | undefined) || []),
...((flagsByName.header as string[] | undefined) || []),
...((flagsByName.H as string[] | undefined) || []),
].map((header) => {
const [name, value] = header.split(/:(.*)$/);
// remove final colon from header name if present
@@ -237,8 +279,8 @@ function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
// Cookies
const cookieHeaderValue = [
...((flagsByName['cookie'] as string[] | undefined) || []),
...((flagsByName['b'] as string[] | undefined) || []),
...((flagsByName.cookie as string[] | undefined) || []),
...((flagsByName.b as string[] | undefined) || []),
]
.map((str) => {
const name = str.split('=', 1)[0];
@@ -269,8 +311,8 @@ function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
// Body (Multipart Form Data)
const formDataParams = [
...((flagsByName['form'] as string[] | undefined) || []),
...((flagsByName['F'] as string[] | undefined) || []),
...((flagsByName.form as string[] | undefined) || []),
...((flagsByName.F as string[] | undefined) || []),
].map((str) => {
const parts = str.split('=');
const name = parts[0] ?? '';
@@ -281,9 +323,9 @@ function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
};
if (value.indexOf('@') === 0) {
item['file'] = value.slice(1);
item.file = value.slice(1);
} else {
item['value'] = value;
item.value = value;
}
return item;
@@ -384,7 +426,7 @@ function pairsToDataParameters(keyedPairs: FlagsByName): DataParameter[] {
for (const p of pairs) {
if (typeof p !== 'string') continue;
const params = p.split("&");
const params = p.split('&');
for (const param of params) {
const [name, value] = splitOnce(param, '=');
if (param.startsWith('@')) {
@@ -398,7 +440,7 @@ function pairsToDataParameters(keyedPairs: FlagsByName): DataParameter[] {
} else {
dataParameters.push({
name: name ?? '',
value: flagName === 'data-urlencode' ? encodeURIComponent(value ?? '') : value ?? '',
value: flagName === 'data-urlencode' ? encodeURIComponent(value ?? '') : (value ?? ''),
enabled: true,
});
}
@@ -415,8 +457,8 @@ const getPairValue = <T extends string | boolean>(
names: string[],
) => {
for (const name of names) {
if (pairsByName[name] && pairsByName[name]!.length) {
return pairsByName[name]![0] as T;
if (pairsByName[name]?.length) {
return pairsByName[name]?.[0] as T;
}
}

View File

@@ -374,7 +374,7 @@ describe('importer-curl', () => {
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: "POST",
method: 'POST',
bodyType: 'application/x-www-form-urlencoded',
body: {
form: [{ name: 'foo', value: 'bar=baz', enabled: true }],
@@ -391,6 +391,56 @@ describe('importer-curl', () => {
},
});
});
test('Imports data with Unicode escape sequences', () => {
expect(
convertCurl(
`curl 'https://yaak.app' -H 'Content-Type: application/json' --data-raw $'{"query":"SearchQueryInput\\u0021"}' -X POST`,
),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: 'POST',
headers: [{ name: 'Content-Type', value: 'application/json', enabled: true }],
bodyType: 'application/json',
body: { text: '{"query":"SearchQueryInput!"}' },
}),
],
},
});
});
test('Imports data with multiple escape sequences', () => {
expect(
convertCurl(
`curl 'https://yaak.app' --data-raw $'Line1\\nLine2\\tTab\\u0021Exclamation' -X POST`,
),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/x-www-form-urlencoded',
body: {
form: [{ name: 'Line1\nLine2\tTab!Exclamation', value: '', enabled: true }],
},
headers: [
{
enabled: true,
name: 'Content-Type',
value: 'application/x-www-form-urlencoded',
},
],
}),
],
},
});
});
});
const idCount: Partial<Record<string, number>> = {};

View File

@@ -7,7 +7,6 @@
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
},
"dependencies": {

View File

@@ -1,9 +1,3 @@
export function convertSyntax(variable: string): string {
if (!isJSString(variable)) return variable;
return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}');
}
export function isJSObject(obj: unknown) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
@@ -22,13 +16,30 @@ export function convertId(id: string): string {
export function deleteUndefinedAttrs<T>(obj: T): T {
if (Array.isArray(obj) && obj != null) {
return obj.map(deleteUndefinedAttrs) as T;
} else if (typeof obj === 'object' && obj != null) {
}
if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, deleteUndefinedAttrs(v)]),
) as T;
} else {
return obj;
}
return obj;
}
/** Recursively render all nested object properties */
export function convertTemplateSyntax<T>(obj: T): T {
if (typeof obj === 'string') {
// biome-ignore lint/suspicious/noTemplateCurlyInString: Yaak template syntax
return obj.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') as T;
}
if (Array.isArray(obj) && obj != null) {
return obj.map(convertTemplateSyntax) as T;
}
if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)]),
) as T;
}
return obj;
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// biome-ignore-all lint/suspicious/noExplicitAny: too flexible for strict types
import type { PartialImportResources } from '@yaakapp/api';
import { convertId, convertSyntax, isJSObject } from './common';
import { convertId, convertTemplateSyntax, isJSObject } from './common';
export function convertInsomniaV4(parsed: any) {
if (!Array.isArray(parsed.resources)) return null;
@@ -60,7 +60,7 @@ export function convertInsomniaV4(parsed: any) {
resources.environments = resources.environments.filter(Boolean);
resources.workspaces = resources.workspaces.filter(Boolean);
return { resources };
return { resources: convertTemplateSyntax(resources) };
}
function importHttpRequest(r: any, workspaceId: string): PartialImportResources['httpRequests'][0] {
@@ -90,10 +90,10 @@ function importHttpRequest(r: any, workspaceId: string): PartialImportResources[
};
} else if (r.body?.mimeType === 'application/graphql') {
bodyType = 'graphql';
body = { text: convertSyntax(r.body.text ?? '') };
body = { text: r.body.text ?? '' };
} else if (r.body?.mimeType === 'application/json') {
bodyType = 'application/json';
body = { text: convertSyntax(r.body.text ?? '') };
body = { text: r.body.text ?? '' };
}
let authenticationType: string | null = null;
@@ -101,13 +101,13 @@ function importHttpRequest(r: any, workspaceId: string): PartialImportResources[
if (r.authentication.type === 'bearer') {
authenticationType = 'bearer';
authentication = {
token: convertSyntax(r.authentication.token),
token: r.authentication.token,
};
} else if (r.authentication.type === 'basic') {
authenticationType = 'basic';
authentication = {
username: convertSyntax(r.authentication.username),
password: convertSyntax(r.authentication.password),
username: r.authentication.username,
password: r.authentication.password,
};
}
@@ -121,13 +121,12 @@ function importHttpRequest(r: any, workspaceId: string): PartialImportResources[
sortPriority: r.metaSortKey,
name: r.name,
description: r.description || undefined,
url: convertSyntax(r.url),
urlParameters: (r.parameters ?? [])
.map((p: any) => ({
enabled: !p.disabled,
name: p.name ?? '',
value: p.value ?? '',
})),
url: r.url,
urlParameters: (r.parameters ?? []).map((p: any) => ({
enabled: !p.disabled,
name: p.name ?? '',
value: p.value ?? '',
})),
body,
bodyType,
authentication,
@@ -158,7 +157,7 @@ function importGrpcRequest(r: any, workspaceId: string): PartialImportResources[
sortPriority: r.metaSortKey,
name: r.name,
description: r.description || undefined,
url: convertSyntax(r.url),
url: r.url,
service,
method,
message: r.body?.text ?? '',
@@ -188,9 +187,9 @@ function importFolder(f: any, workspaceId: string): PartialImportResources['fold
function importEnvironment(
e: any,
workspaceId: string,
isParent?: boolean,
isParentOg?: boolean,
): PartialImportResources['environments'][0] {
isParent ??= e.parentId === workspaceId;
const isParent = isParentOg ?? e.parentId === workspaceId;
return {
id: convertId(e._id),
createdAt: e.created ? new Date(e.created).toISOString().replace('Z', '') : undefined,

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// biome-ignore-all lint/suspicious/noExplicitAny: too flexible for strict types
import type { PartialImportResources } from '@yaakapp/api';
import { convertId, convertSyntax, isJSObject } from './common';
import { convertId, convertTemplateSyntax, isJSObject } from './common';
export function convertInsomniaV5(parsed: any) {
// Assert parsed is object
@@ -69,7 +69,7 @@ export function convertInsomniaV5(parsed: any) {
resources.environments = resources.environments.filter(Boolean);
resources.workspaces = resources.workspaces.filter(Boolean);
return { resources };
return { resources: convertTemplateSyntax(resources) };
}
function importHttpRequest(
@@ -108,10 +108,10 @@ function importHttpRequest(
};
} else if (r.body?.mimeType === 'application/graphql') {
bodyType = 'graphql';
body = { text: convertSyntax(r.body.text ?? '') };
body = { text: r.body.text ?? '' };
} else if (r.body?.mimeType === 'application/json') {
bodyType = 'application/json';
body = { text: convertSyntax(r.body.text ?? '') };
body = { text: r.body.text ?? '' };
}
return {
@@ -124,13 +124,12 @@ function importHttpRequest(
model: 'http_request',
name: r.name,
description: r.meta?.description || undefined,
url: convertSyntax(r.url),
urlParameters: (r.parameters ?? [])
.map((p: any) => ({
enabled: !p.disabled,
name: p.name ?? '',
value: p.value ?? '',
})),
url: r.url,
urlParameters: (r.parameters ?? []).map((p: any) => ({
enabled: !p.disabled,
name: p.name ?? '',
value: p.value ?? '',
})),
body,
bodyType,
method: r.method,
@@ -163,7 +162,7 @@ function importGrpcRequest(
sortPriority: sortKey,
name: r.name,
description: r.description || undefined,
url: convertSyntax(r.url),
url: r.url,
service,
method,
message: r.body?.text ?? '',
@@ -197,7 +196,7 @@ function importWebsocketRequest(
sortPriority: sortKey,
name: r.name,
description: r.description || undefined,
url: convertSyntax(r.url),
url: r.url,
message: r.body?.text ?? '',
...importHeaders(r),
...importAuthentication(r),
@@ -221,13 +220,13 @@ function importAuthentication(obj: any) {
if (obj.authentication?.type === 'bearer') {
authenticationType = 'bearer';
authentication = {
token: convertSyntax(obj.authentication.token),
token: obj.authentication.token,
};
} else if (obj.authentication?.type === 'basic') {
authenticationType = 'basic';
authentication = {
username: convertSyntax(obj.authentication.username),
password: convertSyntax(obj.authentication.password),
username: obj.authentication.username,
password: obj.authentication.password,
};
}
@@ -250,7 +249,7 @@ function importFolder(
let environment: PartialImportResources['environments'][0] | null = null;
if (Object.keys(f.environment ?? {}).length > 0) {
environment = {
id: convertId(id + 'folder'),
id: convertId(`${id}folder`),
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId),

View File

@@ -46,6 +46,10 @@ collection:
name: X-Header
value: xxxx
disabled: false
- id: pair_ab4b870278e943cba6babf5a73e213e3
name: "{{ _.ApiHeaderName }}"
value: "{{ _.ApiKey }}"
disabled: false
authentication:
type: basic
useISO88591: false

View File

@@ -127,6 +127,11 @@
"enabled": true,
"name": "X-Header",
"value": "xxxx"
},
{
"enabled": true,
"name": "${[ApiHeaderName ]}",
"value": "${[ApiKey ]}"
}
],
"id": "GENERATE_ID::req_d72fff2a6b104b91a2ebe9de9edd2785",

View File

@@ -13,9 +13,12 @@ describe('importer-yaak', () => {
continue;
}
test('Imports ' + fixture, () => {
test(`Imports ${fixture}`, () => {
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
const expected = fs.readFileSync(path.join(p, fixture.replace(/.input\..*/, '.output.json')), 'utf-8');
const expected = fs.readFileSync(
path.join(p, fixture.replace(/.input\..*/, '.output.json')),
'utf-8',
);
const result = convertInsomnia(contents);
// console.log(JSON.stringify(result, null, 2))
expect(result).toEqual(parseJsonOrYaml(expected));

View File

@@ -7,7 +7,6 @@
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
},
"dependencies": {

View File

@@ -14,10 +14,11 @@ export const plugin: PluginDefinition = {
};
export async function convertOpenApi(contents: string): Promise<ImportPluginResponse | undefined> {
let postmanCollection;
// biome-ignore lint/suspicious/noExplicitAny: none
let postmanCollection: any;
try {
postmanCollection = await new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
convert({ type: 'string', data: contents }, {}, (err, result: any) => {
if (err != null) reject(err);

View File

@@ -13,7 +13,7 @@ describe('importer-openapi', () => {
});
for (const fixture of fixtures) {
test('Imports ' + fixture, async () => {
test(`Imports ${fixture}`, async () => {
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
const imported = await convertOpenApi(contents);
expect(imported?.resources.workspaces).toEqual([

View File

@@ -8,7 +8,6 @@
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

@@ -93,39 +93,37 @@ function toRecord<T>(value: Record<string, T> | unknown): Record<string, T> {
function toArray<T>(value: unknown): T[] {
if (Object.prototype.toString.call(value) === '[object Array]') return value as T[];
else return [] as T[];
return [] as T[];
}
/** Recursively render all nested object properties */
function convertTemplateSyntax<T>(obj: T): T {
if (typeof obj === 'string') {
return obj.replace(
/{{\s*(_\.)?([^}]*)\s*}}/g,
(_m, _dot, expr) => '${[' + expr.trim() + ']}',
) as T;
} else if (Array.isArray(obj) && obj != null) {
return obj.replace(/{{\s*(_\.)?([^}]*)\s*}}/g, (_m, _dot, expr) => `\${[${expr.trim()}]}`) as T;
}
if (Array.isArray(obj) && obj != null) {
return obj.map(convertTemplateSyntax) as T;
} else if (typeof obj === 'object' && obj != null) {
}
if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [k, convertTemplateSyntax(v)]),
) as T;
} else {
return obj;
}
return obj;
}
function deleteUndefinedAttrs<T>(obj: T): T {
if (Array.isArray(obj) && obj != null) {
return obj.map(deleteUndefinedAttrs) as T;
} else if (typeof obj === 'object' && obj != null) {
}
if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj as Record<string, unknown>)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, deleteUndefinedAttrs(v)]),
) as T;
} else {
return obj;
}
return obj;
}
const idCount: Partial<Record<string, number>> = {};

View File

@@ -12,7 +12,7 @@ describe('importer-postman-environment', () => {
continue;
}
test('Imports ' + fixture, () => {
test(`Imports ${fixture}`, () => {
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
const expected = fs.readFileSync(path.join(p, fixture.replace('.input', '.output')), 'utf-8');
const result = convertPostmanEnvironment(contents);

View File

@@ -8,7 +8,6 @@
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

@@ -205,7 +205,7 @@ function convertUrl(rawUrl: string | unknown): Pick<HttpRequest, 'url' | 'urlPar
if ('variable' in url && Array.isArray(url.variable) && url.variable.length > 0) {
for (const v of url.variable) {
params.push({
name: ':' + (v.key ?? ''),
name: `:${v.key ?? ''}`,
value: v.value ?? '',
enabled: !v.disabled,
});
@@ -414,7 +414,8 @@ function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | '
),
},
};
} else if (body.mode === 'urlencoded') {
}
if (body.mode === 'urlencoded') {
return {
headers: [
{
@@ -432,7 +433,8 @@ function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | '
})),
},
};
} else if (body.mode === 'formdata') {
}
if (body.mode === 'formdata') {
return {
headers: [
{
@@ -459,7 +461,8 @@ function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | '
),
},
};
} else if (body.mode === 'raw') {
}
if (body.mode === 'raw') {
return {
headers: [
{
@@ -473,7 +476,8 @@ function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | '
text: body.raw ?? '',
},
};
} else if (body.mode === 'file') {
}
if (body.mode === 'file') {
return {
headers: [],
bodyType: 'binary',
@@ -481,9 +485,8 @@ function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | '
filePath: body.file?.src,
},
};
} else {
return { headers: [], bodyType: null, body: {} };
}
return { headers: [], bodyType: null, body: {} };
}
function parseJSONToRecord<T>(jsonStr: string): Record<string, T> | null {
@@ -503,7 +506,7 @@ function toRecord<T>(value: Record<string, T> | unknown): Record<string, T> {
function toArray<T>(value: unknown): T[] {
if (Object.prototype.toString.call(value) === '[object Array]') return value as T[];
else return [];
return [];
}
/** Recursively render all nested object properties */
@@ -511,31 +514,32 @@ function convertTemplateSyntax<T>(obj: T): T {
if (typeof obj === 'string') {
return obj.replace(
/{{\s*(_\.)?([^}]*)\s*}}/g,
(_m, _dot, expr) => '${[' + expr.trim().replace(/^vault:/, '') + ']}',
(_m, _dot, expr) => `\${[${expr.trim().replace(/^vault:/, '')}]}`,
) as T;
} else if (Array.isArray(obj) && obj != null) {
}
if (Array.isArray(obj) && obj != null) {
return obj.map(convertTemplateSyntax) as T;
} else if (typeof obj === 'object' && obj != null) {
}
if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)]),
) as T;
} else {
return obj;
}
return obj;
}
function deleteUndefinedAttrs<T>(obj: T): T {
if (Array.isArray(obj) && obj != null) {
return obj.map(deleteUndefinedAttrs) as T;
} else if (typeof obj === 'object' && obj != null) {
}
if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, deleteUndefinedAttrs(v)]),
) as T;
} else {
return obj;
}
return obj;
}
const idCount: Partial<Record<string, number>> = {};

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,38 @@
{
"info": {
"_postman_id": "9e6dfada-256c-49ea-a38f-7d1b05b7ca2d",
"name": "New Collection",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
"_exporter_id": "18798"
},
"item": [
{
"name": "Top Folder",
"item": [
{
"name": "Nested Folder",
"item": [
{
"name": "Request 1",
"request": {
"method": "GET"
}
}
]
},
{
"name": "Request 2",
"request": {
"method": "GET"
}
}
]
},
{
"name": "Request 3",
"request": {
"method": "GET"
}
}
]
"info": {
"_postman_id": "9e6dfada-256c-49ea-a38f-7d1b05b7ca2d",
"name": "New Collection",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
"_exporter_id": "18798"
},
"item": [
{
"name": "Top Folder",
"item": [
{
"name": "Nested Folder",
"item": [
{
"name": "Request 1",
"request": {
"method": "GET"
}
}
]
},
{
"name": "Request 2",
"request": {
"method": "GET"
}
}
]
},
{
"name": "Request 3",
"request": {
"method": "GET"
}
}
]
}

View File

@@ -47,14 +47,8 @@
},
"url": {
"raw": "example.com/:foo/:bar?q=qqq&",
"host": [
"example",
"com"
],
"path": [
":foo",
":bar"
],
"host": ["example", "com"],
"path": [":foo", ":bar"],
"query": [
{
"key": "disabled",
@@ -110,9 +104,7 @@
"script": {
"type": "text/javascript",
"packages": {},
"exec": [
""
]
"exec": [""]
}
},
{
@@ -120,9 +112,7 @@
"script": {
"type": "text/javascript",
"packages": {},
"exec": [
""
]
"exec": [""]
}
}
],

View File

@@ -12,7 +12,7 @@ describe('importer-postman', () => {
continue;
}
test('Imports ' + fixture, () => {
test(`Imports ${fixture}`, () => {
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
const expected = fs.readFileSync(path.join(p, fixture.replace('.input', '.output')), 'utf-8');
const result = convertPostman(contents);

View File

@@ -7,7 +7,6 @@
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

@@ -11,7 +11,8 @@ export const plugin: PluginDefinition = {
};
export function migrateImport(contents: string) {
let parsed;
// biome-ignore lint/suspicious/noExplicitAny: none
let parsed: any;
try {
parsed = JSON.parse(contents);
} catch {
@@ -30,7 +31,7 @@ export function migrateImport(contents: string) {
// Migrate v1 to v2 -- changes requests to httpRequests
if ('requests' in parsed.resources) {
parsed.resources.httpRequests = parsed.resources.requests;
delete parsed.resources['requests'];
parsed.resources.requests = undefined;
}
// Migrate v2 to v3
@@ -38,7 +39,7 @@ export function migrateImport(contents: string) {
if ('variables' in workspace) {
// Create the base environment
const baseEnvironment: Partial<Environment> = {
id: `GENERATE_ID::base_env_${workspace['id']}`,
id: `GENERATE_ID::base_env_${workspace.id}`,
name: 'Global Variables',
variables: workspace.variables,
workspaceId: workspace.id,
@@ -47,7 +48,7 @@ export function migrateImport(contents: string) {
parsed.resources.environments.push(baseEnvironment);
// Delete variables key from the workspace
delete workspace.variables;
workspace.variables = undefined;
// Add environmentId to relevant environments
for (const environment of parsed.resources.environments) {
@@ -62,7 +63,7 @@ export function migrateImport(contents: string) {
for (const environment of parsed.resources.environments ?? []) {
if ('environmentId' in environment) {
environment.base = environment.environmentId == null;
delete environment.environmentId;
environment.environmentId = undefined;
}
}
@@ -71,11 +72,11 @@ export function migrateImport(contents: string) {
if ('base' in environment && environment.base && environment.parentModel == null) {
environment.parentModel = 'workspace';
environment.parentId = null;
delete environment.base;
environment.base = undefined;
} else if ('base' in environment && !environment.base && environment.parentModel == null) {
environment.parentModel = 'environment';
environment.parentId = null;
delete environment.base;
environment.base = undefined;
}
}

View File

@@ -0,0 +1,19 @@
{
"name": "@yaak/template-function-1password",
"displayName": "1Password Template Functions",
"description": "Template function for accessing 1Password secrets",
"private": true,
"version": "0.1.0",
"scripts": {
"build": "run-s build:*",
"build:1-build": "yaakcli build",
"build:2-cpywasm": "cpx \"../../node_modules/@1password/sdk-core/nodejs/core_bg.wasm\" build/",
"dev": "yaakcli dev"
},
"dependencies": {
"@1password/sdk": "^0.4.0-beta.2"
},
"devDependencies": {
"cpx": "^1.5.0"
}
}

View File

@@ -0,0 +1,225 @@
import crypto from 'node:crypto';
import type { Client } from '@1password/sdk';
import { createClient, DesktopAuth } from '@1password/sdk';
import type { JsonPrimitive, PluginDefinition } from '@yaakapp/api';
import type { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins';
const _clients: Record<string, Client> = {};
async function op(args: CallTemplateFunctionArgs): Promise<{ client?: Client; error?: unknown }> {
let authMethod: string | DesktopAuth | null = null;
let hash: string | null = null;
switch (args.values.authMethod) {
case 'desktop': {
const account = args.values.account;
if (typeof account !== 'string' || !account) return { error: 'Missing account name' };
hash = crypto.createHash('sha256').update(`desktop:${account}`).digest('hex');
authMethod = new DesktopAuth(account);
break;
}
case 'token': {
const token = args.values.token;
if (typeof token !== 'string' || !token) return { error: 'Missing service token' };
hash = crypto.createHash('sha256').update(`token:${token}`).digest('hex');
authMethod = token;
break;
}
}
if (hash == null || authMethod == null) return { error: 'Invalid authentication method' };
try {
_clients[hash] ??= await createClient({
auth: authMethod,
integrationName: 'Yaak 1Password Plugin',
integrationVersion: 'v1.0.0',
});
} catch (e) {
return { error: e };
}
return { client: _clients[hash] };
}
async function getValue(
args: CallTemplateFunctionArgs,
vaultId?: JsonPrimitive,
itemId?: JsonPrimitive,
fieldId?: JsonPrimitive,
): Promise<{ value?: string; error?: unknown }> {
const { client, error } = await op(args);
if (!client) return { error };
if (vaultId && typeof vaultId === 'string') {
try {
await client.vaults.getOverview(vaultId);
} catch {
return { error: `Vault ${vaultId} not found` };
}
} else {
return { error: 'No vault specified' };
}
if (itemId && typeof itemId === 'string') {
try {
const item = await client.items.get(vaultId, itemId);
if (fieldId && typeof fieldId === 'string') {
const field = item.fields.find((f) => f.id === fieldId);
if (field) {
return { value: field.value };
} else {
return { error: `Field ${fieldId} not found in item ${itemId} in vault ${vaultId}` };
}
}
} catch {
return { error: `Item ${itemId} not found in vault ${vaultId}` };
}
} else {
return { error: 'No item specified' };
}
return {};
}
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: '1password.item',
description: 'Get a secret',
previewArgs: ['field'],
args: [
{
type: 'h_stack',
inputs: [
{
name: 'authMethod',
type: 'select',
label: 'Authentication Method',
description: '',
options: [
{
label: 'Service Account',
value: 'token',
},
{
label: 'Desktop App',
value: 'desktop',
},
],
defaultValue: 'token',
},
{
name: 'account',
type: 'text',
description: '',
dynamic(_ctx, args) {
switch (args.values.authMethod) {
case 'desktop':
return {
name: 'account',
label: 'Account Name',
description:
'Account name can be taken from the sidebar of the 1Password App. Make sure you\'re on the BETA version of the 1Password app and have "Integrate with other apps" enabled in Settings > Developer.',
};
case 'token':
return {
name: 'token',
label: 'Token',
description:
'Token can be generated from the 1Password website by visiting Developer > Service Accounts',
// biome-ignore lint/suspicious/noTemplateCurlyInString: Yaak template syntax
defaultValue: '${[1PASSWORD_TOKEN]}',
password: true,
};
}
return { hidden: true };
},
},
],
},
{
name: 'vault',
label: 'Vault',
type: 'select',
options: [],
async dynamic(_ctx, args) {
const { client } = await op(args);
if (client == null) return { hidden: true };
// Fetches a secret.
const vaults = await client.vaults.list({ decryptDetails: true });
return {
options: vaults.map((vault) => ({
label: `${vault.title} (${vault.activeItemCount} Items)`,
value: vault.id,
})),
};
},
},
{
name: 'item',
label: 'Item',
type: 'select',
options: [],
async dynamic(_ctx, args) {
const { client } = await op(args);
if (client == null) return { hidden: true };
const vaultId = args.values.vault;
if (typeof vaultId !== 'string') return { hidden: true };
try {
const items = await client.items.list(vaultId);
return {
options: items.map((item) => ({
label: `${item.title} ${item.category}`,
value: item.id,
})),
};
} catch {
// Hide as we can't list the items for this vault
return { hidden: true };
}
},
},
{
name: 'field',
label: 'Field',
type: 'select',
options: [],
async dynamic(_ctx, args) {
const { client } = await op(args);
if (client == null) return { hidden: true };
const vaultId = args.values.vault;
const itemId = args.values.item;
if (typeof vaultId !== 'string' || typeof itemId !== 'string') {
return { hidden: true };
}
try {
const item = await client.items.get(vaultId, itemId);
return {
options: item.fields.map((field) => ({ label: field.title, value: field.id })),
};
} catch {
// Hide as we can't find the item within this vault
return { hidden: true };
}
},
},
],
async onRender(_ctx, args) {
const vaultId = args.values.vault;
const itemId = args.values.item;
const fieldId = args.values.field;
const { value, error } = await getValue(args, vaultId, itemId, fieldId);
if (error) {
throw error;
}
return value ?? '';
},
},
],
};

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -6,7 +6,6 @@
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
"dev": "yaakcli dev"
}
}

View File

@@ -5,15 +5,18 @@ export const plugin: PluginDefinition = {
{
name: 'cookie.value',
description: 'Read the value of a cookie in the jar, by name',
previewArgs: ['name'],
args: [
{
type: 'text',
name: 'cookie_name',
name: 'name',
label: 'Cookie Name',
},
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return ctx.cookies.getValue({ name: String(args.values.cookie_name) });
// The legacy name was cookie_name, but we changed it
const name = args.values.cookie_name ?? args.values.name;
return ctx.cookies.getValue({ name: String(name) });
},
},
],

View File

@@ -0,0 +1,11 @@
{
"name": "@yaak/template-function-ctx",
"displayName": "Window Template Functions",
"description": "Template functions for accessing attributes of the current window",
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev"
}
}

Some files were not shown because too many files have changed in this diff Show More