Compare commits

...

96 Commits

Author SHA1 Message Date
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
476 changed files with 10016 additions and 7159 deletions

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;<!-- 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

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,14 +58,14 @@ 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
@@ -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,7 +49,7 @@ 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;
},
},
@@ -100,7 +108,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 = op.slice(2, op.length - 1).replace(/\\'/g, "'");
currentCommand.push(str);
continue;
@@ -153,7 +161,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 +177,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 +192,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 +221,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 +249,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 +281,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 +293,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 +396,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 +410,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 +427,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 }],

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>> = {};

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,126 @@
import crypto from 'node:crypto';
import type { Client } from '@1password/sdk';
import { createClient } from '@1password/sdk';
import type { PluginDefinition } from '@yaakapp/api';
import type { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins';
const _clients: Record<string, Client> = {};
async function op(args: CallTemplateFunctionArgs): Promise<Client | null> {
const token = args.values.token;
if (typeof token !== 'string') return null;
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
try {
_clients[tokenHash] ??= await createClient({
auth: token,
integrationName: 'Yaak 1Password Plugin',
integrationVersion: 'v1.0.0',
});
} catch {
return null;
}
return _clients[tokenHash];
}
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: '1password.item',
description: 'Get a secret',
previewArgs: ['field'],
args: [
{
name: 'token',
type: 'text',
label: '1Password Service Account 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,
},
{
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 };
const items = await client.items.list(vaultId);
return {
options: items.map((item) => ({
label: `${item.title} ${item.category}`,
value: item.id,
})),
};
},
},
{
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 };
}
const item = await client.items.get(vaultId, itemId);
return {
options: item.fields.map((field) => ({ label: field.title, value: field.id })),
};
},
},
],
async onRender(_ctx, args) {
const client = await op(args);
if (client == null) throw new Error('Invalid token');
const vaultId = args.values.vault;
const itemId = args.values.item;
const fieldId = args.values.field;
if (
typeof vaultId !== 'string' ||
typeof itemId !== 'string' ||
typeof fieldId !== 'string'
) {
return null;
}
const item = await client.items.get(vaultId, itemId);
const field = item.fields.find((f) => f.id === fieldId);
if (field == null) {
throw new Error(`Field not found: ${fieldId}`);
}
return field.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"
}
}

View File

@@ -0,0 +1,30 @@
import type { PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'ctx.request',
description: 'Get the ID of the currently active request',
args: [],
async onRender(ctx) {
return ctx.window.requestId();
},
},
{
name: 'ctx.environment',
description: 'Get the ID of the currently active environment',
args: [],
async onRender(ctx) {
return ctx.window.environmentId();
},
},
{
name: 'ctx.workspace',
description: 'Get the ID of the currently active workspace',
args: [],
async onRender(ctx) {
return ctx.window.workspaceId();
},
},
],
};

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