Compare commits

..

157 Commits

Author SHA1 Message Date
Gregory Schier
569e506f32 Try rel imports 2025-11-18 09:09:56 -08:00
Gregory Schier
6d7a08758f Try rel imports 2025-11-18 09:09:39 -08:00
Gregory Schier
20dfd50a7d Try without && 2025-11-18 09:08:16 -08:00
Gregory Schier
d747eb5e45 Try again 2025-11-18 09:02:10 -08:00
Gregory Schier
81fca7c54f Reverse order 2025-11-18 08:55:46 -08:00
Gregory Schier
5465efea84 Don't build in vendor-plugins 2025-11-18 08:53:55 -08:00
Gregory Schier
96a3630725 Fix package build order 2025-11-18 08:48:58 -08:00
Gregory Schier
f1b6c89186 Fix package types? 2025-11-18 08:39:07 -08:00
Gregory Schier
9c52652a5e Move a bunch of git ops to use the git binary (#302) 2025-11-17 15:22:39 -08:00
Gregory Schier
84219571e8 Improved prompt function add add ctx.* functions (#301) 2025-11-15 08:19:58 -08:00
iammordaty
7ced183b11 Change wording from "Show sidebar" to "Toggle sidebar" (#300) 2025-11-13 13:40:51 -08:00
Gregor Majcen
593a7ab7e5 Add an option to allow jsonpath/xpath to return as array (#297)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-11-13 05:57:11 -08:00
Zhizhen He
a4c4663011 Merge pull request #298
* fix: replace unstable from_mins to stable from_secs
2025-11-12 07:01:58 -08:00
jzhangdev
5745a96106 Merge pull request #299
* Fix scroll bar layout in EventStreamViewer
2025-11-12 06:58:35 -08:00
Gregory Schier
5449e3c620 Add sidebar action to select the active request 2025-11-11 14:38:05 -08:00
Gregory Schier
7b6278405c Focus request/folder after creation 2025-11-11 14:11:43 -08:00
goldlinker
8164a61376 chore: make some documents clearer (#276) 2025-11-10 17:25:54 -08:00
Jeroen Van den Berghe
2e9f21f838 Convert Insomnia variables syntax in headers, parameters and form data (#291)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-11-10 17:24:30 -08:00
Gregory Schier
0d725b59bd Verify trusted-signing-cli version 2025-11-10 15:02:10 -08:00
Gregory Schier
632860c29b Try again 2025-11-10 14:58:54 -08:00
Gregory Schier
e1cf16f6e1 Try again 2025-11-10 14:49:15 -08:00
Gregory Schier
47c9cfb295 Fix release? 2025-11-10 14:46:09 -08:00
Gregory Schier
6389fd3b8f Connection re-use for plugin networking and beta NTLM plugin (#295) 2025-11-10 14:41:49 -08:00
Gregory Schier
d318546d0c Back to vertical tabs in workspace settings 2025-11-10 06:21:26 -08:00
Gregory Schier
2f60b7b1f3 Switch trusted-signing-cli install method 2025-11-09 13:55:51 -08:00
Gregory Schier
75dc82570b Rename BadgeButton to PillButton 2025-11-09 08:18:26 -08:00
Gregory Schier
d7a7a64ec4 New "Triangle" theme 2025-11-09 07:55:31 -08:00
Gregory Schier
3aae1b52d1 Update commercial use trial wording 2025-11-09 07:19:05 -08:00
Gregory Schier
9eddf716e1 Update commercial use trial wording 2025-11-09 07:07:18 -08:00
Gregory Schier
554e632c19 Minor license handling tweaks 2025-11-09 06:01:03 -08:00
Gregory Schier
054916b7af JSON linting 2025-11-08 15:24:31 -08:00
Gregory Schier
f2a63087b0 Actually fix GraphQLEditor.tsx properly 2025-11-06 09:33:12 -08:00
Gregory Schier
6f0d4ad5e4 Fix GraphQL editor 2025-11-06 06:31:56 -08:00
Gregory Schier
cd3530f598 Dropdown to setup sync now opens the correct workspace settings tab 2025-11-06 05:13:18 -08:00
Gregory Schier
53aea914ac Don't drag tree item when editing
https://feedback.yaak.app/p/select-text-of-navbar-in-edit-mode
2025-11-06 05:10:23 -08:00
Gregory Schier
dc0c1decee Fix copy-curl with API key
https://feedback.yaak.app/p/copy-as-curl-bug-when-auth-use-api-key-with
2025-11-05 10:21:26 -08:00
Gregory Schier
32d56f2274 OAuth 1 Authentication Plugin (#292) 2025-11-05 10:12:48 -08:00
Gregory Schier
ef86c1d189 Recursively collapse during "coolapse all" 2025-11-05 10:12:10 -08:00
Gregory Schier
e264c50427 Show more resopnse header y height 2025-11-05 10:11:55 -08:00
Gregory Schier
f05ad62301 Fix zoom hotkey
https://feedback.yaak.app/p/zoom-in-not-working-on-linux-mint
2025-11-05 10:11:46 -08:00
Gregory Schier
0a6228bf16 Fix Input ref timing, PairEditor initialization, and environment variable focus 2025-11-04 14:04:12 -08:00
Gregory Schier
fa3a0b57f9 Fix Editor.tsx wonkiness 2025-11-04 13:44:18 -08:00
Gregory Schier
4390c02117 Fix gRPC message editing 2025-11-04 12:35:36 -08:00
Gregory Schier
77011176af Fix tab flexbox issue 2025-11-04 09:22:28 -08:00
Gregory Schier
759fc503d3 Fix accidental typing 2025-11-04 08:51:46 -08:00
Gregory Schier
0cb633e479 A bunch of fixes 2025-11-04 08:44:08 -08:00
Gregory Schier
81ceb981e8 Oops 2025-11-03 15:05:50 -08:00
Gregory Schier
4dae1a7955 Improve selecting items during filter 2025-11-03 15:04:02 -08:00
Gregory Schier
d119f4cab2 Fix confirm with text autofocus 2025-11-03 14:42:30 -08:00
Gregory Schier
7e1eb90d29 Show error when enabling encryption fails 2025-11-03 14:34:43 -08:00
Gregory Schier
bf97ea1659 Some sidebar fixes 2025-11-03 14:17:11 -08:00
Gregory Schier
749ca968ec Fix environment sorting 2025-11-03 13:53:41 -08:00
Gregory Schier
0c54b481fb Fix unused variable 2025-11-03 13:29:47 -08:00
Jeroen Van den Berghe
4943bad8ec Import query parameters from Insomnia v4 and v5 exports (#290) 2025-11-03 13:03:24 -08:00
Gregory Schier
450dbd0053 Better syntax highlighting for filter expressions 2025-11-03 06:30:41 -08:00
Gregory Schier
236c8fa656 Fix sidebar reselection after dragging non-selelected item or renaming 2025-11-03 06:19:04 -08:00
Gregory Schier
1dfc2ee602 Support encoding values to base64 (url safe) 2025-11-03 06:07:34 -08:00
Gregory Schier
1d158082f6 Pass host environment variable to plugin runtime
https://feedback.yaak.app/p/when-i-use-clash-yaak-fails-to-launch
2025-11-03 06:02:18 -08:00
Gregory Schier
f3e44c53d7 Show full paths in command palette switcher
https://feedback.yaak.app/p/command-palette-search-should-include-parent-folder-names
2025-11-03 05:54:29 -08:00
Gregory Schier
c8d5e7c97b Add support for API key authentication in cURL conversion
https://feedback.yaak.app/p/copy-as-curl-without-api-key
2025-11-03 05:05:54 -08:00
Gregory Schier
9bde6bbd0a More efficient editor state saves 2025-11-02 06:16:45 -08:00
Gregory Schier
df5be218a5 Remove debug console logs from Input component 2025-11-02 05:52:56 -08:00
Gregory Schier
2deb870bb6 Fix pair editor 2025-11-02 05:52:36 -08:00
Gregory Schier
0f9975339c Fixes for last commit 2025-11-01 09:33:57 -07:00
Gregory Schier
6ad4e7bbb5 Click env var to edit AND improve input/editor ref handling 2025-11-01 08:39:07 -07:00
Gregory Schier
2bcf67aaa6 Fallback to jsonpath for response filter 2025-10-31 09:45:29 -07:00
Gregory Schier
c01b8ce4ca Fix sort priority 2025-10-31 09:40:37 -07:00
Gregory Schier
f7bb649b16 Fix ref type 2025-10-31 09:25:04 -07:00
Gregory Schier
e3e67c8df7 Use TRee component for Environment dialog (#288) 2025-10-31 09:16:29 -07:00
gschier
c9698c0f23 Deploying to main from @ mountain-loop/yaak@2cdd1d8136 🚀 2025-10-31 15:36:52 +00:00
Gregory Schier
2cdd1d8136 Tree fixes and sidebar filter DSL 2025-10-31 05:59:46 -07:00
gschier
8d8e5c0317 Deploying to main from @ mountain-loop/yaak@4e66a73677 🚀 2025-10-30 00:20:16 +00:00
Gregory Schier
4e66a73677 npm i 2025-10-29 15:37:46 -07:00
Gregory Schier
08f1bc4e65 Disable sidebar filtering for now 2025-10-29 15:30:18 -07:00
Gregory Schier
c6d9cb9c9e Narrow vim keys selector 2025-10-29 14:59:33 -07:00
Gregory Schier
efbb90dd60 Prevent vim hotkeys from activating tree in sidebar filter 2025-10-29 14:59:13 -07:00
Gregory Schier
7a7940d365 Change response history dropdown icon 2025-10-29 14:58:56 -07:00
Börge Kiss
8a6f80a181 Fix dismissable banner action button title (#273) 2025-10-29 08:16:33 -07:00
Quentin Ross
e8e0097e2d Fix websocket url parameters not parsing variables (#281) 2025-10-29 08:16:07 -07:00
Zhizhen He
f475b05c51 Allow specifying time for unix / unix millis / iso 8601 format (#283)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-10-29 08:15:19 -07:00
Madeleaan
7e5f9004e2 Fix text on plugin installation button (#284) 2025-10-29 08:14:40 -07:00
Gregory Schier
660771b48c Add random.range() template function 2025-10-29 08:02:12 -07:00
Gregory Schier
030e8b837e Fix incorrect Postman AWS auth key mapping and update test fixtures 2025-10-29 07:08:02 -07:00
Gregory Schier
a42cba567c Support all possible Postman auth types 2025-10-29 07:06:10 -07:00
Gregory Schier
484b5b2fd8 Switch to vkbeautify for XML
https://feedback.yaak.app/p/xml-pretty-formatter-not-rendering-correctly
2025-10-28 14:03:49 -07:00
Gregory Schier
a71fb8ed6c Don't trigger hotkeys within sidebar edit input 2025-10-28 13:03:37 -07:00
Gregory Schier
5b8114f6f3 Add context menu support and Vim keybindings in Sidebar and Tree components 2025-10-28 08:45:36 -07:00
Gregory Schier
68637d24c7 Don't throw on empty variable values
https://feedback.yaak.app/p/variable-with-empty-value-in-request-will-cause-error
2025-10-28 07:20:41 -07:00
Gregory Schier
c097afe657 Skip disabled headers and URL parameters during rendering 2025-10-28 07:11:37 -07:00
Gregory Schier
78bc7d7909 Update label for "trialing" state to "Commercial Trial" in LicenseBadge 2025-10-28 07:11:17 -07:00
Gregory Schier
b68ce44d52 Colorize HTTP methods in dropdown
https://feedback.yaak.app/p/colorized-methods-on-dropdown-select
2025-10-28 07:11:03 -07:00
Gregory Schier
632344d166 Adjust LicenseBadge color for "trialing" state to secondary 2025-10-28 07:04:16 -07:00
Gregory Schier
f3814b7d2b Show cursor in response view 2025-10-28 07:03:19 -07:00
Gregory Schier
618a544dbd Adjust default font sizes for editor and interface settings 2025-10-28 07:03:06 -07:00
Gregory Schier
9a55426236 Fix incorrect Sidebar hidden state logic 2025-10-28 06:58:31 -07:00
Gregory Schier
b7ad490c9b Add setting to disable checking for notifications 2025-10-28 06:55:56 -07:00
Gregory Schier
2095cb88c2 Fix entering encryption key
https://feedback.yaak.app/p/encryption-feature-error
2025-10-28 06:55:03 -07:00
Gregory Schier
a9e05ae988 Copy on "type to confirm" dialog 2025-10-28 06:15:44 -07:00
Gregory Schier
99a6c38632 Sidebar filtering and improvements (#285) 2025-10-27 14:10:28 -07:00
Gregory Schier
b2766509e3 Hotkey for creating environment when dialog open 2025-10-26 12:10:41 -07:00
Gregory Schier
3f5b5a397c Better environment color picker (#282) 2025-10-26 12:05:03 -07:00
Gregory Schier
923b1ac830 Fix indent guide on drag and drop after expand folder
https://feedback.yaak.app/p/displace-moving-caret-on-spring-loaded-folder
2025-10-25 09:41:06 -07:00
Gregory Schier
17dbe7c9a7 API key auth to copy-as-grpcurl 2025-10-25 08:43:50 -07:00
Gregory Schier
df80cdfe33 Copy as curl AWS auth, and handle disabled auth 2025-10-25 08:33:27 -07:00
Gregory Schier
eb1916b773 Fix tests 2025-10-24 15:22:20 -07:00
Gregory Schier
a3df0489b1 Fix Insomnia v4 environment importer 2025-10-24 15:21:20 -07:00
Gregory Schier
b19e036a61 Better CSS 2025-10-24 15:06:08 -07:00
Gregory Schier
b51e37f221 Try fix folder variable pane layout 2025-10-24 14:53:07 -07:00
Gregory Schier
cf9882b5b9 Fix response viewer stream scrolling 2025-10-24 14:39:25 -07:00
Gregory Schier
bbf85c953d Better XML formatting, fix pointer cursor in sidebar, copy/create URL in response 2025-10-24 09:50:42 -07:00
Gregory Schier
17ddc76223 Better XML beautify 2025-10-24 08:59:16 -07:00
Gregory Schier
754ec0ba86 Fix AWS auth
https://x.com/NilsFleischer63/status/1981719735432511553
2025-10-24 08:42:18 -07:00
Gregory Schier
1198aa7d87 Add tree rename (on Enter) and global rename hotkeys (#279) 2025-10-24 08:01:38 -07:00
Gregory Schier
43437abae7 Add custom DNS resolver for *.localhost (#280) 2025-10-24 08:01:12 -07:00
moebiuscorzer
9439cfa2ba fix: typo 'validatation' corrected into 'validation' (#278) 2025-10-24 06:09:00 -07:00
gschier
a731ccc8bd Deploying to main from @ mountain-loop/yaak@451c8b9dde 🚀 2025-10-23 15:36:39 +00:00
Gregory Schier
451c8b9dde Fix PDF viewer 2025-10-22 08:56:36 -07:00
Gregory Schier
b7682db9a3 Remove duplicate themes in getThemes function 2025-10-22 06:56:00 -07:00
Gregory Schier
7e2d72c4e3 Fix secure() function editing 2025-10-21 20:09:56 -07:00
Gregory Schier
28bb460409 Add empty workspaces array to environment output fixture 2025-10-21 08:16:33 -07:00
Gregory Schier
56d635166b Add tsconfig.json for importer-postman-environment plugin 2025-10-21 08:08:27 -07:00
Gregory Schier
f6a7257104 Text color for selected 2025-10-21 07:46:55 -07:00
Gregory Schier
1fce060ef7 Npm i 2025-10-21 07:36:21 -07:00
Gregory Schier
5c966e5a95 Add bracket matching 2025-10-21 07:27:07 -07:00
Gregory Schier
0520ef5d43 Import postman environments
https://feedback.yaak.app/p/import-postman-environments
2025-10-21 07:20:37 -07:00
dependabot[bot]
25b110778a Bump vite from 7.0.7 to 7.0.8 (#269) 2025-10-20 21:19:55 -07:00
Gregory Schier
327bf84e57 Clarify proto import buttons 2025-10-20 09:23:12 -07:00
Gregory Schier
1c48b309b5 Fix indent guide hovering 2025-10-20 09:13:00 -07:00
Gregory Schier
7c5dec821d Remove React.lazy on overlay and tooltip 2025-10-19 12:00:30 -07:00
gschier
dcd8f6c08a Deploying to main from @ mountain-loop/yaak@31f9a63c3b 🚀 2025-10-19 17:19:55 +00:00
Gregory Schier
31f9a63c3b Don't force push 2025-10-19 10:18:32 -07:00
Gregory Schier
e902b67a63 Replace arrayMove with custom implementation in PairEditor to remove dependency on @dnd-kit/sortable. 2025-10-19 09:40:11 -07:00
Gregory Schier
b11c72fde4 Add back creation items to context menu 2025-10-19 08:52:03 -07:00
Gregory Schier
07b90c6ae3 Make plugins scrollable 2025-10-19 08:21:36 -07:00
Gregory Schier
ba6163b6d8 Better code splitting and removed final instances of react-dnd 2025-10-19 08:16:56 -07:00
Gregory Schier
8055b625d0 Improve handling of drag-and-drop for collapsed and empty folders in tree component 2025-10-18 07:59:14 -07:00
Gregory Schier
3a61ffbbb0 Better drag for empty folders 2025-10-18 07:41:33 -07:00
Gregory Schier
f8478677c5 Pass the previous app version to the notification endpoint so the update notification can display all missed changelogs, not just the latest one. 2025-10-18 07:13:52 -07:00
Gregory Schier
f5094c5a94 Fix drop marker 2025-10-17 16:15:14 -07:00
Étienne Lévesque
8300187566 [Plugins] [Auth] [oauth2] Support identity platforms with underlying IDPs (#261)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-10-17 16:07:25 -07:00
Kien Dang
cd8ab3616e Fix GraphQL doc explorer CountBadge stacking order (#262) 2025-10-17 15:33:40 -07:00
Maksim Karelov
be0c92b755 Add ability to select fs.readFile encoding (#267) 2025-10-17 15:32:04 -07:00
Gregory Schier
c34ea20406 Flattened the sidebar tree 2025-10-17 15:07:02 -07:00
Gregory Schier
6e9b1db196 Bump version 2025-10-16 14:42:02 -07:00
Gregory Schier
d83aabd2be Dynamic template function args and TTL option for request chaining (#266) 2025-10-16 14:39:30 -07:00
Gregory Schier
d46479cd22 Remove debug console log from TreeDragOverlay component 2025-10-15 14:08:21 -07:00
Gregory Schier
19cae33382 Fix crash when delete after drag 2025-10-15 14:07:55 -07:00
Gregory Schier
267cd079ad New sidebar and folder view (#263) 2025-10-15 13:46:57 -07:00
Gregory Schier
19c1efc73e Resolve 2025-10-11 08:28:07 -07:00
Gregory Schier
dfa9a22861 Merge remote-tracking branch 'origin/main' 2025-10-11 06:29:17 -07:00
Gregory Schier
533f9bacc4 Add AWS authentication 2025-10-11 06:29:06 -07:00
Zhizhen He
0358748729 Fix icon paths in package.json (#265) 2025-10-09 04:24:44 -07:00
Zhizhen He
1540d0a5a5 Fix typo (#264) 2025-10-08 19:54:18 -07:00
Gregory Schier
d177e164f1 Fix log 2025-10-08 04:25:06 -07:00
Gregory Schier
f1355c9d15 Fix non-release build 2025-10-08 04:25:00 -07:00
Gregory Schier
485a9ea47c Show toast on plugin event handling errors instead of crashing
Also set folder context on template render and fix timestamp function
2025-10-06 06:53:45 -07:00
Gregory Schier
dbc606fb53 Update README 2025-10-04 08:22:39 -07:00
364 changed files with 14366 additions and 6051 deletions

View File

@@ -54,15 +54,24 @@ jobs:
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo- restore-keys: ${{ runner.os }}-cargo-
- name: install dependencies (ubuntu only) - name: install dependencies (Linux only)
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above. if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: install dependencies (windows only) - name: Install trusted-signing-cli (Windows only)
if: matrix.platform == 'windows-latest' if: matrix.platform == 'windows-latest'
run: cargo install --force trusted-signing-cli 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: Install NPM Dependencies - name: Install NPM Dependencies
run: npm ci run: npm ci
@@ -114,4 +123,4 @@ jobs:
releaseBody: '[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)' releaseBody: '[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)'
releaseDraft: true releaseDraft: true
prerelease: true prerelease: true
args: '${{ matrix.args }} --config ./src-tauri/tauri.commercial.conf.json' args: '${{ matrix.args }} --config ./src-tauri/tauri.release.conf.json'

View File

@@ -40,4 +40,5 @@ jobs:
uses: JamesIves/github-pages-deploy-action@v4 uses: JamesIves/github-pages-deploy-action@v4
with: with:
branch: main branch: main
force: false
folder: '.' folder: '.'

View File

@@ -54,7 +54,7 @@ Rerun the app to apply the migrations.
_Note: For safety, development builds use a separate database location from production builds._ _Note: For safety, development builds use a separate database location from production builds._
## Lezer Grammer Generation ## Lezer Grammar Generation
```sh ```sh
# Example # Example

View File

@@ -22,7 +22,7 @@
<!-- 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/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 -->
</p> </p>
<p align="center"> <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;<!-- 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/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> </p>
![Yaak API Client](https://yaak.app/static/screenshot.png) ![Yaak API Client](https://yaak.app/static/screenshot.png)
@@ -42,7 +42,7 @@ Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight
### 🔐 Stay secure ### 🔐 Stay secure
- Use OAuth 2.0, JWT, Basic Auth, or custom plugins for authentication. - Use OAuth 2.0, JWT, Basic Auth, or custom plugins for authentication.
- Secure sensitive values with end-to-end encryption. - Secure sensitive values with encrypted secrets.
- Store secrets in your OS keychain. - Store secrets in your OS keychain.
### ☁️ Organize & collaborate ### ☁️ Organize & collaborate
@@ -58,7 +58,7 @@ Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight
## Contribution Policy ## Contribution Policy
Yaak is open source, but only accepting contributions for bug fixes. To get started, Yaak is open source but only accepting contributions for bug fixes. To get started,
visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment. visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment.
## Useful Resources ## Useful Resources
@@ -68,4 +68,3 @@ visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment
- [Yaak vs Postman](https://yaak.app/alternatives/postman) - [Yaak vs Postman](https://yaak.app/alternatives/postman)
- [Yaak vs Bruno](https://yaak.app/alternatives/bruno) - [Yaak vs Bruno](https://yaak.app/alternatives/bruno)
- [Yaak vs Insomnia](https://yaak.app/alternatives/insomnia) - [Yaak vs Insomnia](https://yaak.app/alternatives/insomnia)

672
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,40 +7,46 @@
"url": "git+https://github.com/mountain-loop/yaak.git" "url": "git+https://github.com/mountain-loop/yaak.git"
}, },
"workspaces": [ "workspaces": [
"packages/common-lib",
"packages/plugin-runtime", "packages/plugin-runtime",
"packages/plugin-runtime-types", "packages/plugin-runtime-types",
"packages/common-lib", "plugins/action-copy-curl",
"plugins/action-copy-grpcurl",
"plugins/auth-apikey", "plugins/auth-apikey",
"plugins/auth-aws",
"plugins/auth-basic", "plugins/auth-basic",
"plugins/auth-bearer", "plugins/auth-bearer",
"plugins/auth-jwt", "plugins/auth-jwt",
"plugins/auth-ntlm",
"plugins/auth-oauth2", "plugins/auth-oauth2",
"plugins/action-copy-curl", "plugins/auth-oauth1",
"plugins/action-copy-grpcurl",
"plugins/filter-jsonpath", "plugins/filter-jsonpath",
"plugins/filter-xpath", "plugins/filter-xpath",
"plugins/importer-curl", "plugins/importer-curl",
"plugins/importer-insomnia", "plugins/importer-insomnia",
"plugins/importer-openapi", "plugins/importer-openapi",
"plugins/importer-postman", "plugins/importer-postman",
"plugins/importer-postman-environment",
"plugins/importer-yaak", "plugins/importer-yaak",
"plugins/template-function-cookie", "plugins/template-function-cookie",
"plugins/template-function-timestamp", "plugins/template-function-ctx",
"plugins/template-function-encode", "plugins/template-function-encode",
"plugins/template-function-fs", "plugins/template-function-fs",
"plugins/template-function-hash", "plugins/template-function-hash",
"plugins/template-function-json", "plugins/template-function-json",
"plugins/template-function-prompt", "plugins/template-function-prompt",
"plugins/template-function-random",
"plugins/template-function-regex", "plugins/template-function-regex",
"plugins/template-function-request", "plugins/template-function-request",
"plugins/template-function-response", "plugins/template-function-timestamp",
"plugins/template-function-uuid", "plugins/template-function-uuid",
"plugins/template-function-xml", "plugins/template-function-xml",
"plugins/template-function-response",
"plugins/themes-yaak", "plugins/themes-yaak",
"src-tauri", "src-tauri",
"src-tauri/yaak-crypto", "src-tauri/yaak-crypto",
"src-tauri/yaak-git",
"src-tauri/yaak-fonts", "src-tauri/yaak-fonts",
"src-tauri/yaak-git",
"src-tauri/yaak-license", "src-tauri/yaak-license",
"src-tauri/yaak-mac-window", "src-tauri/yaak-mac-window",
"src-tauri/yaak-models", "src-tauri/yaak-models",
@@ -60,9 +66,10 @@
"build-plugins": "npm run --workspaces --if-present build", "build-plugins": "npm run --workspaces --if-present build",
"test": "npm run --workspaces --if-present test", "test": "npm run --workspaces --if-present test",
"icons": "run-p icons:*", "icons": "run-p icons:*",
"icons:dev": "tauri icon src-tauri/icons/icon.png --output src-tauri/icons/release", "icons:dev": "tauri icon src-tauri/icons/icon-dev.png --output src-tauri/icons/dev",
"icons:release": "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": "run-s bootstrap:*",
"bootstrap:build": "npm run build",
"bootstrap:vendor-node": "node scripts/vendor-node.cjs", "bootstrap:vendor-node": "node scripts/vendor-node.cjs",
"bootstrap:vendor-plugins": "node scripts/vendor-plugins.cjs", "bootstrap:vendor-plugins": "node scripts/vendor-plugins.cjs",
"bootstrap:vendor-protoc": "node scripts/vendor-protoc.cjs", "bootstrap:vendor-protoc": "node scripts/vendor-protoc.cjs",
@@ -79,7 +86,7 @@
"@eslint/compat": "^1.3.0", "@eslint/compat": "^1.3.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.29.0", "@eslint/js": "^9.29.0",
"@tauri-apps/cli": "^2.8.4", "@tauri-apps/cli": "^2.9.1",
"@typescript-eslint/eslint-plugin": "^8.27.0", "@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0", "@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.2.7", "@yaakapp/cli": "^0.2.7",

View File

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

View File

@@ -30,7 +30,7 @@ export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, }; 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, }; 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 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, }; export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
@@ -224,6 +224,8 @@ defaultValue?: string, disabled?: boolean,
*/ */
description?: string, }; description?: string, };
export type FormInputHStack = { inputs?: Array<FormInput>, hidden?: boolean, };
export type FormInputHttpRequest = { export type FormInputHttpRequest = {
/** /**
* The name of the input. The value will be stored at this object attribute in the resulting data * The name of the input. The value will be stored at this object attribute in the resulting data
@@ -361,7 +363,11 @@ export type GetKeyValueRequest = { key: string, };
export type GetKeyValueResponse = { value?: string, }; export type GetKeyValueResponse = { value?: string, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, }; export type GetTemplateFunctionConfigRequest = { contextId: string, name: string, values: { [key in string]?: JsonPrimitive }, };
export type GetTemplateFunctionConfigResponse = { function: TemplateFunction, pluginRefId: string, };
export type GetTemplateFunctionSummaryResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type GetThemesRequest = Record<string, never>; export type GetThemesRequest = Record<string, never>;
@@ -383,9 +389,9 @@ export type ImportResources = { workspaces: Array<Workspace>, environments: Arra
export type ImportResponse = { resources: ImportResources, }; 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_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "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": "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": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "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; export type JsonPrimitive = string | number | boolean | null;
@@ -399,13 +405,13 @@ export type OpenWindowRequest = { url: string,
*/ */
label: string, title?: string, size?: WindowSize, dataDirKey?: 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, export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/** /**
* Text to add to the confirmation button * Text to add to the confirmation button
*/ */
confirmText?: string, confirmText?: string, password?: boolean,
/** /**
* Text to add to the cancel button * Text to add to the cancel button
*/ */
@@ -437,9 +443,9 @@ export type SetKeyValueRequest = { key: string, value: string, };
export type SetKeyValueResponse = {}; export type SetKeyValueResponse = {};
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, }; 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 * Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property * tags when changing the `name` property
@@ -451,6 +457,8 @@ aliases?: Array<string>, args: Array<TemplateFunctionArg>, };
*/ */
export type TemplateFunctionArg = FormInput; export type TemplateFunctionArg = FormInput;
export type TemplateFunctionPreviewType = "live" | "click" | "none";
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, }; export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, }; export type TemplateRenderResponse = { data: JsonValue, };
@@ -481,6 +489,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 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 WindowNavigateEvent = { url: string, };
export type WindowSize = { width: number, height: number, }; export type WindowSize = { width: number, height: number, };

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. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, variables: Array<EnvironmentVariable>, color: string | null, parentModel: string, parentId: string | null, }; export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

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

View File

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

View File

@@ -1,12 +1,31 @@
import { import { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events';
CallTemplateFunctionArgs, import { MaybePromise } from '../helpers';
TemplateFunction, import { Context } from './Context';
} from "../bindings/gen_events";
import { Context } from "./Context";
export type TemplateFunctionPlugin = TemplateFunction & { type AddDynamicMethod<T> = {
onRender( dynamic?: (
ctx: Context, ctx: Context,
args: CallTemplateFunctionArgs, args: CallTemplateFunctionArgs,
): Promise<string | null>; ) => 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: 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 { AuthenticationPlugin } from './AuthenticationPlugin';
import type { Context } from './Context';
import type { FilterPlugin } from './FilterPlugin'; import type { FilterPlugin } from './FilterPlugin';
import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin'; import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin'; import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
@@ -6,9 +8,10 @@ import type { ImporterPlugin } from './ImporterPlugin';
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin'; import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
import type { ThemePlugin } from './ThemePlugin'; import type { ThemePlugin } from './ThemePlugin';
import type { Context } from './Context';
export type { Context }; export type { Context };
export type { DynamicTemplateFunctionArg } from './TemplateFunctionPlugin';
export type { DynamicAuthenticationArg } from './AuthenticationPlugin';
export type { TemplateFunctionPlugin };
/** /**
* The global structure of a Yaak plugin * 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 { BootRequest, InternalEvent } from '@yaakapp/api';
import type { EventChannel } from './EventChannel'; import type { EventChannel } from './EventChannel';
import { PluginInstance, PluginWorkerData } from './PluginInstance'; import { PluginInstance, PluginWorkerData } from './PluginInstance';
@@ -6,14 +7,12 @@ export class PluginHandle {
#instance: PluginInstance; #instance: PluginInstance;
constructor( constructor(
readonly pluginRefId: string, pluginRefId: string,
readonly bootRequest: BootRequest, context: PluginContext,
readonly pluginToAppEvents: EventChannel, bootRequest: BootRequest,
pluginToAppEvents: EventChannel,
) { ) {
const workerData: PluginWorkerData = { const workerData: PluginWorkerData = { pluginRefId, context, bootRequest };
pluginRefId: this.pluginRefId,
bootRequest: this.bootRequest,
};
this.#instance = new PluginInstance(workerData, pluginToAppEvents); this.#instance = new PluginInstance(workerData, pluginToAppEvents);
} }

View File

@@ -2,7 +2,6 @@ import {
BootRequest, BootRequest,
DeleteKeyValueResponse, DeleteKeyValueResponse,
FindHttpResponsesResponse, FindHttpResponsesResponse,
FormInput,
GetCookieValueRequest, GetCookieValueRequest,
GetCookieValueResponse, GetCookieValueResponse,
GetHttpRequestByIdResponse, GetHttpRequestByIdResponse,
@@ -13,32 +12,32 @@ import {
InternalEvent, InternalEvent,
InternalEventPayload, InternalEventPayload,
ListCookieNamesResponse, ListCookieNamesResponse,
PluginWindowContext, PluginContext,
PromptTextResponse, PromptTextResponse,
RenderGrpcRequestResponse, RenderGrpcRequestResponse,
RenderHttpRequestResponse, RenderHttpRequestResponse,
SendHttpRequestResponse, SendHttpRequestResponse,
TemplateFunction, TemplateFunction,
TemplateFunctionArg,
TemplateRenderResponse, TemplateRenderResponse,
WindowInfoResponse,
} from '@yaakapp-internal/plugins'; } from '@yaakapp-internal/plugins';
import { Context, PluginDefinition } from '@yaakapp/api'; import { Context, PluginDefinition } from '@yaakapp/api';
import { JsonValue } from '@yaakapp/api/lib/bindings/serde_json/JsonValue';
import console from 'node:console'; 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 path from 'node:path';
import { applyDynamicFormInput, applyFormInputDefaults } from './common';
import { EventChannel } from './EventChannel'; import { EventChannel } from './EventChannel';
import { migrateTemplateFunctionSelectOptions } from './migrations'; import { migrateTemplateFunctionSelectOptions } from './migrations';
export interface PluginWorkerData { export interface PluginWorkerData {
bootRequest: BootRequest; bootRequest: BootRequest;
pluginRefId: string; pluginRefId: string;
context: PluginContext;
} }
export class PluginInstance { export class PluginInstance {
#workerData: PluginWorkerData; #workerData: PluginWorkerData;
#mod: PluginDefinition; #mod: PluginDefinition;
#pkg: { name?: string; version?: string };
#pluginToAppEvents: EventChannel; #pluginToAppEvents: EventChannel;
#appToPluginEvents: EventChannel; #appToPluginEvents: EventChannel;
@@ -52,18 +51,14 @@ export class PluginInstance {
await this.#onMessage(event); await this.#onMessage(event);
}); });
// Reload plugin if the JS or package.json changes
const windowContextNone: PluginWindowContext = { type: 'none' };
this.#mod = {} as any; this.#mod = {} as any;
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
const fileChangeCallback = async () => { const fileChangeCallback = async () => {
await this.#mod?.dispose?.(); await this.#mod?.dispose?.();
this.#importModule(); this.#importModule();
await this.#mod?.init?.(this.#newCtx({ type: 'none' })); await this.#mod?.init?.(this.#newCtx(workerData.context));
return this.#sendPayload( return this.#sendPayload(
windowContextNone, workerData.context,
{ {
type: 'reload_response', type: 'reload_response',
silent: false, silent: false,
@@ -90,14 +85,14 @@ export class PluginInstance {
} }
async #onMessage(event: InternalEvent) { 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 { try {
if (payload.type === 'boot_request') { if (payload.type === 'boot_request') {
await this.#mod?.init?.(ctx); await this.#mod?.init?.(ctx);
this.#sendPayload(windowContext, { type: 'boot_response' }, replyId); this.#sendPayload(context, { type: 'boot_response' }, replyId);
return; return;
} }
@@ -106,7 +101,7 @@ export class PluginInstance {
type: 'terminate_response', type: 'terminate_response',
}; };
await this.terminate(); await this.terminate();
this.#sendPayload(windowContext, payload, replyId); this.#sendPayload(context, payload, replyId);
return; return;
} }
@@ -123,10 +118,10 @@ export class PluginInstance {
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
resources: reply.resources as any, resources: reply.resources as any,
}; };
this.#sendPayload(windowContext, replyPayload, replyId); this.#sendPayload(context, replyPayload, replyId);
return; return;
} else { } else {
// Continue, to send back an empty reply // Send back an empty reply (below)
} }
} }
@@ -136,7 +131,7 @@ export class PluginInstance {
payload: payload.content, payload: payload.content,
mimeType: payload.type, mimeType: payload.type,
}); });
this.#sendPayload(windowContext, { type: 'filter_response', ...reply }, replyId); this.#sendPayload(context, { type: 'filter_response', ...reply }, replyId);
return; return;
} }
@@ -154,7 +149,7 @@ export class PluginInstance {
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
actions: reply, actions: reply,
}; };
this.#sendPayload(windowContext, replyPayload, replyId); this.#sendPayload(context, replyPayload, replyId);
return; return;
} }
@@ -172,7 +167,7 @@ export class PluginInstance {
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
actions: reply, actions: reply,
}; };
this.#sendPayload(windowContext, replyPayload, replyId); this.#sendPayload(context, replyPayload, replyId);
return; return;
} }
@@ -181,55 +176,74 @@ export class PluginInstance {
type: 'get_themes_response', type: 'get_themes_response',
themes: this.#mod.themes, themes: this.#mod.themes,
}; };
this.#sendPayload(windowContext, replyPayload, replyId); this.#sendPayload(context, replyPayload, replyId);
return; return;
} }
if ( if (
payload.type === 'get_template_functions_request' && payload.type === 'get_template_function_summary_request' &&
Array.isArray(this.#mod?.templateFunctions) Array.isArray(this.#mod?.templateFunctions)
) { ) {
const reply: TemplateFunction[] = this.#mod.templateFunctions.map((templateFunction) => { const functions: TemplateFunction[] = this.#mod.templateFunctions.map(
return { (templateFunction) => {
...migrateTemplateFunctionSelectOptions(templateFunction), return {
// Add everything except render ...migrateTemplateFunctionSelectOptions(templateFunction),
onRender: undefined, // Add everything except render
}; onRender: undefined,
}); };
},
);
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_template_functions_response', type: 'get_template_function_summary_response',
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
functions: reply, functions,
}; };
this.#sendPayload(windowContext, replyPayload, replyId); this.#sendPayload(context, replyPayload, replyId);
return;
}
if (
payload.type === 'get_template_function_config_request' &&
Array.isArray(this.#mod?.templateFunctions)
) {
let templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name);
if (templateFunction == null) {
this.#sendEmpty(context, replyId);
return;
}
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: { ...fn, args: resolvedArgs },
};
this.#sendPayload(context, replyPayload, replyId);
return; return;
} }
if (payload.type === 'get_http_authentication_summary_request' && this.#mod?.authentication) { if (payload.type === 'get_http_authentication_summary_request' && this.#mod?.authentication) {
const { name, shortLabel, label } = this.#mod.authentication;
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_summary_response', type: 'get_http_authentication_summary_response',
name, ...this.#mod.authentication,
label,
shortLabel,
}; };
this.#sendPayload(windowContext, replyPayload, replyId); this.#sendPayload(context, replyPayload, replyId);
return; return;
} }
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) { if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
const { args, actions } = this.#mod.authentication; const { args, actions } = this.#mod.authentication;
const resolvedArgs: FormInput[] = []; payload.values = applyFormInputDefaults(args, payload.values);
for (const v of args) { const resolvedArgs = await applyDynamicFormInput(ctx, args, payload);
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);
}
}
const resolvedActions: HttpAuthenticationAction[] = []; const resolvedActions: HttpAuthenticationAction[] = [];
for (const { onSelect, ...action } of actions ?? []) { for (const { onSelect, ...action } of actions ?? []) {
resolvedActions.push(action); resolvedActions.push(action);
@@ -242,16 +256,17 @@ export class PluginInstance {
pluginRefId: this.#workerData.pluginRefId, pluginRefId: this.#workerData.pluginRefId,
}; };
this.#sendPayload(windowContext, replyPayload, replyId); this.#sendPayload(context, replyPayload, replyId);
return; return;
} }
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) { if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) {
const auth = this.#mod.authentication; const auth = this.#mod.authentication;
if (typeof auth?.onApply === 'function') { if (typeof auth?.onApply === 'function') {
applyFormInputDefaults(auth.args, payload.values); auth.args = await applyDynamicFormInput(ctx, auth.args, payload);
payload.values = applyFormInputDefaults(auth.args, payload.values);
this.#sendPayload( this.#sendPayload(
windowContext, context,
{ {
type: 'call_http_authentication_response', type: 'call_http_authentication_response',
...(await auth.onApply(ctx, payload)), ...(await auth.onApply(ctx, payload)),
@@ -269,7 +284,7 @@ export class PluginInstance {
const action = this.#mod.authentication.actions?.[payload.index]; const action = this.#mod.authentication.actions?.[payload.index];
if (typeof action?.onSelect === 'function') { if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args); await action.onSelect(ctx, payload.args);
this.#sendEmpty(windowContext, replyId); this.#sendEmpty(context, replyId);
return; return;
} }
} }
@@ -281,7 +296,7 @@ export class PluginInstance {
const action = this.#mod.httpRequestActions[payload.index]; const action = this.#mod.httpRequestActions[payload.index];
if (typeof action?.onSelect === 'function') { if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args); await action.onSelect(ctx, payload.args);
this.#sendEmpty(windowContext, replyId); this.#sendEmpty(context, replyId);
return; return;
} }
} }
@@ -293,7 +308,7 @@ export class PluginInstance {
const action = this.#mod.grpcRequestActions[payload.index]; const action = this.#mod.grpcRequestActions[payload.index];
if (typeof action?.onSelect === 'function') { if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args); await action.onSelect(ctx, payload.args);
this.#sendEmpty(windowContext, replyId); this.#sendEmpty(context, replyId);
return; return;
} }
} }
@@ -303,12 +318,27 @@ export class PluginInstance {
Array.isArray(this.#mod?.templateFunctions) Array.isArray(this.#mod?.templateFunctions)
) { ) {
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name); const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
if (typeof fn?.onRender === 'function') { if (
applyFormInputDefaults(fn.args, payload.args.values); 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);
payload.args.values = applyFormInputDefaults(resolvedArgs, payload.args.values);
try { try {
const result = await fn.onRender(ctx, payload.args); const result = await fn.onRender(ctx, payload.args);
this.#sendPayload( this.#sendPayload(
windowContext, context,
{ {
type: 'call_template_function_response', type: 'call_template_function_response',
value: result ?? null, value: result ?? null,
@@ -317,7 +347,7 @@ export class PluginInstance {
); );
} catch (err) { } catch (err) {
this.#sendPayload( this.#sendPayload(
windowContext, context,
{ {
type: 'call_template_function_response', type: 'call_template_function_response',
value: null, value: null,
@@ -332,12 +362,12 @@ export class PluginInstance {
} catch (err) { } catch (err) {
const error = `${err}`.replace(/^Error:\s*/g, ''); const error = `${err}`.replace(/^Error:\s*/g, '');
console.log('Plugin call threw exception', payload.type, '→', error); 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; return;
} }
// No matches, so send back an empty response so the caller doesn't block forever // No matches, so send back an empty response so the caller doesn't block forever
this.#sendEmpty(windowContext, replyId); this.#sendEmpty(context, replyId);
} }
#pathMod() { #pathMod() {
@@ -360,7 +390,7 @@ export class PluginInstance {
} }
#buildEventToSend( #buildEventToSend(
windowContext: PluginWindowContext, context: PluginContext,
payload: InternalEventPayload, payload: InternalEventPayload,
replyId: string | null = null, replyId: string | null = null,
): InternalEvent { ): InternalEvent {
@@ -370,16 +400,16 @@ export class PluginInstance {
id: genId(), id: genId(),
replyId, replyId,
payload, payload,
windowContext, context,
}; };
} }
#sendPayload( #sendPayload(
windowContext: PluginWindowContext, context: PluginContext,
payload: InternalEventPayload, payload: InternalEventPayload,
replyId: string | null, replyId: string | null,
): string { ): string {
const event = this.#buildEventToSend(windowContext, payload, replyId); const event = this.#buildEventToSend(context, payload, replyId);
this.#sendEvent(event); this.#sendEvent(event);
return event.id; return event.id;
} }
@@ -391,16 +421,16 @@ export class PluginInstance {
this.#pluginToAppEvents.emit(event); this.#pluginToAppEvents.emit(event);
} }
#sendEmpty(windowContext: PluginWindowContext, replyId: string | null = null): string { #sendEmpty(context: PluginContext, replyId: string | null = null): string {
return this.#sendPayload(windowContext, { type: 'empty_response' }, replyId); return this.#sendPayload(context, { type: 'empty_response' }, replyId);
} }
#sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>( #sendForReply<T extends Omit<InternalEventPayload, 'type'>>(
windowContext: PluginWindowContext, context: PluginContext,
payload: InternalEventPayload, payload: InternalEventPayload,
): Promise<T> { ): Promise<T> {
// 1. Build event to send // 1. Build event to send
const eventToSend = this.#buildEventToSend(windowContext, payload, null); const eventToSend = this.#buildEventToSend(context, payload, null);
// 2. Spawn listener in background // 2. Spawn listener in background
const promise = new Promise<T>((resolve) => { const promise = new Promise<T>((resolve) => {
@@ -422,12 +452,12 @@ export class PluginInstance {
} }
#sendAndListenForEvents( #sendAndListenForEvents(
windowContext: PluginWindowContext, context: PluginContext,
payload: InternalEventPayload, payload: InternalEventPayload,
onEvent: (event: InternalEventPayload) => void, onEvent: (event: InternalEventPayload) => void,
): void { ): void {
// 1. Build event to send // 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 // 2. Listen for replies in the background
this.#appToPluginEvents.listen((event: InternalEvent) => { this.#appToPluginEvents.listen((event: InternalEvent) => {
@@ -440,11 +470,23 @@ export class PluginInstance {
this.#sendEvent(eventToSend); 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 { return {
clipboard: { clipboard: {
copyText: async (text) => { copyText: async (text) => {
await this.#sendAndWaitForReply(windowContext, { await this.#sendForReply(context, {
type: 'copy_text_request', type: 'copy_text_request',
text, text,
}); });
@@ -452,13 +494,24 @@ export class PluginInstance {
}, },
toast: { toast: {
show: async (args) => { show: async (args) => {
await this.#sendAndWaitForReply(windowContext, { await this.#sendForReply(context, {
type: 'show_toast_request', type: 'show_toast_request',
// Handle default here because null/undefined both convert to None in Rust translation
timeout: args.timeout === undefined ? 5000 : args.timeout,
...args, ...args,
}); });
}, },
}, },
window: { 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 }) => { openUrl: async ({ onNavigate, onClose, ...args }) => {
args.label = args.label || `${Math.random()}`; args.label = args.label || `${Math.random()}`;
const payload: InternalEventPayload = { type: 'open_window_request', ...args }; const payload: InternalEventPayload = { type: 'open_window_request', ...args };
@@ -469,21 +522,21 @@ export class PluginInstance {
onClose?.(); onClose?.();
} }
}; };
this.#sendAndListenForEvents(windowContext, payload, onEvent); this.#sendAndListenForEvents(context, payload, onEvent);
return { return {
close: () => { close: () => {
const closePayload: InternalEventPayload = { const closePayload: InternalEventPayload = {
type: 'close_window_request', type: 'close_window_request',
label: args.label, label: args.label,
}; };
this.#sendPayload(windowContext, closePayload, null); this.#sendPayload(context, closePayload, null);
}, },
}; };
}, },
}, },
prompt: { prompt: {
text: async (args) => { text: async (args) => {
const reply: PromptTextResponse = await this.#sendAndWaitForReply(windowContext, { const reply: PromptTextResponse = await this.#sendForReply(context, {
type: 'prompt_text_request', type: 'prompt_text_request',
...args, ...args,
}); });
@@ -496,8 +549,8 @@ export class PluginInstance {
type: 'find_http_responses_request', type: 'find_http_responses_request',
...args, ...args,
} as const; } as const;
const { httpResponses } = await this.#sendAndWaitForReply<FindHttpResponsesResponse>( const { httpResponses } = await this.#sendForReply<FindHttpResponsesResponse>(
windowContext, context,
payload, payload,
); );
return httpResponses; return httpResponses;
@@ -509,8 +562,8 @@ export class PluginInstance {
type: 'render_grpc_request_request', type: 'render_grpc_request_request',
...args, ...args,
} as const; } as const;
const { grpcRequest } = await this.#sendAndWaitForReply<RenderGrpcRequestResponse>( const { grpcRequest } = await this.#sendForReply<RenderGrpcRequestResponse>(
windowContext, context,
payload, payload,
); );
return grpcRequest; return grpcRequest;
@@ -522,8 +575,8 @@ export class PluginInstance {
type: 'get_http_request_by_id_request', type: 'get_http_request_by_id_request',
...args, ...args,
} as const; } as const;
const { httpRequest } = await this.#sendAndWaitForReply<GetHttpRequestByIdResponse>( const { httpRequest } = await this.#sendForReply<GetHttpRequestByIdResponse>(
windowContext, context,
payload, payload,
); );
return httpRequest; return httpRequest;
@@ -533,8 +586,8 @@ export class PluginInstance {
type: 'send_http_request_request', type: 'send_http_request_request',
...args, ...args,
} as const; } as const;
const { httpResponse } = await this.#sendAndWaitForReply<SendHttpRequestResponse>( const { httpResponse } = await this.#sendForReply<SendHttpRequestResponse>(
windowContext, context,
payload, payload,
); );
return httpResponse; return httpResponse;
@@ -544,8 +597,8 @@ export class PluginInstance {
type: 'render_http_request_request', type: 'render_http_request_request',
...args, ...args,
} as const; } as const;
const { httpRequest } = await this.#sendAndWaitForReply<RenderHttpRequestResponse>( const { httpRequest } = await this.#sendForReply<RenderHttpRequestResponse>(
windowContext, context,
payload, payload,
); );
return httpRequest; return httpRequest;
@@ -557,18 +610,12 @@ export class PluginInstance {
type: 'get_cookie_value_request', type: 'get_cookie_value_request',
...args, ...args,
} as const; } as const;
const { value } = await this.#sendAndWaitForReply<GetCookieValueResponse>( const { value } = await this.#sendForReply<GetCookieValueResponse>(context, payload);
windowContext,
payload,
);
return value; return value;
}, },
listNames: async () => { listNames: async () => {
const payload = { type: 'list_cookie_names_request' } as const; const payload = { type: 'list_cookie_names_request' } as const;
const { names } = await this.#sendAndWaitForReply<ListCookieNamesResponse>( const { names } = await this.#sendForReply<ListCookieNamesResponse>(context, payload);
windowContext,
payload,
);
return names; return names;
}, },
}, },
@@ -579,20 +626,14 @@ export class PluginInstance {
*/ */
render: async (args) => { render: async (args) => {
const payload = { type: 'template_render_request', ...args } as const; const payload = { type: 'template_render_request', ...args } as const;
const result = await this.#sendAndWaitForReply<TemplateRenderResponse>( const result = await this.#sendForReply<TemplateRenderResponse>(context, payload);
windowContext,
payload,
);
return result.data as any; return result.data as any;
}, },
}, },
store: { store: {
get: async <T>(key: string) => { get: async <T>(key: string) => {
const payload = { type: 'get_key_value_request', key } as const; const payload = { type: 'get_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<GetKeyValueResponse>( const result = await this.#sendForReply<GetKeyValueResponse>(context, payload);
windowContext,
payload,
);
return result.value ? (JSON.parse(result.value) as T) : undefined; return result.value ? (JSON.parse(result.value) as T) : undefined;
}, },
set: async <T>(key: string, value: T) => { set: async <T>(key: string, value: T) => {
@@ -602,20 +643,17 @@ export class PluginInstance {
key, key,
value: valueStr, value: valueStr,
}; };
await this.#sendAndWaitForReply<GetKeyValueResponse>(windowContext, payload); await this.#sendForReply<GetKeyValueResponse>(context, payload);
}, },
delete: async (key: string) => { delete: async (key: string) => {
const payload = { type: 'delete_key_value_request', key } as const; const payload = { type: 'delete_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<DeleteKeyValueResponse>( const result = await this.#sendForReply<DeleteKeyValueResponse>(context, payload);
windowContext,
payload,
);
return result.deleted; return result.deleted;
}, },
}, },
plugin: { plugin: {
reload: () => { reload: () => {
this.#sendPayload({ type: 'none' }, { type: 'reload_response', silent: true }, null); this.#sendPayload(context, { type: 'reload_response', silent: true }, null);
}, },
}, },
}; };
@@ -631,20 +669,6 @@ function genId(len = 5): string {
return id; 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> = {}; const watchedFiles: Record<string, Stats | null> = {};
/** /**

View File

@@ -0,0 +1,56 @@
import {
CallHttpAuthenticationActionArgs,
CallTemplateFunctionArgs,
JsonPrimitive,
TemplateFunctionArg,
} from '@yaakapp-internal/plugins';
import { Context, DynamicAuthenticationArg, DynamicTemplateFunctionArg } from '@yaakapp/api';
/** 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;
}
// Recurse down to all child inputs
if ('inputs' in input) {
newValues = applyFormInputDefaults(input.inputs ?? [], newValues);
}
}
return newValues;
}
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)) {
newArg.inputs = await applyDynamicFormInput(ctx, newArg.inputs, callArgs as any);
}
resolvedArgs.push(newArg);
}
return resolvedArgs;
}

View File

@@ -8,10 +8,15 @@ if (!port) {
throw new Error('Plugin runtime missing PORT') throw new Error('Plugin runtime missing PORT')
} }
const host = process.env.HOST;
if (!host) {
throw new Error('Plugin runtime missing HOST')
}
const pluginToAppEvents = new EventChannel(); const pluginToAppEvents = new EventChannel();
const plugins: Record<string, PluginHandle> = {}; const plugins: Record<string, PluginHandle> = {};
const ws = new WebSocket(`ws://localhost:${port}`); const ws = new WebSocket(`ws://${host}:${port}`);
ws.on('message', async (e: Buffer) => { ws.on('message', async (e: Buffer) => {
try { try {
@@ -34,7 +39,7 @@ async function handleIncoming(msg: string) {
const pluginEvent: InternalEvent = JSON.parse(msg); const pluginEvent: InternalEvent = JSON.parse(msg);
// Handle special event to bootstrap plugin // Handle special event to bootstrap plugin
if (pluginEvent.payload.type === 'boot_request') { 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; plugins[pluginEvent.pluginRefId] = plugin;
} }

View File

@@ -1,6 +1,8 @@
import { TemplateFunction } from '@yaakapp/api'; import type { TemplateFunctionPlugin } from '@yaakapp/api';
export function migrateTemplateFunctionSelectOptions(f: TemplateFunction): TemplateFunction { export function migrateTemplateFunctionSelectOptions(
f: TemplateFunctionPlugin,
): TemplateFunctionPlugin {
const migratedArgs = f.args.map((a) => { const migratedArgs = f.args.map((a) => {
if (a.type === 'select') { if (a.type === 'select') {
a.options = a.options.map((o) => ({ a.options = a.options.map((o) => ({
@@ -11,8 +13,5 @@ export function migrateTemplateFunctionSelectOptions(f: TemplateFunction): Templ
return a; return a;
}); });
return { return { ...f, args: migratedArgs };
...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

@@ -43,6 +43,26 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
finalUrl = base + separator + queryString + (hash ? `#${hash}` : ''); finalUrl = base + separator + queryString + (hash ? `#${hash}` : '');
} }
// Add API key authentication
if (request.authenticationType === 'apikey') {
if (request.authentication?.location === 'query') {
const sep = finalUrl.includes('?') ? '&' : '?';
finalUrl = [
finalUrl,
sep,
encodeURIComponent(request.authentication?.key ?? 'token'),
'=',
encodeURIComponent(request.authentication?.value ?? ''),
].join('');
} else {
request.headers = request.headers ?? [];
request.headers.push({
name: request.authentication?.key ?? 'X-Api-Key',
value: request.authentication?.value ?? '',
});
}
}
xs.push(quote(finalUrl)); xs.push(quote(finalUrl));
xs.push(NEWLINE); xs.push(NEWLINE);
@@ -82,21 +102,49 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
} }
// Add basic/digest authentication // Add basic/digest authentication
if (request.authenticationType === 'basic' || request.authenticationType === 'digest') { if (request.authentication?.disabled !== true) {
if (request.authenticationType === 'digest') xs.push('--digest'); if (request.authenticationType === 'basic' || request.authenticationType === 'digest') {
xs.push( if (request.authenticationType === 'digest') xs.push('--digest');
'--user', xs.push(
quote(`${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`), '--user',
); quote(
xs.push(NEWLINE); `${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`,
} ),
);
xs.push(NEWLINE);
}
// Add bearer authentication // Add bearer authentication
if (request.authenticationType === 'bearer') { if (request.authenticationType === 'bearer') {
const value = const value =
`${request.authentication?.prefix ?? 'Bearer'} ${request.authentication?.token ?? ''}`.trim(); `${request.authentication?.prefix ?? 'Bearer'} ${request.authentication?.token ?? ''}`.trim();
xs.push('--header', quote(`Authorization: ${value}`)); xs.push('--header', quote(`Authorization: ${value}`));
xs.push(NEWLINE); xs.push(NEWLINE);
}
if (request.authenticationType === 'auth-aws-sig-v4') {
xs.push(
'--aws-sigv4',
[
'aws',
'amz',
request.authentication?.region ?? '',
request.authentication?.service ?? '',
].join(':'),
);
xs.push(NEWLINE);
xs.push(
'--user',
quote(
`${request.authentication?.accessKeyId ?? ''}:${request.authentication?.secretAccessKey ?? ''}`,
),
);
if (request.authentication?.sessionToken) {
xs.push(NEWLINE);
xs.push('--header', quote(`X-Amz-Security-Token: ${request.authentication.sessionToken}`));
}
xs.push(NEWLINE);
}
} }
// Remove trailing newline // Remove trailing newline

View File

@@ -27,6 +27,7 @@ describe('exporter-curl', () => {
}), }),
).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 () => { test('Exports POST with url form data', async () => {
expect( expect(
await convertToCurl({ await convertToCurl({
@@ -170,6 +171,20 @@ describe('exporter-curl', () => {
).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 () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {
disabled: true,
username: 'user',
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`].join(` \\\n `));
});
test('Broken basic auth', async () => { test('Broken basic auth', async () => {
expect( expect(
await convertToCurl({ await convertToCurl({
@@ -246,6 +261,145 @@ describe('exporter-curl', () => {
).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 () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'auth-aws-sig-v4',
authentication: {
accessKeyId: 'ak',
secretAccessKey: 'sk',
sessionToken: '',
region: 'us-east-1',
service: 's3',
},
}),
).toEqual(
[`curl 'https://yaak.app'`, `--aws-sigv4 aws:amz:us-east-1:s3`, `--user 'ak:sk'`].join(
` \\\n `,
),
);
});
test('AWS v4 auth with session', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'auth-aws-sig-v4',
authentication: {
accessKeyId: 'ak',
secretAccessKey: 'sk',
sessionToken: 'st',
region: 'us-east-1',
service: 's3',
},
}),
).toEqual(
[
`curl 'https://yaak.app'`,
`--aws-sigv4 aws:amz:us-east-1:s3`,
`--user 'ak:sk'`,
`--header 'X-Amz-Security-Token: st'`,
].join(` \\\n `),
);
});
test('API key auth header', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'apikey',
authentication: {
location: 'header',
key: 'X-Header',
value: 'my-token',
},
}),
).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 () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'apikey',
authentication: {
location: 'header',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'X-Api-Key: '`].join(` \\\n `));
});
test('API key auth query', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'apikey',
authentication: {
location: 'query',
key: 'foo',
value: 'bar-baz',
},
}),
).toEqual([`curl 'https://yaak.app?foo=bar-baz'`].join(` \\\n `));
});
test('API key auth query with existing', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app?foo=bar&baz=qux',
authenticationType: 'apikey',
authentication: {
location: 'query',
key: 'hi',
value: 'there',
},
}),
).toEqual([`curl 'https://yaak.app?foo=bar&baz=qux&hi=there'`].join(` \\\n `));
});
test('API key auth query default', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app?foo=bar&baz=qux',
authenticationType: 'apikey',
authentication: {
location: 'query',
},
}),
).toEqual([`curl 'https://yaak.app?foo=bar&baz=qux&token='`].join(` \\\n `));
});
test('Stale body data', async () => { test('Stale body data', async () => {
expect( expect(
await convertToCurl({ await convertToCurl({

View File

@@ -68,16 +68,37 @@ export async function convert(request: Partial<GrpcRequest>, allProtoFiles: stri
} }
// Add basic authentication // Add basic authentication
if (request.authenticationType === 'basic') { if (request.authentication?.disabled !== true) {
const user = request.authentication?.username ?? ''; if (request.authenticationType === 'basic') {
const pass = request.authentication?.password ?? ''; const user = request.authentication?.username ?? '';
const encoded = btoa(`${user}:${pass}`); const pass = request.authentication?.password ?? '';
xs.push('-H', quote(`Authorization: Basic ${encoded}`)); const encoded = btoa(`${user}:${pass}`);
xs.push(NEWLINE); xs.push('-H', quote(`Authorization: Basic ${encoded}`));
} else if (request.authenticationType === 'bearer') { xs.push(NEWLINE);
// Add bearer authentication } else if (request.authenticationType === 'bearer') {
xs.push('-H', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`)); // Add bearer authentication
xs.push(NEWLINE); xs.push('-H', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`));
xs.push(NEWLINE);
} else if (request.authenticationType === 'apikey') {
if (request.authentication?.location === 'query') {
const sep = request.url?.includes('?') ? '&' : '?';
request.url = [
request.url,
sep,
encodeURIComponent(request.authentication?.key ?? 'token'),
'=',
encodeURIComponent(request.authentication?.value ?? ''),
].join('');
} else {
xs.push(
'-H',
quote(
`${request.authentication?.key ?? 'X-Api-Key'}: ${request.authentication?.value ?? ''}`,
),
);
}
xs.push(NEWLINE);
}
} }
// Add form params // Add form params

View File

@@ -27,6 +27,55 @@ 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(
await convert(
{
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {
username: 'user',
password: 'pass',
},
},
[],
),
).toEqual([`grpcurl -H 'Authorization: Basic dXNlcjpwYXNz'`, `yaak.app`].join(` \\\n `));
});
test('API key auth', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
authenticationType: 'apikey',
authentication: {
key: 'X-Token',
value: 'tok',
},
},
[],
),
).toEqual([`grpcurl -H 'X-Token: tok'`, `yaak.app`].join(` \\\n `));
});
test('API key auth', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
authenticationType: 'apikey',
authentication: {
location: 'query',
key: 'token',
value: 'tok 1',
},
},
[],
),
).toEqual([`grpcurl`, `yaak.app?token=tok%201`].join(` \\\n `));
});
test('Single proto file', async () => { test('Single proto file', async () => {
expect(await convert({ url: 'https://yaak.app' }, ['/foo/bar/baz.proto'])).toEqual( expect(await convert({ url: 'https://yaak.app' }, ['/foo/bar/baz.proto'])).toEqual(
[ [

View File

@@ -0,0 +1,49 @@
# AWS Signature Version 4 Auth
A plugin for authenticating AWS-compatible requests using the
[AWS Signature Version 4 signing process](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html).
This enables secure, signed requests to AWS services (or any S3-compatible APIs like
Cloudflare R2).
![Screenshot of AWS SigV4 UI](screenshot.png)
## Overview
This plugin provides AWS Signature authentication for API requests in Yaak. SigV4 is used
by nearly all AWS APIs to verify the authenticity and integrity of requests using
cryptographic signatures.
With this plugin, you can securely sign requests to AWS services such as S3, STS, Lambda,
API Gateway, DynamoDB, and more. You can also authenticate against S3-compatible services
like **Cloudflare R2**, **MinIO**, or **Wasabi**.
## How AWS Signature Version 4 Works
SigV4 signs requests by creating a hash of key request components (method, URL, headers,
and optionally the payload) using your AWS credentials. The resulting HMAC signature is
added in the `Authorization` header along with credential scope metadata.
Example header:
```
Authorization: AWS4-HMAC-SHA256 Credential=AKIA…/20251011/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=abcdef123456…
```
Each request must include a timestamp (`X-Amz-Date`) and may include a session token if
using temporary credentials.
## Configuration
The plugin presents the following fields:
- **Access Key ID** Your AWS access key identifier
- **Secret Access Key** The secret associated with the access key
- **Session Token** *(optional)* Used for temporary or assumed-role credentials (treated as secret)
- **Region** AWS region (e.g., `us-east-1`)
- **Service** AWS service identifier (e.g., `sts`, `s3`, `execute-api`)
## Usage
1. Configure a request, folder, or workspace to use **AWS SigV4 Authentication**
2. Enter your AWS credentials and target service/region
3. The plugin will automatically sign outgoing requests with valid SigV4 headers

View File

@@ -0,0 +1,23 @@
{
"name": "@yaak/auth-aws",
"displayName": "AWS SigV4",
"description": "Authenticate requests using AWS SigV4 signing",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-aws"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
},
"dependencies": {
"aws4": "^1.13.2"
},
"devDependencies": {
"@types/aws4": "^1.11.6"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

View File

@@ -0,0 +1,88 @@
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';
export const plugin: PluginDefinition = {
authentication: {
name: 'awsv4',
label: 'AWS Signature',
shortLabel: 'AWS v4',
args: [
{ name: 'accessKeyId', label: 'Access Key ID', type: 'text', password: true },
{
name: 'secretAccessKey',
label: 'Secret Access Key',
type: 'text',
password: true,
},
{
name: 'service',
label: 'Service Name',
type: 'text',
defaultValue: 'sts',
placeholder: 'sts',
description: 'The service that is receiving the request (sts, s3, sqs, ...)',
},
{
name: 'region',
label: 'Region',
type: 'text',
placeholder: 'us-east-1',
description: 'The region that is receiving the request (defaults to us-east-1)',
optional: true,
},
{
name: 'sessionToken',
label: 'Session Token',
type: 'text',
password: true,
optional: true,
description: 'Only required if you are using temporary credentials',
},
],
onApply(_ctx, { values, ...args }): CallHttpAuthenticationResponse {
const accessKeyId = String(values.accessKeyId || '');
const secretAccessKey = String(values.secretAccessKey || '');
const sessionToken = String(values.sessionToken || '') || undefined;
const url = new URL(args.url);
const headers: NonNullable<Request['headers']> = {};
for (const headerName of ['content-type', 'host', 'x-amz-date', 'x-amz-security-token']) {
const v = args.headers.find((h) => h.name.toLowerCase() === headerName);
if (v != null) {
headers[headerName] = v.value;
}
}
const signature = aws4.sign(
{
host: url.host,
method: args.method,
path: url.pathname + (url.search || ''),
service: String(values.service || 'sts'),
region: values.region ? String(values.region) : undefined,
body: values.body ? String(values.body) : undefined,
headers,
},
{
accessKeyId,
secretAccessKey,
sessionToken,
},
);
if (signature.headers == null) {
return {};
}
return {
setHeaders: Object.entries(signature.headers)
.filter(([name]) => name !== 'content-type') // Don't add this because we already have it
.map(([name, value]) => ({ name, value: String(value || '') })),
};
},
},
};

View File

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

View File

@@ -46,6 +46,50 @@ export const plugin: PluginDefinition = {
name: 'secretBase64', name: 'secretBase64',
label: 'Secret is base64 encoded', label: 'Secret is base64 encoded',
}, },
{
type: 'select',
name: 'location',
label: 'Behavior',
defaultValue: 'header',
options: [
{ label: 'Insert Header', value: 'header' },
{ label: 'Append Query Parameter', value: 'query' },
],
},
{
type: 'text',
name: 'name',
label: 'Header Name',
defaultValue: 'Authorization',
optional: true,
dynamic(_ctx, args) {
if (args.values.location === 'query') {
return {
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',
};
}
},
},
{
type: 'text',
name: 'headerPrefix',
label: 'Header Prefix',
optional: true,
defaultValue: 'Bearer',
dynamic(_ctx, args) {
if (args.values.location === 'query') {
return {
hidden: true,
};
}
},
},
{ {
type: 'editor', type: 'editor',
name: 'payload', name: 'payload',
@@ -61,8 +105,17 @@ export const plugin: PluginDefinition = {
const token = jwt.sign(`${payload}`, secret, { const token = jwt.sign(`${payload}`, secret, {
algorithm: algorithm as (typeof algorithms)[number], algorithm: algorithm as (typeof algorithms)[number],
}); });
const value = `Bearer ${token}`;
return { setHeaders: [{ name: 'Authorization', value }] }; if (values.location === 'query') {
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 }] };
}
}, },
}, },
}; };

View File

@@ -0,0 +1,20 @@
{
"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",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
},
"dependencies": {
"httpntlm": "^1.8.13"
}
}

View File

@@ -0,0 +1,76 @@
import type { PluginDefinition } from '@yaakapp/api';
import { ntlm } from 'httpntlm';
export const plugin: PluginDefinition = {
authentication: {
name: 'windows',
label: 'NTLM Auth',
shortLabel: 'NTLM',
args: [
{
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,20 @@
{
"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",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
},
"dependencies": {
"oauth-1.0a": "^2.2.6"
}
}

View File

@@ -0,0 +1,197 @@
import type { Context, GetHttpAuthenticationConfigRequest, PluginDefinition } from '@yaakapp/api';
import crypto from 'node:crypto';
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: [
{
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);
requestData.data[key] = all.length > 1 ? all : all[0]!;
}
// (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,6 +12,7 @@
"scripts": { "scripts": {
"build": "yaakcli build", "build": "yaakcli build",
"dev": "yaakcli dev", "dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx" "lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
} }
} }

View File

@@ -4,6 +4,7 @@ import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken'; import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import type { AccessToken, TokenStoreArgs } from '../store'; import type { AccessToken, TokenStoreArgs } from '../store';
import { getDataDirKey, storeToken } from '../store'; import { getDataDirKey, storeToken } from '../store';
import { extractCode } from '../util';
export const PKCE_SHA256 = 'S256'; export const PKCE_SHA256 = 'S256';
export const PKCE_PLAIN = 'plain'; export const PKCE_PLAIN = 'plain';
@@ -79,7 +80,6 @@ export async function getAuthorizationCode(
authorizationUrl.searchParams.set('code_challenge_method', pkce.challengeMethod); authorizationUrl.searchParams.set('code_challenge_method', pkce.challengeMethod);
} }
const logsEnabled = (await ctx.store.get('enable_logs')) ?? false;
const dataDirKey = await getDataDirKey(ctx, contextId); const dataDirKey = await getDataDirKey(ctx, contextId);
const authorizationUrlStr = authorizationUrl.toString(); const authorizationUrlStr = authorizationUrl.toString();
console.log('[oauth2] Authorizing', authorizationUrlStr); console.log('[oauth2] Authorizing', authorizationUrlStr);
@@ -97,18 +97,17 @@ export async function getAuthorizationCode(
} }
}, },
async onNavigate({ url: urlStr }) { async onNavigate({ url: urlStr }) {
const url = new URL(urlStr); let code;
if (logsEnabled) console.log('[oauth2] Navigated to', urlStr); try {
code = extractCode(urlStr, redirectUri);
if (url.searchParams.has('error')) { } catch (err) {
reject(err);
close(); close();
return reject(new Error(`Failed to authorize: ${url.searchParams.get('error')}`)); return;
} }
const code = url.searchParams.get('code');
if (!code) { if (!code) {
console.log('[oauth2] Code not found'); return;
return; // Could be one of many redirects in a chain, so skip it
} }
// Close the window here, because we don't need it anymore! // Close the window here, because we don't need it anymore!

View File

@@ -6,8 +6,8 @@ import type {
PluginDefinition, PluginDefinition,
} from '@yaakapp/api'; } from '@yaakapp/api';
import { import {
genPkceCodeVerifier,
DEFAULT_PKCE_METHOD, DEFAULT_PKCE_METHOD,
genPkceCodeVerifier,
getAuthorizationCode, getAuthorizationCode,
PKCE_PLAIN, PKCE_PLAIN,
PKCE_SHA256, PKCE_SHA256,
@@ -125,17 +125,6 @@ export const plugin: PluginDefinition = {
await resetDataDirKey(ctx, contextId); await resetDataDirKey(ctx, contextId);
}, },
}, },
{
label: 'Toggle Debug Logs',
async onSelect(ctx) {
const enableLogs = !(await ctx.store.get('enable_logs'));
await ctx.store.set('enable_logs', enableLogs);
await ctx.toast.show({
message: `Debug logs ${enableLogs ? 'enabled' : 'disabled'}`,
color: 'info',
});
},
},
], ],
args: [ args: [
{ {
@@ -271,6 +260,12 @@ export const plugin: PluginDefinition = {
label: 'Advanced', label: 'Advanced',
inputs: [ inputs: [
{ type: 'text', name: 'scope', label: 'Scope', optional: true }, { type: 'text', name: 'scope', label: 'Scope', optional: true },
{
type: 'text',
name: 'headerName',
label: 'Header Name',
defaultValue: 'Authorization',
},
{ {
type: 'text', type: 'text',
name: 'headerPrefix', name: 'headerPrefix',
@@ -293,6 +288,7 @@ export const plugin: PluginDefinition = {
{ {
type: 'accordion', type: 'accordion',
label: 'Access Token Response', label: 'Access Token Response',
inputs: [],
async dynamic(ctx, { contextId, values }) { async dynamic(ctx, { contextId, values }) {
const tokenArgs: TokenStoreArgs = { const tokenArgs: TokenStoreArgs = {
contextId, contextId,
@@ -309,6 +305,7 @@ export const plugin: PluginDefinition = {
inputs: [ inputs: [
{ {
type: 'editor', type: 'editor',
name: 'response',
defaultValue: JSON.stringify(token.response, null, 2), defaultValue: JSON.stringify(token.response, null, 2),
hideLabel: true, hideLabel: true,
readOnly: true, readOnly: true,
@@ -397,15 +394,9 @@ export const plugin: PluginDefinition = {
throw new Error('Invalid grant type ' + grantType); throw new Error('Invalid grant type ' + grantType);
} }
const headerName = stringArg(values, 'headerName') || 'Authorization';
const headerValue = `${headerPrefix} ${token.response[tokenName]}`.trim(); const headerValue = `${headerPrefix} ${token.response[tokenName]}`.trim();
return { return { setHeaders: [{ name: headerName, value: headerValue }] };
setHeaders: [
{
name: 'Authorization',
value: headerValue,
},
],
};
}, },
}, },
}; };

View File

@@ -3,3 +3,83 @@ import type { AccessToken } from './store';
export function isTokenExpired(token: AccessToken) { export function isTokenExpired(token: AccessToken) {
return token.expiresAt && Date.now() > token.expiresAt; return token.expiresAt && Date.now() > token.expiresAt;
} }
export function extractCode(urlStr: string, redirectUri: string | null): string | null {
const url = new URL(urlStr);
if (!urlMatchesRedirect(url, redirectUri)) {
console.log('[oauth2] URL does not match redirect origin/path; skipping.');
return null;
}
// Prefer query param; fall back to fragment if query lacks it
const query = url.searchParams;
const queryError = query.get('error');
const queryDesc = query.get('error_description');
const queryUri = query.get('error_uri');
let hashParams: URLSearchParams | null = null;
if (url.hash && url.hash.length > 1) {
hashParams = new URLSearchParams(url.hash.slice(1));
}
const hashError = hashParams?.get('error');
const hashDesc = hashParams?.get('error_description');
const hashUri = hashParams?.get('error_uri');
const error = queryError || hashError;
if (error) {
const desc = queryDesc || hashDesc;
const uri = queryUri || hashUri;
let message = `Failed to authorize: ${error}`;
if (desc) message += ` (${desc})`;
if (uri) message += ` [${uri}]`;
throw new Error(message);
}
const queryCode = query.get('code');
if (queryCode) return queryCode;
const hashCode = hashParams?.get('code');
if (hashCode) return hashCode;
console.log('[oauth2] Code not found');
return null;
}
export function urlMatchesRedirect(url: URL, redirectUrl: string | null): boolean {
if (!redirectUrl) return true;
let redirect;
try {
redirect = new URL(redirectUrl);
} catch {
console.log('[oauth2] Invalid redirect URI; skipping.');
return false;
}
const sameProtocol = url.protocol === redirect.protocol;
const sameHost = url.hostname.toLowerCase() === redirect.hostname.toLowerCase();
const normalizePort = (u: URL) =>
(u.protocol === 'https:' && (!u.port || u.port === '443')) ||
(u.protocol === 'http:' && (!u.port || u.port === '80'))
? ''
: u.port;
const samePort = normalizePort(url) === normalizePort(redirect);
const normPath = (p: string) => {
const withLeading = p.startsWith('/') ? p : `/${p}`;
// strip trailing slashes, keep root as "/"
return withLeading.replace(/\/+$/g, '') || '/';
};
// Require redirect path to be a prefix of the navigated URL path
const urlPath = normPath(url.pathname);
const redirectPath = normPath(redirect.pathname);
const pathMatches = urlPath === redirectPath || urlPath.startsWith(`${redirectPath}/`);
return sameProtocol && sameHost && samePort && pathMatches;
}

View File

@@ -0,0 +1,109 @@
import { describe, test, expect } from 'vitest';
import { extractCode } from '../src/util';
describe('extractCode', () => {
test('extracts code from query when same origin + path', () => {
const url = 'https://app.example.com/cb?code=abc123&state=xyz';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('abc123');
});
test('extracts code from query with weird path', () => {
const url = 'https://app.example.com/cbwithextra?code=abc123&state=xyz';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBeNull();
});
test('allows trailing slash differences', () => {
expect(extractCode('https://app.example.com/cb/?code=abc', 'https://app.example.com/cb')).toBe(
'abc',
);
expect(extractCode('https://app.example.com/cb?code=abc', 'https://app.example.com/cb/')).toBe(
'abc',
);
});
test('treats default ports as equal (https:443, http:80)', () => {
expect(
extractCode('https://app.example.com/cb?code=abc', 'https://app.example.com:443/cb'),
).toBe('abc');
expect(extractCode('http://app.example.com/cb?code=abc', 'http://app.example.com:80/cb')).toBe(
'abc',
);
});
test('rejects different port', () => {
expect(
extractCode('https://app.example.com/cb?code=abc', 'https://app.example.com:8443/cb'),
).toBeNull();
});
test('rejects different hostname (including subdomain changes)', () => {
expect(
extractCode('https://evil.example.com/cb?code=abc', 'https://app.example.com/cb'),
).toBeNull();
});
test('requires path to start with redirect path (ignoring query/hash)', () => {
// same origin but wrong path -> null
expect(
extractCode('https://app.example.com/other?code=abc', 'https://app.example.com/cb'),
).toBeNull();
// deeper subpath under the redirect path -> allowed (prefix match)
expect(
extractCode('https://app.example.com/cb/deep?code=abc', 'https://app.example.com/cb'),
).toBe('abc');
});
test('works with custom schemes', () => {
expect(extractCode('myapp://cb?code=abc', 'myapp://cb')).toBe('abc');
});
test('prefers query over fragment when both present', () => {
const url = 'https://app.example.com/cb?code=queryCode#code=hashCode';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('queryCode');
});
test('extracts code from fragment when query lacks code', () => {
const url = 'https://app.example.com/cb#code=fromHash&state=xyz';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('fromHash');
});
test('returns null if no code present (query or fragment)', () => {
const url = 'https://app.example.com/cb?state=only';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBeNull();
});
test('returns null when provider reports an error', () => {
const url = 'https://app.example.com/cb?error=access_denied&error_description=oopsy';
const redirect = 'https://app.example.com/cb';
expect(() => extractCode(url, redirect)).toThrow('Failed to authorize: access_denied');
});
test('when redirectUri is null, extracts code from any URL', () => {
expect(extractCode('https://random.example.com/whatever?code=abc', null)).toBe('abc');
});
test('handles extra params gracefully', () => {
const url = 'https://app.example.com/cb?foo=1&bar=2&code=abc&baz=3';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('abc');
});
test('ignores fragment noise when code is in query', () => {
const url = 'https://app.example.com/cb?code=abc#some=thing';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('abc');
});
// If you decide NOT to support fragment-based codes, flip these to expect null or mark as .skip
test('supports fragment-only code for response_mode=fragment providers', () => {
const url = 'https://app.example.com/cb#state=xyz&code=abc';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('abc');
});
});

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) { export function isJSObject(obj: unknown) {
return Object.prototype.toString.call(obj) === '[object Object]'; return Object.prototype.toString.call(obj) === '[object Object]';
} }
@@ -32,3 +26,18 @@ export function deleteUndefinedAttrs<T>(obj: T): T {
return obj; return obj;
} }
} }
/** Recursively render all nested object properties */
export function convertTemplateSyntax<T>(obj: T): T {
if (typeof obj === 'string') {
return obj.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') as T;
} else if (Array.isArray(obj) && obj != null) {
return obj.map(convertTemplateSyntax) as T;
} else if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)]),
) as T;
} else {
return obj;
}
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import type { PartialImportResources } from '@yaakapp/api'; import type { PartialImportResources } from '@yaakapp/api';
import { convertId, convertSyntax, isJSObject } from './common'; import { convertId, convertTemplateSyntax, isJSObject } from './common';
export function convertInsomniaV4(parsed: any) { export function convertInsomniaV4(parsed: any) {
if (!Array.isArray(parsed.resources)) return null; if (!Array.isArray(parsed.resources)) return null;
@@ -60,7 +60,7 @@ export function convertInsomniaV4(parsed: any) {
resources.environments = resources.environments.filter(Boolean); resources.environments = resources.environments.filter(Boolean);
resources.workspaces = resources.workspaces.filter(Boolean); resources.workspaces = resources.workspaces.filter(Boolean);
return { resources }; return { resources: convertTemplateSyntax(resources) };
} }
function importHttpRequest(r: any, workspaceId: string): PartialImportResources['httpRequests'][0] { 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') { } else if (r.body?.mimeType === 'application/graphql') {
bodyType = 'graphql'; bodyType = 'graphql';
body = { text: convertSyntax(r.body.text ?? '') }; body = { text: r.body.text ?? '' };
} else if (r.body?.mimeType === 'application/json') { } else if (r.body?.mimeType === 'application/json') {
bodyType = 'application/json'; bodyType = 'application/json';
body = { text: convertSyntax(r.body.text ?? '') }; body = { text: r.body.text ?? '' };
} }
let authenticationType: string | null = null; let authenticationType: string | null = null;
@@ -101,13 +101,13 @@ function importHttpRequest(r: any, workspaceId: string): PartialImportResources[
if (r.authentication.type === 'bearer') { if (r.authentication.type === 'bearer') {
authenticationType = 'bearer'; authenticationType = 'bearer';
authentication = { authentication = {
token: convertSyntax(r.authentication.token), token: r.authentication.token,
}; };
} else if (r.authentication.type === 'basic') { } else if (r.authentication.type === 'basic') {
authenticationType = 'basic'; authenticationType = 'basic';
authentication = { authentication = {
username: convertSyntax(r.authentication.username), username: r.authentication.username,
password: convertSyntax(r.authentication.password), password: r.authentication.password,
}; };
} }
@@ -121,7 +121,12 @@ function importHttpRequest(r: any, workspaceId: string): PartialImportResources[
sortPriority: r.metaSortKey, sortPriority: r.metaSortKey,
name: r.name, name: r.name,
description: r.description || undefined, description: r.description || undefined,
url: convertSyntax(r.url), url: r.url,
urlParameters: (r.parameters ?? []).map((p: any) => ({
enabled: !p.disabled,
name: p.name ?? '',
value: p.value ?? '',
})),
body, body,
bodyType, bodyType,
authentication, authentication,
@@ -152,7 +157,7 @@ function importGrpcRequest(r: any, workspaceId: string): PartialImportResources[
sortPriority: r.metaSortKey, sortPriority: r.metaSortKey,
name: r.name, name: r.name,
description: r.description || undefined, description: r.description || undefined,
url: convertSyntax(r.url), url: r.url,
service, service,
method, method,
message: r.body?.text ?? '', message: r.body?.text ?? '',
@@ -184,15 +189,15 @@ function importEnvironment(
workspaceId: string, workspaceId: string,
isParent?: boolean, isParent?: boolean,
): PartialImportResources['environments'][0] { ): PartialImportResources['environments'][0] {
isParent ??= e.parentId === workspaceId;
return { return {
id: convertId(e._id), id: convertId(e._id),
createdAt: e.created ? new Date(e.created).toISOString().replace('Z', '') : undefined, createdAt: e.created ? new Date(e.created).toISOString().replace('Z', '') : undefined,
updatedAt: e.modified ? new Date(e.modified).toISOString().replace('Z', '') : undefined, updatedAt: e.modified ? new Date(e.modified).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId), workspaceId: convertId(workspaceId),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment sortPriority: e.metaSortKey,
// @ts-expect-error parentModel: isParent ? 'workspace' : 'environment',
sortPriority: e.metaSortKey, // Will be added to Yaak later parentId: null,
base: isParent ?? e.parentId === workspaceId,
model: 'environment', model: 'environment',
name: e.name, name: e.name,
variables: Object.entries(e.data).map(([name, value]) => ({ variables: Object.entries(e.data).map(([name, value]) => ({

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import type { PartialImportResources } from '@yaakapp/api'; import type { PartialImportResources } from '@yaakapp/api';
import { convertId, convertSyntax, isJSObject } from './common'; import { convertId, convertTemplateSyntax, isJSObject } from './common';
export function convertInsomniaV5(parsed: any) { export function convertInsomniaV5(parsed: any) {
// Assert parsed is object // Assert parsed is object
@@ -69,7 +69,7 @@ export function convertInsomniaV5(parsed: any) {
resources.environments = resources.environments.filter(Boolean); resources.environments = resources.environments.filter(Boolean);
resources.workspaces = resources.workspaces.filter(Boolean); resources.workspaces = resources.workspaces.filter(Boolean);
return { resources }; return { resources: convertTemplateSyntax(resources) };
} }
function importHttpRequest( function importHttpRequest(
@@ -108,10 +108,10 @@ function importHttpRequest(
}; };
} else if (r.body?.mimeType === 'application/graphql') { } else if (r.body?.mimeType === 'application/graphql') {
bodyType = 'graphql'; bodyType = 'graphql';
body = { text: convertSyntax(r.body.text ?? '') }; body = { text: r.body.text ?? '' };
} else if (r.body?.mimeType === 'application/json') { } else if (r.body?.mimeType === 'application/json') {
bodyType = 'application/json'; bodyType = 'application/json';
body = { text: convertSyntax(r.body.text ?? '') }; body = { text: r.body.text ?? '' };
} }
return { return {
@@ -124,7 +124,12 @@ function importHttpRequest(
model: 'http_request', model: 'http_request',
name: r.name, name: r.name,
description: r.meta?.description || undefined, description: r.meta?.description || undefined,
url: convertSyntax(r.url), url: r.url,
urlParameters: (r.parameters ?? []).map((p: any) => ({
enabled: !p.disabled,
name: p.name ?? '',
value: p.value ?? '',
})),
body, body,
bodyType, bodyType,
method: r.method, method: r.method,
@@ -157,7 +162,7 @@ function importGrpcRequest(
sortPriority: sortKey, sortPriority: sortKey,
name: r.name, name: r.name,
description: r.description || undefined, description: r.description || undefined,
url: convertSyntax(r.url), url: r.url,
service, service,
method, method,
message: r.body?.text ?? '', message: r.body?.text ?? '',
@@ -191,7 +196,7 @@ function importWebsocketRequest(
sortPriority: sortKey, sortPriority: sortKey,
name: r.name, name: r.name,
description: r.description || undefined, description: r.description || undefined,
url: convertSyntax(r.url), url: r.url,
message: r.body?.text ?? '', message: r.body?.text ?? '',
...importHeaders(r), ...importHeaders(r),
...importAuthentication(r), ...importAuthentication(r),
@@ -215,13 +220,13 @@ function importAuthentication(obj: any) {
if (obj.authentication?.type === 'bearer') { if (obj.authentication?.type === 'bearer') {
authenticationType = 'bearer'; authenticationType = 'bearer';
authentication = { authentication = {
token: convertSyntax(obj.authentication.token), token: obj.authentication.token,
}; };
} else if (obj.authentication?.type === 'basic') { } else if (obj.authentication?.type === 'basic') {
authenticationType = 'basic'; authenticationType = 'basic';
authentication = { authentication = {
username: convertSyntax(obj.authentication.username), username: obj.authentication.username,
password: convertSyntax(obj.authentication.password), password: obj.authentication.password,
}; };
} }
@@ -295,9 +300,7 @@ function importEnvironment(
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined, updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId), workspaceId: convertId(workspaceId),
public: !e.isPrivate, public: !e.isPrivate,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment sortPriority: sortKey,
// @ts-expect-error
sortPriority: sortKey, // Will be added to Yaak later
parentModel: isParent ? 'workspace' : 'environment', parentModel: isParent ? 'workspace' : 'environment',
parentId: null, parentId: null,
model: 'environment', model: 'environment',

View File

@@ -5,7 +5,8 @@
"createdAt": "2025-01-13T15:15:43.767", "createdAt": "2025-01-13T15:15:43.767",
"updatedAt": "2025-01-13T15:15:55.209", "updatedAt": "2025-01-13T15:15:55.209",
"sortPriority": 1736781343767, "sortPriority": 1736781343767,
"base": true, "parentId": null,
"parentModel": "workspace",
"id": "GENERATE_ID::env_16c0dec5b77c414ae0e419b8f10c3701300c5900", "id": "GENERATE_ID::env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"model": "environment", "model": "environment",
"name": "Base Environment", "name": "Base Environment",
@@ -22,7 +23,8 @@
"createdAt": "2025-01-13T15:15:58.515", "createdAt": "2025-01-13T15:15:58.515",
"updatedAt": "2025-01-13T15:16:34.705", "updatedAt": "2025-01-13T15:16:34.705",
"sortPriority": 1736781358515, "sortPriority": 1736781358515,
"base": false, "parentId": null,
"parentModel": "environment",
"id": "GENERATE_ID::env_799ae3d723ef44af91b4817e5d057e6d", "id": "GENERATE_ID::env_799ae3d723ef44af91b4817e5d057e6d",
"model": "environment", "model": "environment",
"name": "Production", "name": "Production",
@@ -39,7 +41,8 @@
"createdAt": "2025-01-13T15:16:14.707", "createdAt": "2025-01-13T15:16:14.707",
"updatedAt": "2025-01-13T15:16:31.078", "updatedAt": "2025-01-13T15:16:31.078",
"sortPriority": 1736781358565, "sortPriority": 1736781358565,
"base": false, "parentId": null,
"parentModel": "environment",
"id": "GENERATE_ID::env_030fbfdbb274426ebd78e2e6518f8553", "id": "GENERATE_ID::env_030fbfdbb274426ebd78e2e6518f8553",
"model": "environment", "model": "environment",
"name": "Staging", "name": "Staging",
@@ -110,6 +113,13 @@
"model": "http_request", "model": "http_request",
"name": "New Request", "name": "New Request",
"url": "${[BASE_URL ]}/foo/:id", "url": "${[BASE_URL ]}/foo/:id",
"urlParameters": [
{
"name": "query",
"value": "qqq",
"enabled": true
}
],
"workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019" "workspaceId": "GENERATE_ID::wrk_d4d92f7c0ee947b89159243506687019"
} }
], ],

View File

@@ -76,6 +76,7 @@
"sortPriority": -1747414129276, "sortPriority": -1747414129276,
"updatedAt": "2025-05-16T16:48:49.313", "updatedAt": "2025-05-16T16:48:49.313",
"url": "https://httpbin.org/post", "url": "https://httpbin.org/post",
"urlParameters": [],
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c" "workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
}, },
{ {
@@ -98,6 +99,7 @@
"name": "New Request", "name": "New Request",
"sortPriority": -1747414160498, "sortPriority": -1747414160498,
"updatedAt": "2025-05-16T16:49:20.497", "updatedAt": "2025-05-16T16:49:20.497",
"urlParameters": [],
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c" "workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
} }
], ],

View File

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

View File

@@ -127,6 +127,11 @@
"enabled": true, "enabled": true,
"name": "X-Header", "name": "X-Header",
"value": "xxxx" "value": "xxxx"
},
{
"enabled": true,
"name": "${[ApiHeaderName ]}",
"value": "${[ApiKey ]}"
} }
], ],
"id": "GENERATE_ID::req_d72fff2a6b104b91a2ebe9de9edd2785", "id": "GENERATE_ID::req_d72fff2a6b104b91a2ebe9de9edd2785",
@@ -135,6 +140,13 @@
"name": "New Request", "name": "New Request",
"sortPriority": -1736781406672, "sortPriority": -1736781406672,
"url": "${[BASE_URL ]}/foo/:id", "url": "${[BASE_URL ]}/foo/:id",
"urlParameters": [
{
"name": "query",
"value": "qqq",
"enabled": true
}
],
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53" "workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
} }
], ],

View File

@@ -0,0 +1,14 @@
{
"name": "@yaak/importer-postman-environment",
"displayName": "Postman Environment Importer",
"description": "Import environments from Postman",
"private": true,
"version": "0.1.0",
"main": "./build/index.js",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

@@ -0,0 +1,138 @@
import type {
Context,
Environment,
PartialImportResources,
PluginDefinition,
Workspace,
} from '@yaakapp/api';
import type { ImportPluginResponse } from '@yaakapp/api/lib/plugins/ImporterPlugin';
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
interface ExportResources {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
}
export const plugin: PluginDefinition = {
importer: {
name: 'Postman Environment',
description: 'Import postman environment exports',
onImport(_ctx: Context, args: { text: string }) {
return convertPostmanEnvironment(args.text);
},
},
};
export function convertPostmanEnvironment(contents: string): ImportPluginResponse | undefined {
const root = parseJSONToRecord(contents);
if (root == null) return;
// Validate that it looks like a Postman Environment export
const values = toArray<{
key?: string;
value?: unknown;
enabled?: boolean;
description?: string;
type?: string;
}>(root.values);
const scope = root._postman_variable_scope;
const hasEnvMarkers = typeof scope === 'string';
if (values.length === 0 || (!hasEnvMarkers && typeof root.name !== 'string')) {
// Not a Postman environment file, skip
return;
}
const exportResources: ExportResources = {
workspaces: [],
environments: [],
};
const envVariables = values
.map((v) => ({
enabled: v.enabled ?? true,
name: String(v.key ?? ''),
value: String(v.value),
description: v.description ? String(v.description) : null,
}))
.filter((v) => v.name.length > 0);
const environment: ExportResources['environments'][0] = {
model: 'environment',
id: generateId('environment'),
name: root.name ? String(root.name) : 'Environment',
workspaceId: 'CURRENT_WORKSPACE',
parentModel: 'environment',
parentId: null,
variables: envVariables,
};
exportResources.environments.push(environment);
const resources = deleteUndefinedAttrs(
convertTemplateSyntax(exportResources),
) as PartialImportResources;
return { resources };
}
function parseJSONToRecord<T>(jsonStr: string): Record<string, T> | null {
try {
return toRecord(JSON.parse(jsonStr));
} catch {
return null;
}
}
function toRecord<T>(value: Record<string, T> | unknown): Record<string, T> {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value as Record<string, T>;
}
return {} as 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[];
}
/** 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.map(convertTemplateSyntax) as T;
} else 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;
}
}
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) {
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;
}
}
const idCount: Partial<Record<string, number>> = {};
function generateId(model: string): string {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}
export default plugin;

View File

@@ -0,0 +1,27 @@
{
"id": "123",
"name": "My Environment",
"values": [
{
"key": "baseUrl",
"value": "https://api.example.com",
"type": "default",
"enabled": true
},
{
"key": "token",
"value": "{{ access_token }}",
"type": "default",
"description": "Access token for the API.",
"enabled": true
},
{
"key": "disabled",
"type": "secret",
"value": "hello",
"enabled": false
}
],
"_postman_variable_scope": "environment",
"_postman_exported_using": "PostmanRuntime/1.0.0"
}

View File

@@ -0,0 +1,35 @@
{
"resources": {
"workspaces": [],
"environments": [
{
"id": "GENERATE_ID::ENVIRONMENT_0",
"model": "environment",
"name": "My Environment",
"variables": [
{
"enabled": true,
"description": null,
"name": "baseUrl",
"value": "https://api.example.com"
},
{
"enabled": true,
"description": "Access token for the API.",
"name": "token",
"value": "${[access_token]}"
},
{
"enabled": false,
"description": null,
"name": "disabled",
"value": "hello"
}
],
"workspaceId": "CURRENT_WORKSPACE",
"parentId": null,
"parentModel": "environment"
}
]
}
}

View File

@@ -0,0 +1,22 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { describe, expect, test } from 'vitest';
import { convertPostmanEnvironment } from '../src';
describe('importer-postman-environment', () => {
const p = path.join(__dirname, 'fixtures');
const fixtures = fs.readdirSync(p);
for (const fixture of fixtures) {
if (fixture.includes('.output')) {
continue;
}
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);
expect(result).toEqual(JSON.parse(expected));
});
}
});

View File

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

View File

@@ -68,6 +68,7 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
id: generateId('workspace'), id: generateId('workspace'),
name: info.name ? String(info.name) : 'Postman Import', name: info.name ? String(info.name) : 'Postman Import',
description, description,
...globalAuth,
}; };
exportResources.workspaces.push(workspace); exportResources.workspaces.push(workspace);
@@ -105,8 +106,7 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
} else if (typeof v.name === 'string' && 'request' in v) { } else if (typeof v.name === 'string' && 'request' in v) {
const r = toRecord(v.request); const r = toRecord(v.request);
const bodyPatch = importBody(r.body); const bodyPatch = importBody(r.body);
const requestAuthPath = importAuth(r.auth); const requestAuth = importAuth(r.auth);
const authPatch = requestAuthPath.authenticationType == null ? globalAuth : requestAuthPath;
const headers: HttpRequestHeader[] = toArray<{ const headers: HttpRequestHeader[] = toArray<{
key: string; key: string;
@@ -145,10 +145,9 @@ export function convertPostman(contents: string): ImportPluginResponse | undefin
urlParameters, urlParameters,
body: bodyPatch.body, body: bodyPatch.body,
bodyType: bodyPatch.bodyType, bodyType: bodyPatch.bodyType,
authentication: authPatch.authentication,
authenticationType: authPatch.authenticationType,
sortPriority: sortPriorityIndex++, sortPriority: sortPriorityIndex++,
headers, headers,
...requestAuth,
}; };
exportResources.httpRequests.push(request); exportResources.httpRequests.push(request);
} else { } else {
@@ -223,25 +222,159 @@ function convertUrl(rawUrl: string | unknown): Pick<HttpRequest, 'url' | 'urlPar
} }
function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'authenticationType'> { function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'authenticationType'> {
const auth = toRecord<{ username?: string; password?: string; token?: string }>(rawAuth); const auth = toRecord<Record<string, string>>(rawAuth);
if ('basic' in auth) {
// Helper: Postman stores auth params as an array of { key, value, ... }
const pmArrayToObj = (v: unknown): Record<string, unknown> => {
if (!Array.isArray(v)) return toRecord(v);
const o: Record<string, unknown> = {};
for (const i of v) {
const ii = toRecord(i);
if (typeof ii.key === 'string') {
o[ii.key] = ii.value;
}
}
return o;
};
const authType: string | undefined = auth.type ? String(auth.type) : undefined;
if (authType === 'noauth') {
return {
authenticationType: 'none',
authentication: {},
};
}
if ('basic' in auth && authType === 'basic') {
const b = pmArrayToObj(auth.basic);
return { return {
authenticationType: 'basic', authenticationType: 'basic',
authentication: { authentication: {
username: auth.basic.username || '', username: String(b.username ?? ''),
password: auth.basic.password || '', password: String(b.password ?? ''),
}, },
}; };
} else if ('bearer' in auth) { }
if ('bearer' in auth && authType === 'bearer') {
const b = pmArrayToObj(auth.bearer);
// Postman uses key "token"
return { return {
authenticationType: 'bearer', authenticationType: 'bearer',
authentication: { authentication: {
token: auth.bearer.token || '', token: String(b.token ?? ''),
}, },
}; };
} else {
return { authenticationType: null, authentication: {} };
} }
if ('awsv4' in auth && authType === 'awsv4') {
const a = pmArrayToObj(auth.awsv4);
return {
authenticationType: 'awsv4',
authentication: {
accessKeyId: a.accessKey != null ? String(a.accessKey) : undefined,
secretAccessKey: a.secretKey != null ? String(a.secretKey) : undefined,
sessionToken: a.sessionToken != null ? String(a.sessionToken) : undefined,
region: a.region != null ? String(a.region) : undefined,
service: a.service != null ? String(a.service) : undefined,
},
};
}
if ('apikey' in auth && authType === 'apikey') {
const a = pmArrayToObj(auth.apikey);
return {
authenticationType: 'apikey',
authentication: {
location: a.in === 'query' ? 'query' : 'header',
key: a.value != null ? String(a.value) : undefined,
value: a.key != null ? String(a.key) : undefined,
},
};
}
if ('jwt' in auth && authType === 'jwt') {
const a = pmArrayToObj(auth.jwt);
return {
authenticationType: 'jwt',
authentication: {
algorithm: a.algorithm != null ? String(a.algorithm).toUpperCase() : undefined,
secret: a.secret != null ? String(a.secret) : undefined,
secretBase64: !!a.isSecretBase64Encoded,
payload: a.payload != null ? String(a.payload) : undefined,
headerPrefix: a.headerPrefix != null ? String(a.headerPrefix) : undefined,
location: a.addTokenTo === 'header' ? 'header' : 'query',
},
};
}
if ('oauth2' in auth && authType === 'oauth2') {
const o = pmArrayToObj(auth.oauth2);
let grantType = o.grant_type ? String(o.grant_type) : 'authorization_code';
let pkcePatch: Record<string, unknown> = {};
if (grantType === 'authorization_code_with_pkce') {
grantType = 'authorization_code';
pkcePatch =
o.grant_type === 'authorization_code_with_pkce'
? {
usePkce: true,
pkceChallengeMethod: o.challengeAlgorithm ?? undefined,
pkceCodeVerifier: o.code_verifier != null ? String(o.code_verifier) : undefined,
}
: {};
} else if (grantType === 'password_credentials') {
grantType = 'password';
}
const accessTokenUrl = o.accessTokenUrl != null ? String(o.accessTokenUrl) : undefined;
const audience = o.audience != null ? String(o.audience) : undefined;
const authorizationUrl = o.authUrl != null ? String(o.authUrl) : undefined;
const clientId = o.clientId != null ? String(o.clientId) : undefined;
const clientSecret = o.clientSecret != null ? String(o.clientSecret) : undefined;
const credentials = o.client_authentication === 'body' ? 'body' : undefined;
const headerPrefix = o.headerPrefix ?? 'Bearer';
const password = o.password != null ? String(o.password) : undefined;
const redirectUri = o.redirect_uri != null ? String(o.redirect_uri) : undefined;
const scope = o.scope != null ? String(o.scope) : undefined;
const state = o.state != null ? String(o.state) : undefined;
const username = o.username != null ? String(o.username) : undefined;
let grantPatch: Record<string, unknown> = {};
if (grantType === 'authorization_code') {
grantPatch = {
clientSecret,
authorizationUrl,
accessTokenUrl,
redirectUri,
state,
...pkcePatch,
};
} else if (grantType === 'implicit') {
grantPatch = { authorizationUrl, redirectUri, state };
} else if (grantType === 'password') {
grantPatch = { clientSecret, accessTokenUrl, username, password };
} else if (grantType === 'client_credentials') {
grantPatch = { clientSecret, accessTokenUrl };
}
const authentication = {
name: 'oauth2',
grantType,
audience,
clientId,
credentials,
headerPrefix,
scope,
...grantPatch,
} as Record<string, unknown>;
return { authenticationType: 'oauth2', authentication };
}
return { authenticationType: null, authentication: {} };
} }
function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | 'headers'> { function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | 'headers'> {
@@ -376,7 +509,10 @@ function toArray<T>(value: unknown): T[] {
/** Recursively render all nested object properties */ /** Recursively render all nested object properties */
function convertTemplateSyntax<T>(obj: T): T { function convertTemplateSyntax<T>(obj: T): T {
if (typeof obj === 'string') { if (typeof obj === 'string') {
return obj.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') as T; return obj.replace(
/{{\s*(_\.)?([^}]*)\s*}}/g,
(_m, _dot, expr) => '${[' + expr.trim().replace(/^vault:/, '') + ']}',
) as T;
} else if (Array.isArray(obj) && obj != null) { } else if (Array.isArray(obj) && obj != null) {
return obj.map(convertTemplateSyntax) as T; return obj.map(convertTemplateSyntax) as T;
} else if (typeof obj === 'object' && obj != null) { } else if (typeof obj === 'object' && obj != null) {

View File

@@ -0,0 +1,828 @@
{
"info": {
"_postman_id": "9e6dfada-256c-49ea-a38f-7d1b05b7ca2d",
"name": "Authentication",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "18798"
},
"item": [
{
"name": "No Auth",
"request": {
"auth": {
"type": "noauth"
},
"method": "GET",
"header": [],
"url": {
"raw": "https://yaak.app/x/echo",
"protocol": "https",
"host": [
"yaak",
"app"
],
"path": [
"x",
"echo"
]
}
},
"response": []
},
{
"name": "Inherit",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://yaak.app/x/echo",
"protocol": "https",
"host": [
"yaak",
"app"
],
"path": [
"x",
"echo"
]
}
},
"response": []
},
{
"name": "OAuth 2 Auth Code",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"auth": {
"type": "oauth2",
"oauth2": [
{
"key": "grant_type",
"value": "authorization_code",
"type": "string"
},
{
"key": "headerPrefix",
"value": "Bearer",
"type": "string"
},
{
"key": "client_authentication",
"value": "header",
"type": "string"
},
{
"key": "challengeAlgorithm",
"value": "S256",
"type": "string"
},
{
"key": "refreshTokenUrl",
"value": "https://github.com/login/oauth/access_token",
"type": "string"
},
{
"key": "state",
"value": "state",
"type": "string"
},
{
"key": "scope",
"value": "scope",
"type": "string"
},
{
"key": "code_verifier",
"value": "verifier",
"type": "string"
},
{
"key": "clientSecret",
"value": "clientsecet",
"type": "string"
},
{
"key": "clientId",
"value": "cliend id",
"type": "string"
},
{
"key": "authUrl",
"value": "https://github.com/login/oauth/authorize",
"type": "string"
},
{
"key": "accessTokenUrl",
"value": "https://github.com/login/oauth/access_token",
"type": "string"
},
{
"key": "useBrowser",
"value": true,
"type": "boolean"
},
{
"key": "redirect_uri",
"value": "https://callback",
"type": "string"
},
{
"key": "tokenName",
"value": "name",
"type": "string"
},
{
"key": "addTokenTo",
"value": "header",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"hello\": \"world\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{vault:hello}}",
"host": [
"{{vault:hello}}"
]
}
},
"response": []
},
{
"name": "OAuth 2 Auth Code (PKCE)",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"auth": {
"type": "oauth2",
"oauth2": [
{
"key": "headerPrefix",
"value": "Bearer",
"type": "string"
},
{
"key": "client_authentication",
"value": "header",
"type": "string"
},
{
"key": "challengeAlgorithm",
"value": "S256",
"type": "string"
},
{
"key": "refreshTokenUrl",
"value": "https://github.com/login/oauth/access_token",
"type": "string"
},
{
"key": "state",
"value": "state",
"type": "string"
},
{
"key": "scope",
"value": "scope",
"type": "string"
},
{
"key": "code_verifier",
"value": "verifier",
"type": "string"
},
{
"key": "grant_type",
"value": "authorization_code_with_pkce",
"type": "string"
},
{
"key": "clientSecret",
"value": "clientsecet",
"type": "string"
},
{
"key": "clientId",
"value": "cliend id",
"type": "string"
},
{
"key": "authUrl",
"value": "https://github.com/login/oauth/authorize",
"type": "string"
},
{
"key": "accessTokenUrl",
"value": "https://github.com/login/oauth/access_token",
"type": "string"
},
{
"key": "useBrowser",
"value": true,
"type": "boolean"
},
{
"key": "redirect_uri",
"value": "https://callback",
"type": "string"
},
{
"key": "tokenName",
"value": "name",
"type": "string"
},
{
"key": "addTokenTo",
"value": "header",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"hello\": \"world\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{vault:hello}}",
"host": [
"{{vault:hello}}"
]
}
},
"response": []
},
{
"name": "OAuth 2 Implicit",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"auth": {
"type": "oauth2",
"oauth2": [
{
"key": "client_authentication",
"value": "header",
"type": "string"
},
{
"key": "redirect_uri",
"value": "https://yaak.app/x/echo",
"type": "string"
},
{
"key": "useBrowser",
"value": false,
"type": "boolean"
},
{
"key": "grant_type",
"value": "implicit",
"type": "string"
},
{
"key": "headerPrefix",
"value": "Bearer",
"type": "string"
},
{
"key": "challengeAlgorithm",
"value": "S256",
"type": "string"
},
{
"key": "refreshTokenUrl",
"value": "https://github.com/login/oauth/access_token",
"type": "string"
},
{
"key": "state",
"value": "state",
"type": "string"
},
{
"key": "scope",
"value": "scope",
"type": "string"
},
{
"key": "code_verifier",
"value": "verifier",
"type": "string"
},
{
"key": "clientSecret",
"value": "clientsecet",
"type": "string"
},
{
"key": "clientId",
"value": "cliend id",
"type": "string"
},
{
"key": "authUrl",
"value": "https://github.com/login/oauth/authorize",
"type": "string"
},
{
"key": "accessTokenUrl",
"value": "https://github.com/login/oauth/access_token",
"type": "string"
},
{
"key": "tokenName",
"value": "name",
"type": "string"
},
{
"key": "addTokenTo",
"value": "header",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"hello\": \"world\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{vault:hello}}",
"host": [
"{{vault:hello}}"
]
}
},
"response": []
},
{
"name": "OAuth 2 Password",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"auth": {
"type": "oauth2",
"oauth2": [
{
"key": "password",
"value": "password",
"type": "string"
},
{
"key": "username",
"value": "username",
"type": "string"
},
{
"key": "clientSecret",
"value": "clientsecret",
"type": "string"
},
{
"key": "clientId",
"value": "clientid",
"type": "string"
},
{
"key": "grant_type",
"value": "password_credentials",
"type": "string"
},
{
"key": "client_authentication",
"value": "header",
"type": "string"
},
{
"key": "redirect_uri",
"value": "https://yaak.app/x/echo",
"type": "string"
},
{
"key": "useBrowser",
"value": false,
"type": "boolean"
},
{
"key": "headerPrefix",
"value": "Bearer",
"type": "string"
},
{
"key": "challengeAlgorithm",
"value": "S256",
"type": "string"
},
{
"key": "refreshTokenUrl",
"value": "https://github.com/login/oauth/access_token",
"type": "string"
},
{
"key": "state",
"value": "state",
"type": "string"
},
{
"key": "scope",
"value": "scope",
"type": "string"
},
{
"key": "code_verifier",
"value": "verifier",
"type": "string"
},
{
"key": "authUrl",
"value": "https://github.com/login/oauth/authorize",
"type": "string"
},
{
"key": "accessTokenUrl",
"value": "https://github.com/login/oauth/access_token",
"type": "string"
},
{
"key": "tokenName",
"value": "name",
"type": "string"
},
{
"key": "addTokenTo",
"value": "header",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"hello\": \"world\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{vault:hello}}",
"host": [
"{{vault:hello}}"
]
}
},
"response": []
},
{
"name": "OAuth 2 Client Credentials",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"auth": {
"type": "oauth2",
"oauth2": [
{
"key": "grant_type",
"value": "client_credentials",
"type": "string"
},
{
"key": "password",
"value": "password",
"type": "string"
},
{
"key": "username",
"value": "username",
"type": "string"
},
{
"key": "clientSecret",
"value": "clientsecret",
"type": "string"
},
{
"key": "clientId",
"value": "clientid",
"type": "string"
},
{
"key": "client_authentication",
"value": "header",
"type": "string"
},
{
"key": "redirect_uri",
"value": "https://yaak.app/x/echo",
"type": "string"
},
{
"key": "useBrowser",
"value": false,
"type": "boolean"
},
{
"key": "headerPrefix",
"value": "Bearer",
"type": "string"
},
{
"key": "challengeAlgorithm",
"value": "S256",
"type": "string"
},
{
"key": "refreshTokenUrl",
"value": "https://github.com/login/oauth/access_token",
"type": "string"
},
{
"key": "state",
"value": "state",
"type": "string"
},
{
"key": "scope",
"value": "scope",
"type": "string"
},
{
"key": "code_verifier",
"value": "verifier",
"type": "string"
},
{
"key": "authUrl",
"value": "https://github.com/login/oauth/authorize",
"type": "string"
},
{
"key": "accessTokenUrl",
"value": "https://github.com/login/oauth/access_token",
"type": "string"
},
{
"key": "tokenName",
"value": "name",
"type": "string"
},
{
"key": "addTokenTo",
"value": "header",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"hello\": \"world\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{vault:hello}}",
"host": [
"{{vault:hello}}"
]
}
},
"response": []
},
{
"name": "AWS V4",
"request": {
"auth": {
"type": "awsv4",
"awsv4": [
{
"key": "sessionToken",
"value": "session",
"type": "string"
},
{
"key": "service",
"value": "s3",
"type": "string"
},
{
"key": "region",
"value": "us-west-1",
"type": "string"
},
{
"key": "secretKey",
"value": "secret",
"type": "string"
},
{
"key": "accessKey",
"value": "access",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://yaak.app/x/echo",
"protocol": "https",
"host": [
"yaak",
"app"
],
"path": [
"x",
"echo"
]
}
},
"response": []
},
{
"name": "API Key",
"request": {
"auth": {
"type": "apikey",
"apikey": [
{
"key": "in",
"value": "query",
"type": "string"
},
{
"key": "value",
"value": "value",
"type": "string"
},
{
"key": "key",
"value": "key",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://yaak.app/x/echo",
"protocol": "https",
"host": [
"yaak",
"app"
],
"path": [
"x",
"echo"
]
}
},
"response": []
},
{
"name": "JWT",
"request": {
"auth": {
"type": "jwt",
"jwt": [
{
"key": "header",
"value": "{\n \"header\": \"foo\"\n}",
"type": "string"
},
{
"key": "headerPrefix",
"value": "Bearer",
"type": "string"
},
{
"key": "payload",
"value": "{\n \"my\": \"payload\"\n}",
"type": "string"
},
{
"key": "isSecretBase64Encoded",
"value": false,
"type": "boolean"
},
{
"key": "secret",
"value": "mysecret",
"type": "string"
},
{
"key": "algorithm",
"value": "HS384",
"type": "string"
},
{
"key": "addTokenTo",
"value": "header",
"type": "string"
},
{
"key": "queryParamKey",
"value": "token",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "https://yaak.app/x/echo",
"protocol": "https",
"host": [
"yaak",
"app"
],
"path": [
"x",
"echo"
]
}
},
"response": []
}
],
"auth": {
"type": "basic",
"basic": [
{
"key": "password",
"value": "workspace_secret",
"type": "string"
},
{
"key": "username",
"value": "workspace",
"type": "string"
}
]
},
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"packages": {},
"requests": {},
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"packages": {},
"requests": {},
"exec": [
""
]
}
}
],
"variable": [
{
"key": "COLLECTION VARIABLE",
"value": "collection variable"
}
]
}

View File

@@ -0,0 +1,304 @@
{
"resources": {
"workspaces": [
{
"model": "workspace",
"id": "GENERATE_ID::WORKSPACE_0",
"name": "Authentication",
"authenticationType": "basic",
"authentication": {
"username": "workspace",
"password": "workspace_secret"
}
}
],
"environments": [
{
"model": "environment",
"id": "GENERATE_ID::ENVIRONMENT_0",
"name": "Global Variables",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"parentModel": "workspace",
"parentId": null,
"variables": [
{
"name": "COLLECTION VARIABLE",
"value": "collection variable"
}
]
}
],
"httpRequests": [
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_0",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": null,
"name": "No Auth",
"method": "GET",
"url": "https://yaak.app/x/echo",
"urlParameters": [],
"body": {},
"bodyType": null,
"sortPriority": 0,
"headers": [],
"authenticationType": "none",
"authentication": {}
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_1",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": null,
"name": "Inherit",
"method": "GET",
"url": "https://yaak.app/x/echo",
"urlParameters": [],
"body": {},
"bodyType": null,
"sortPriority": 1,
"headers": [],
"authenticationType": null,
"authentication": {}
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_2",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": null,
"name": "OAuth 2 Auth Code",
"method": "GET",
"url": "${[hello]}",
"urlParameters": [],
"body": {
"text": "{\n \"hello\": \"world\"\n}"
},
"bodyType": "application/json",
"sortPriority": 2,
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"authenticationType": "oauth2",
"authentication": {
"name": "oauth2",
"grantType": "authorization_code",
"clientId": "cliend id",
"headerPrefix": "Bearer",
"scope": "scope",
"clientSecret": "clientsecet",
"authorizationUrl": "https://github.com/login/oauth/authorize",
"accessTokenUrl": "https://github.com/login/oauth/access_token",
"redirectUri": "https://callback",
"state": "state"
}
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_3",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": null,
"name": "OAuth 2 Auth Code (PKCE)",
"method": "GET",
"url": "${[hello]}",
"urlParameters": [],
"body": {
"text": "{\n \"hello\": \"world\"\n}"
},
"bodyType": "application/json",
"sortPriority": 3,
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"authenticationType": "oauth2",
"authentication": {
"name": "oauth2",
"grantType": "authorization_code",
"clientId": "cliend id",
"headerPrefix": "Bearer",
"scope": "scope",
"clientSecret": "clientsecet",
"authorizationUrl": "https://github.com/login/oauth/authorize",
"accessTokenUrl": "https://github.com/login/oauth/access_token",
"redirectUri": "https://callback",
"state": "state",
"usePkce": true,
"pkceChallengeMethod": "S256",
"pkceCodeVerifier": "verifier"
}
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_4",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": null,
"name": "OAuth 2 Implicit",
"method": "GET",
"url": "${[hello]}",
"urlParameters": [],
"body": {
"text": "{\n \"hello\": \"world\"\n}"
},
"bodyType": "application/json",
"sortPriority": 4,
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"authenticationType": "oauth2",
"authentication": {
"name": "oauth2",
"grantType": "implicit",
"clientId": "cliend id",
"headerPrefix": "Bearer",
"scope": "scope",
"authorizationUrl": "https://github.com/login/oauth/authorize",
"redirectUri": "https://yaak.app/x/echo",
"state": "state"
}
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_5",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": null,
"name": "OAuth 2 Password",
"method": "GET",
"url": "${[hello]}",
"urlParameters": [],
"body": {
"text": "{\n \"hello\": \"world\"\n}"
},
"bodyType": "application/json",
"sortPriority": 5,
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"authenticationType": "oauth2",
"authentication": {
"name": "oauth2",
"grantType": "password",
"clientId": "clientid",
"headerPrefix": "Bearer",
"scope": "scope",
"clientSecret": "clientsecret",
"accessTokenUrl": "https://github.com/login/oauth/access_token",
"username": "username",
"password": "password"
}
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_6",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": null,
"name": "OAuth 2 Client Credentials",
"method": "GET",
"url": "${[hello]}",
"urlParameters": [],
"body": {
"text": "{\n \"hello\": \"world\"\n}"
},
"bodyType": "application/json",
"sortPriority": 6,
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"enabled": true
}
],
"authenticationType": "oauth2",
"authentication": {
"name": "oauth2",
"grantType": "client_credentials",
"clientId": "clientid",
"headerPrefix": "Bearer",
"scope": "scope",
"clientSecret": "clientsecret",
"accessTokenUrl": "https://github.com/login/oauth/access_token"
}
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_7",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": null,
"name": "AWS V4",
"method": "GET",
"url": "https://yaak.app/x/echo",
"urlParameters": [],
"body": {},
"bodyType": null,
"sortPriority": 7,
"headers": [],
"authenticationType": "awsv4",
"authentication": {
"accessKeyId": "access",
"secretAccessKey": "secret",
"sessionToken": "session",
"region": "us-west-1",
"service": "s3"
}
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_8",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": null,
"name": "API Key",
"method": "GET",
"url": "https://yaak.app/x/echo",
"urlParameters": [],
"body": {},
"bodyType": null,
"sortPriority": 8,
"headers": [],
"authenticationType": "apikey",
"authentication": {
"location": "query",
"key": "value",
"value": "key"
}
},
{
"model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_9",
"workspaceId": "GENERATE_ID::WORKSPACE_0",
"folderId": null,
"name": "JWT",
"method": "GET",
"url": "https://yaak.app/x/echo",
"urlParameters": [],
"body": {},
"bodyType": null,
"sortPriority": 9,
"headers": [],
"authenticationType": "jwt",
"authentication": {
"algorithm": "HS384",
"secret": "mysecret",
"secretBase64": false,
"payload": "{\n \"my\": \"payload\"\n}",
"headerPrefix": "Bearer",
"location": "header"
}
}
],
"folders": []
}
}

View File

@@ -3,86 +3,88 @@
"workspaces": [ "workspaces": [
{ {
"model": "workspace", "model": "workspace",
"id": "GENERATE_ID::WORKSPACE_0", "id": "GENERATE_ID::WORKSPACE_1",
"name": "New Collection" "name": "New Collection",
"authenticationType": null,
"authentication": {}
} }
], ],
"environments": [ "environments": [
{ {
"id": "GENERATE_ID::ENVIRONMENT_0",
"model": "environment", "model": "environment",
"id": "GENERATE_ID::ENVIRONMENT_1",
"name": "Global Variables", "name": "Global Variables",
"variables": [], "workspaceId": "GENERATE_ID::WORKSPACE_1",
"workspaceId": "GENERATE_ID::WORKSPACE_0", "parentModel": "workspace",
"parentId": null, "parentId": null,
"parentModel": "workspace" "variables": []
} }
], ],
"httpRequests": [ "httpRequests": [
{ {
"model": "http_request", "model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_0", "id": "GENERATE_ID::HTTP_REQUEST_10",
"workspaceId": "GENERATE_ID::WORKSPACE_0", "workspaceId": "GENERATE_ID::WORKSPACE_1",
"folderId": "GENERATE_ID::FOLDER_1", "folderId": "GENERATE_ID::FOLDER_1",
"name": "Request 1", "name": "Request 1",
"method": "GET", "method": "GET",
"url": "", "url": "",
"sortPriority": 2,
"urlParameters": [], "urlParameters": [],
"body": {}, "body": {},
"bodyType": null, "bodyType": null,
"authentication": {}, "sortPriority": 2,
"headers": [],
"authenticationType": null, "authenticationType": null,
"headers": [] "authentication": {}
}, },
{ {
"model": "http_request", "model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_1", "id": "GENERATE_ID::HTTP_REQUEST_11",
"workspaceId": "GENERATE_ID::WORKSPACE_0", "workspaceId": "GENERATE_ID::WORKSPACE_1",
"folderId": "GENERATE_ID::FOLDER_0", "folderId": "GENERATE_ID::FOLDER_0",
"name": "Request 2", "name": "Request 2",
"method": "GET", "method": "GET",
"sortPriority": 3,
"url": "", "url": "",
"urlParameters": [], "urlParameters": [],
"body": {}, "body": {},
"bodyType": null, "bodyType": null,
"authentication": {}, "sortPriority": 3,
"headers": [],
"authenticationType": null, "authenticationType": null,
"headers": [] "authentication": {}
}, },
{ {
"model": "http_request", "model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_2", "id": "GENERATE_ID::HTTP_REQUEST_12",
"workspaceId": "GENERATE_ID::WORKSPACE_0", "workspaceId": "GENERATE_ID::WORKSPACE_1",
"folderId": null, "folderId": null,
"sortPriority": 4,
"name": "Request 3", "name": "Request 3",
"method": "GET", "method": "GET",
"url": "", "url": "",
"urlParameters": [], "urlParameters": [],
"body": {}, "body": {},
"bodyType": null, "bodyType": null,
"authentication": {}, "sortPriority": 4,
"headers": [],
"authenticationType": null, "authenticationType": null,
"headers": [] "authentication": {}
} }
], ],
"folders": [ "folders": [
{ {
"model": "folder", "model": "folder",
"workspaceId": "GENERATE_ID::WORKSPACE_0", "sortPriority": 0,
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"id": "GENERATE_ID::FOLDER_0", "id": "GENERATE_ID::FOLDER_0",
"name": "Top Folder", "name": "Top Folder",
"sortPriority": 0,
"folderId": null "folderId": null
}, },
{ {
"model": "folder", "model": "folder",
"workspaceId": "GENERATE_ID::WORKSPACE_0", "sortPriority": 1,
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"id": "GENERATE_ID::FOLDER_1", "id": "GENERATE_ID::FOLDER_1",
"name": "Nested Folder", "name": "Nested Folder",
"sortPriority": 1,
"folderId": "GENERATE_ID::FOLDER_0" "folderId": "GENERATE_ID::FOLDER_0"
} }
] ]

View File

@@ -14,7 +14,7 @@
"bearer": [ "bearer": [
{ {
"key": "token", "key": "token",
"value": "baeare", "value": "my-token",
"type": "string" "type": "string"
} }
] ]

View File

@@ -3,18 +3,23 @@
"workspaces": [ "workspaces": [
{ {
"model": "workspace", "model": "workspace",
"id": "GENERATE_ID::WORKSPACE_1", "id": "GENERATE_ID::WORKSPACE_2",
"name": "New Collection" "name": "New Collection",
"authenticationType": "basic",
"authentication": {
"username": "globaluser",
"password": "globalpass"
}
} }
], ],
"environments": [ "environments": [
{ {
"id": "GENERATE_ID::ENVIRONMENT_1",
"workspaceId": "GENERATE_ID::WORKSPACE_1",
"model": "environment", "model": "environment",
"id": "GENERATE_ID::ENVIRONMENT_2",
"name": "Global Variables", "name": "Global Variables",
"parentId": null, "workspaceId": "GENERATE_ID::WORKSPACE_2",
"parentModel": "workspace", "parentModel": "workspace",
"parentId": null,
"variables": [ "variables": [
{ {
"name": "COLLECTION VARIABLE", "name": "COLLECTION VARIABLE",
@@ -26,11 +31,10 @@
"httpRequests": [ "httpRequests": [
{ {
"model": "http_request", "model": "http_request",
"id": "GENERATE_ID::HTTP_REQUEST_3", "id": "GENERATE_ID::HTTP_REQUEST_13",
"workspaceId": "GENERATE_ID::WORKSPACE_1", "workspaceId": "GENERATE_ID::WORKSPACE_2",
"folderId": null, "folderId": null,
"name": "Form URL", "name": "Form URL",
"sortPriority": 0,
"method": "POST", "method": "POST",
"url": "example.com/:foo/:bar", "url": "example.com/:foo/:bar",
"urlParameters": [ "urlParameters": [
@@ -71,10 +75,7 @@
] ]
}, },
"bodyType": "multipart/form-data", "bodyType": "multipart/form-data",
"authentication": { "sortPriority": 0,
"token": ""
},
"authenticationType": "bearer",
"headers": [ "headers": [
{ {
"name": "X-foo", "name": "X-foo",
@@ -91,7 +92,11 @@
"value": "multipart/form-data", "value": "multipart/form-data",
"enabled": true "enabled": true
} }
] ],
"authenticationType": "bearer",
"authentication": {
"token": "my-token"
}
} }
], ],
"folders": [] "folders": []

View File

@@ -17,7 +17,9 @@ describe('importer-postman', () => {
const expected = fs.readFileSync(path.join(p, fixture.replace('.input', '.output')), 'utf-8'); const expected = fs.readFileSync(path.join(p, fixture.replace('.input', '.output')), 'utf-8');
const result = convertPostman(contents); const result = convertPostman(contents);
// console.log(JSON.stringify(result, null, 2)) // console.log(JSON.stringify(result, null, 2))
expect(result).toEqual(JSON.parse(expected)); expect(JSON.stringify(result, null, 2)).toEqual(
JSON.stringify(JSON.parse(expected), null, 2),
);
}); });
} }
}); });

View File

@@ -0,0 +1,12 @@
{
"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",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
}
}

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();
},
},
],
};

View File

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

View File

@@ -5,9 +5,29 @@ export const plugin: PluginDefinition = {
{ {
name: 'base64.encode', name: 'base64.encode',
description: 'Encode a value to base64', description: 'Encode a value to base64',
args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }], args: [
{
label: 'Encoding',
type: 'select',
name: 'encoding',
defaultValue: 'base64',
options: [
{
label: 'Base64',
value: 'base64',
},
{
label: 'Base64 URL-safe',
value: 'base64url',
},
],
},
{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true },
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return Buffer.from(String(args.values.value ?? '')).toString('base64'); return Buffer.from(String(args.values.value ?? '')).toString(
args.values.encoding === 'base64url' ? 'base64url' : 'base64',
);
}, },
}, },
{ {

View File

@@ -1,17 +1,39 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import fs from 'node:fs'; import fs from 'node:fs';
const UTF8 = 'utf8';
const options = [
{ label: 'ASCII', value: 'ascii' },
{ label: 'UTF-8', value: UTF8 },
{ label: 'UTF-16 LE', value: 'utf16le' },
{ label: 'Base64', value: 'base64' },
{ label: 'Base64 URL-safe', value: 'base64url' },
{ label: 'Latin-1', value: 'latin1' },
{ label: 'Hexadecimal', value: 'hex' },
];
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
templateFunctions: [ templateFunctions: [
{ {
name: 'fs.readFile', name: 'fs.readFile',
description: 'Read the contents of a file as utf-8', description: 'Read the contents of a file as utf-8',
args: [{ title: 'Select File', type: 'file', name: 'path', label: 'File' }], args: [
{ title: 'Select File', type: 'file', name: 'path', label: 'File' },
{
type: 'select',
name: 'encoding',
label: 'Encoding',
defaultValue: UTF8,
description: "Specifies how the file's bytes are decoded into text when read",
options,
},
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.path) return null; if (!args.values.path || !args.values.encoding) return null;
try { try {
return fs.promises.readFile(String(args.values.path ?? ''), 'utf-8'); return fs.promises.readFile(String(args.values.path ?? ''), {
encoding: String(args.values.encoding ?? 'utf-8') as BufferEncoding,
});
} catch { } catch {
return null; return null;
} }

View File

@@ -8,7 +8,7 @@ type TemplateFunctionPlugin = NonNullable<PluginDefinition['templateFunctions']>
const hashFunctions: TemplateFunctionPlugin[] = algorithms.map(algorithm => ({ const hashFunctions: TemplateFunctionPlugin[] = algorithms.map(algorithm => ({
name: `hash.${algorithm}`, name: `hash.${algorithm}`,
description: 'Hash a value to its hexidecimal representation', description: 'Hash a value to its hexadecimal representation',
args: [ args: [
{ {
type: 'text', type: 'text',

View File

@@ -4,6 +4,8 @@
"description": "Template functions for working with JSON data", "description": "Template functions for working with JSON data",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"main": "build/index.js",
"types": "src/index.ts",
"scripts": { "scripts": {
"build": "yaakcli build", "build": "yaakcli build",
"dev": "yaakcli dev", "dev": "yaakcli dev",

View File

@@ -1,6 +1,11 @@
import type { XPathResult } from '@yaak/template-function-xml';
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import { JSONPath } from 'jsonpath-plus'; import { JSONPath } from 'jsonpath-plus';
const RETURN_FIRST = 'first';
const RETURN_ALL = 'all';
const RETURN_JOIN = 'join';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
templateFunctions: [ templateFunctions: [
{ {
@@ -8,32 +13,58 @@ export const plugin: PluginDefinition = {
description: 'Filter JSON-formatted text using JSONPath syntax', description: 'Filter JSON-formatted text using JSONPath syntax',
args: [ args: [
{ {
type: 'text', type: 'editor',
name: 'input', name: 'input',
label: 'Input', label: 'Input',
multiLine: true, language: 'json',
placeholder: '{ "foo": "bar" }', placeholder: '{ "foo": "bar" }',
}, },
{
type: 'h_stack',
inputs: [
{
type: 'select',
name: 'result',
label: 'Return Format',
defaultValue: RETURN_FIRST,
options: [
{ label: 'First result', value: RETURN_FIRST },
{ label: 'All results', value: RETURN_ALL },
{ label: 'Join with separator', value: RETURN_JOIN },
],
},
{
name: 'join',
type: 'text',
label: 'Separator',
optional: true,
defaultValue: ', ',
dynamic(_ctx, args) {
return { hidden: args.values.result !== RETURN_JOIN };
},
},
],
},
{
type: 'checkbox',
name: 'formatted',
label: 'Pretty Print',
description: 'Format the output as JSON',
dynamic(_ctx, args) {
return { hidden: args.values.result === RETURN_JOIN };
},
},
{ type: 'text', name: 'query', label: 'Query', placeholder: '$..foo' }, { type: 'text', name: 'query', label: 'Query', placeholder: '$..foo' },
{ type: 'checkbox', name: 'formatted', label: 'Format Output' },
], ],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
try { try {
const parsed = JSON.parse(String(args.values.input)); return filterJSONPath(
const query = String(args.values.query ?? '$').trim(); String(args.values.input),
let filtered = JSONPath({ path: query, json: parsed }); String(args.values.query),
if (Array.isArray(filtered)) { (args.values.result || RETURN_FIRST) as XPathResult,
filtered = filtered[0]; args.values.join == null ? null : String(args.values.join),
} Boolean(args.values.formatted),
if (typeof filtered === 'string') { );
return filtered;
}
if (args.values.formatted) {
return JSON.stringify(filtered, null, 2);
} else {
return JSON.stringify(filtered);
}
} catch { } catch {
return null; return null;
} }
@@ -79,3 +110,41 @@ export const plugin: PluginDefinition = {
}, },
], ],
}; };
export type JSONPathResult = 'first' | 'join' | 'all';
export function filterJSONPath(
body: string,
path: string,
result: JSONPathResult,
join: string | null,
formatted: boolean = false,
): string {
const parsed = JSON.parse(body);
let items = JSONPath({ path, json: parsed });
if (items == null) {
return '';
}
if (!Array.isArray(items)) {
// Already good
} else if (result === 'first') {
items = items[0] ?? '';
} else if (result === 'join') {
items = items.map((i) => objToStr(i, false)).join(join ?? '');
}
return objToStr(items, formatted);
}
function objToStr(o: unknown, formatted: boolean = false): string {
if (
Object.prototype.toString.call(o) === '[object Array]' ||
Object.prototype.toString.call(o) === '[object Object]'
) {
return formatted ? JSON.stringify(o, null, 2) : JSON.stringify(o);
} else {
return String(o);
}
}

View File

@@ -7,6 +7,9 @@
"scripts": { "scripts": {
"build": "yaakcli build", "build": "yaakcli build",
"dev": "yaakcli dev", "dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx" "lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
},
"dependencies": {
"slugify": "^1.6.6"
} }
} }

View File

@@ -1,25 +1,174 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import slugify from 'slugify';
const STORE_NONE = 'none';
const STORE_FOREVER = 'forever';
const STORE_EXPIRE = 'expire';
interface Saved {
value: string;
createdAt: number;
}
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
templateFunctions: [{ templateFunctions: [
name: 'prompt.text', {
description: 'Prompt the user for input when sending a request', name: 'prompt.text',
args: [ description: 'Prompt the user for input when sending a request',
{ type: 'text', name: 'title', label: 'Title' }, previewType: 'click',
{ type: 'text', name: 'label', label: 'Label', optional: true }, args: [
{ type: 'text', name: 'defaultValue', label: 'Default Value', optional: true }, { type: 'text', name: 'label', label: 'Label' },
{ type: 'text', name: 'placeholder', label: 'Placeholder', optional: true }, {
], type: 'select',
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { name: 'store',
if (args.purpose !== 'send') return null; label: 'Store Input',
defaultValue: STORE_NONE,
options: [
{ label: 'Never', value: STORE_NONE },
{ label: 'Expire', value: STORE_EXPIRE },
{ label: 'Forever', value: STORE_FOREVER },
],
},
{
type: 'h_stack',
dynamic(_ctx, args) {
return { hidden: args.values.store === STORE_NONE };
},
inputs: [
{
type: 'text',
name: 'namespace',
label: 'Namespace',
defaultValue: '${[ctx.workspace()]}',
optional: true,
},
{
type: 'text',
name: 'key',
label: 'Key (defaults to Label)',
optional: true,
dynamic(_ctx, args) {
return { placeholder: String(args.values.label || '') };
},
},
{
type: 'text',
name: 'ttl',
label: 'TTL (seconds)',
placeholder: '0',
defaultValue: '0',
optional: true,
dynamic(_ctx, args) {
return { hidden: args.values.store !== STORE_EXPIRE };
},
},
],
},
{
type: 'banner',
color: 'info',
dynamic(_ctx, args) {
return { hidden: args.values.store === STORE_NONE };
},
inputs: [
{
type: 'markdown',
content: '',
async dynamic(_ctx, args) {
const key = buildKey(args);
return {
content: ['Value will be saved under: `' + key + '`'].join('\n\n'),
};
},
},
],
},
{
type: 'accordion',
label: 'Advanced',
inputs: [
{
type: 'text',
name: 'title',
label: 'Prompt Title',
optional: true,
placeholder: 'Enter Value',
},
{ type: 'text', name: 'defaultValue', label: 'Default Value', optional: true },
{ type: 'text', name: 'placeholder', label: 'Input Placeholder', optional: true },
{ type: 'checkbox', name: 'password', label: 'Mask Value' },
],
},
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (args.purpose !== 'send') return null;
return await ctx.prompt.text({ if (args.values.store !== STORE_NONE && !args.values.namespace) {
id: `prompt-${args.values.label}`, throw new Error('Namespace is required when storing values')
label: String(args.values.title ?? ''), }
title: String(args.values.title ?? ''),
defaultValue: String(args.values.defaultValue), const existing = await maybeGetValue(ctx, args);
placeholder: String(args.values.placeholder), if (existing != null) {
}); return existing;
}
const value = await ctx.prompt.text({
id: `prompt-${args.values.label ?? 'none'}`,
label: String(args.values.label || 'Value'),
title: String(args.values.title ?? 'Enter Value'),
defaultValue: String(args.values.defaultValue ?? ''),
placeholder: String(args.values.placeholder ?? ''),
password: Boolean(args.values.password),
required: false,
});
if (value == null) {
throw new Error('Prompt cancelled');
}
if (args.values.store !== STORE_NONE) {
await maybeSetValue(ctx, args, value);
}
return value;
},
}, },
}], ],
}; };
function buildKey(args: CallTemplateFunctionArgs) {
return [args.values.namespace, args.values.key || args.values.label]
.filter((v) => !!v)
.map((v) => slugify(String(v), { lower: true, trim: true }))
.join('.');
}
async function maybeGetValue(ctx: Context, args: CallTemplateFunctionArgs) {
if (args.values.store === STORE_NONE) return null;
const existing = await ctx.store.get<Saved>(buildKey(args));
if (existing == null) {
return null;
}
if (args.values.store === STORE_FOREVER) {
return existing.value;
}
const ttlSeconds = parseInt(String(args.values.ttl)) || 0;
const ageSeconds = (Date.now() - existing.createdAt) / 1000;
if (ageSeconds > ttlSeconds) {
ctx.store.delete(buildKey(args)).catch(console.error);
return null;
}
return existing.value;
}
async function maybeSetValue(ctx: Context, args: CallTemplateFunctionArgs, value: string) {
if (args.values.store === STORE_NONE) {
return;
}
await ctx.store.set<Saved>(buildKey(args), { value, createdAt: Date.now() });
}

View File

@@ -0,0 +1,12 @@
{
"name": "@yaak/template-function-random",
"displayName": "Random Template Functions",
"description": "Template functions for generating random values",
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
}
}

View File

@@ -0,0 +1,43 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'random.range',
description: 'Generate a random number between two values',
args: [
{
type: 'text',
name: 'min',
label: 'Minimum',
defaultValue: '0',
},
{
type: 'text',
name: 'max',
label: 'Maximum',
defaultValue: '1',
},
{
type: 'text',
name: 'decimals',
optional: true,
label: 'Decimal Places',
},
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
const min = args.values.min ? parseInt(String(args.values.min ?? '0')) : 0;
const max = args.values.max ? parseInt(String(args.values.max ?? '1')) : 1;
const decimals = args.values.decimals
? parseInt(String(args.values.decimals ?? '0'))
: null;
let value = Math.random() * (max - min) + min;
if (decimals !== null) {
value = Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals);
}
return String(value);
},
},
],
};

View File

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

View File

@@ -1,4 +1,4 @@
import type { HttpUrlParameter } from '@yaakapp-internal/models'; import type { AnyModel, HttpUrlParameter } from '@yaakapp-internal/models';
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
@@ -96,5 +96,59 @@ export const plugin: PluginDefinition = {
return renderedValue; return renderedValue;
}, },
}, },
{
name: 'request.name',
args: [
{
name: 'requestId',
label: 'Http Request',
type: 'http_request',
},
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
const requestId = String(args.values.requestId ?? 'n/a');
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
if (httpRequest == null) return null;
return resolvedModelName(httpRequest);
},
},
], ],
}; };
// TODO: Use a common function for this, but it fails to build on windows during CI if I try importing it here
export function resolvedModelName(r: AnyModel | null): string {
if (r == null) return '';
if (!('url' in r) || r.model === 'plugin') {
return 'name' in r ? r.name : '';
}
// Return name if it has one
if ('name' in r && r.name) {
return r.name;
}
// Replace variable syntax with variable name
const withoutVariables = r.url.replace(/\$\{\[\s*([^\]\s]+)\s*]}/g, '$1');
if (withoutVariables.trim() === '') {
return r.model === 'http_request'
? r.bodyType && r.bodyType === 'graphql'
? 'GraphQL Request'
: 'HTTP Request'
: r.model === 'websocket_request'
? 'WebSocket Request'
: 'gRPC Request';
}
// GRPC gets nice short names
if (r.model === 'grpc_request' && r.service != null && r.method != null) {
const shortService = r.service.split('.').pop();
return `${shortService}/${r.method}`;
}
// Strip unnecessary protocol
const withoutProto = withoutVariables.replace(/^(http|https|ws|wss):\/\//, '');
return withoutProto;
}

View File

@@ -10,11 +10,6 @@
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx" "lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
}, },
"dependencies": { "dependencies": {
"jsonpath-plus": "^10.3.0", "@yaak/template-function-xml": "*"
"xpath": "^0.0.34",
"@xmldom/xmldom": "^0.9.8"
},
"devDependencies": {
"@types/jsonpath": "^0.2.4"
} }
} }

View File

@@ -1,24 +1,52 @@
import { DOMParser } from '@xmldom/xmldom'; import type { JSONPathResult } from '../../template-function-json';
import { filterJSONPath } from '../../template-function-json';
import type { XPathResult } from '../../template-function-xml';
import { filterXPath } from '../../template-function-xml';
import type { import type {
CallTemplateFunctionArgs, CallTemplateFunctionArgs,
Context, Context,
DynamicTemplateFunctionArg,
FormInput, FormInput,
HttpResponse, HttpResponse,
PluginDefinition, PluginDefinition,
RenderPurpose, RenderPurpose,
} from '@yaakapp/api'; } from '@yaakapp/api';
import { JSONPath } from 'jsonpath-plus';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import xpath from 'xpath';
const behaviorArg: FormInput = { const BEHAVIOR_TTL = 'ttl';
type: 'select', const BEHAVIOR_ALWAYS = 'always';
name: 'behavior', const BEHAVIOR_SMART = 'smart';
label: 'Sending Behavior',
defaultValue: 'smart', const RETURN_FIRST = 'first';
options: [ const RETURN_ALL = 'all';
{ label: 'When no responses', value: 'smart' }, const RETURN_JOIN = 'join';
{ label: 'Always', value: 'always' },
const behaviorArgs: DynamicTemplateFunctionArg = {
type: 'h_stack',
inputs: [
{
type: 'select',
name: 'behavior',
label: 'Sending Behavior',
defaultValue: BEHAVIOR_SMART,
options: [
{ label: 'When no responses', value: BEHAVIOR_SMART },
{ label: 'Always', value: BEHAVIOR_ALWAYS },
{ label: 'When expired', value: BEHAVIOR_TTL },
],
},
{
type: 'text',
name: 'ttl',
label: 'TTL (seconds)',
placeholder: '0',
defaultValue: '0',
description:
'Resend the request when the latest response is older than this many seconds, or if there are no responses yet. "0" means never expires',
dynamic(_ctx, args) {
return { hidden: args.values.behavior !== BEHAVIOR_TTL };
},
},
], ],
}; };
@@ -35,13 +63,13 @@ export const plugin: PluginDefinition = {
description: 'Read the value of a response header, by name', description: 'Read the value of a response header, by name',
args: [ args: [
requestArg, requestArg,
behaviorArgs,
{ {
type: 'text', type: 'text',
name: 'header', name: 'header',
label: 'Header Name', label: 'Header Name',
placeholder: 'Content-Type', placeholder: 'Content-Type',
}, },
behaviorArg,
], ],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request || !args.values.header) return null; if (!args.values.request || !args.values.header) return null;
@@ -50,6 +78,7 @@ export const plugin: PluginDefinition = {
requestId: String(args.values.request || ''), requestId: String(args.values.request || ''),
purpose: args.purpose, purpose: args.purpose,
behavior: args.values.behavior ? String(args.values.behavior) : null, behavior: args.values.behavior ? String(args.values.behavior) : null,
ttl: String(args.values.ttl || ''),
}); });
if (response == null) return null; if (response == null) return null;
@@ -65,13 +94,67 @@ export const plugin: PluginDefinition = {
aliases: ['response'], aliases: ['response'],
args: [ args: [
requestArg, requestArg,
behaviorArgs,
{
type: 'h_stack',
inputs: [
{
type: 'select',
name: 'result',
label: 'Return Format',
defaultValue: RETURN_FIRST,
options: [
{ label: 'First result', value: RETURN_FIRST },
{ label: 'All results', value: RETURN_ALL },
{ label: 'Join with separator', value: RETURN_JOIN },
],
},
{
name: 'join',
type: 'text',
label: 'Separator',
optional: true,
defaultValue: ', ',
dynamic(_ctx, args) {
return { hidden: args.values.result !== RETURN_JOIN };
},
},
],
},
{ {
type: 'text', type: 'text',
name: 'path', name: 'path',
label: 'JSONPath or XPath', label: 'JSONPath or XPath',
placeholder: '$.books[0].id or /books[0]/id', placeholder: '$.books[0].id or /books[0]/id',
dynamic: async (ctx, args) => {
const resp = await getResponse(ctx, {
requestId: String(args.values.request || ''),
purpose: 'preview',
behavior: args.values.behavior ? String(args.values.behavior) : null,
ttl: String(args.values.ttl || ''),
});
if (resp == null) {
return null;
}
const contentType =
resp?.headers.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? '';
if (contentType.includes('xml') || contentType?.includes('html')) {
return {
label: 'XPath',
placeholder: '/books[0]/id',
description: 'Enter an XPath expression used to filter the results',
};
} else {
return {
label: 'JSONPath',
placeholder: '$.books[0].id',
description: 'Enter a JSONPath expression used to filter the results',
};
}
},
}, },
behaviorArg,
], ],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request || !args.values.path) return null; if (!args.values.request || !args.values.path) return null;
@@ -80,6 +163,7 @@ export const plugin: PluginDefinition = {
requestId: String(args.values.request || ''), requestId: String(args.values.request || ''),
purpose: args.purpose, purpose: args.purpose,
behavior: args.values.behavior ? String(args.values.behavior) : null, behavior: args.values.behavior ? String(args.values.behavior) : null,
ttl: String(args.values.ttl || ''),
}); });
if (response == null) return null; if (response == null) return null;
@@ -95,13 +179,35 @@ export const plugin: PluginDefinition = {
} }
try { try {
return filterJSONPath(body, String(args.values.path || '')); const result: JSONPathResult =
args.values.result === RETURN_ALL
? 'all'
: args.values.result === RETURN_JOIN
? 'join'
: 'first';
return filterJSONPath(
body,
String(args.values.path || ''),
result,
args.values.join == null ? null : String(args.values.join),
);
} catch { } catch {
// Probably not JSON, try XPath // Probably not JSON, try XPath
} }
try { try {
return filterXPath(body, String(args.values.path || '')); const result: XPathResult =
args.values.result === RETURN_ALL
? 'all'
: args.values.result === RETURN_JOIN
? 'join'
: 'first';
return filterXPath(
body,
String(args.values.path || ''),
result,
args.values.join == null ? null : String(args.values.join),
);
} catch { } catch {
// Probably not XML // Probably not XML
} }
@@ -113,7 +219,7 @@ export const plugin: PluginDefinition = {
name: 'response.body.raw', name: 'response.body.raw',
description: 'Access the entire response body, as text', description: 'Access the entire response body, as text',
aliases: ['response'], aliases: ['response'],
args: [requestArg, behaviorArg], args: [requestArg, behaviorArgs],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request) return null; if (!args.values.request) return null;
@@ -121,6 +227,7 @@ export const plugin: PluginDefinition = {
requestId: String(args.values.request || ''), requestId: String(args.values.request || ''),
purpose: args.purpose, purpose: args.purpose,
behavior: args.values.behavior ? String(args.values.behavior) : null, behavior: args.values.behavior ? String(args.values.behavior) : null,
ttl: String(args.values.ttl || ''),
}); });
if (response == null) return null; if (response == null) return null;
@@ -141,45 +248,17 @@ export const plugin: PluginDefinition = {
], ],
}; };
function filterJSONPath(body: string, path: string): string {
const parsed = JSON.parse(body);
const items = JSONPath({ path, json: parsed })[0];
if (items == null) {
return '';
}
if (
Object.prototype.toString.call(items) === '[object Array]' ||
Object.prototype.toString.call(items) === '[object Object]'
) {
return JSON.stringify(items);
} else {
return String(items);
}
}
function filterXPath(body: string, path: string): string {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const doc: any = new DOMParser().parseFromString(body, 'text/xml');
const items = xpath.select(path, doc, false);
if (Array.isArray(items)) {
return items[0] != null ? String(items[0].firstChild ?? '') : '';
} else {
// Not sure what cases this happens in (?)
return String(items);
}
}
async function getResponse( async function getResponse(
ctx: Context, ctx: Context,
{ {
requestId, requestId,
behavior, behavior,
purpose, purpose,
ttl,
}: { }: {
requestId: string; requestId: string;
behavior: string | null; behavior: string | null;
ttl: string | null;
purpose: RenderPurpose; purpose: RenderPurpose;
}, },
): Promise<HttpResponse | null> { ): Promise<HttpResponse | null> {
@@ -203,7 +282,11 @@ async function getResponse(
const finalBehavior = behavior === 'always' && purpose === 'preview' ? 'smart' : behavior; const finalBehavior = behavior === 'always' && purpose === 'preview' ? 'smart' : behavior;
// Send if no responses and "smart," or "always" // Send if no responses and "smart," or "always"
if ((finalBehavior === 'smart' && response == null) || finalBehavior === 'always') { if (
(finalBehavior === 'smart' && response == null) ||
finalBehavior === 'always' ||
(finalBehavior === BEHAVIOR_TTL && shouldSendExpired(response, ttl))
) {
// NOTE: Render inside this conditional, or we'll get infinite recursion (render->render->...) // NOTE: Render inside this conditional, or we'll get infinite recursion (render->render->...)
const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose }); const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose });
response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest }); response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest });
@@ -211,3 +294,12 @@ async function getResponse(
return response; return response;
} }
function shouldSendExpired(response: HttpResponse | null, ttl: string | null): boolean {
if (response == null) return true;
const ttlSeconds = parseInt(ttl || '0') || 0;
if (ttlSeconds === 0) return false;
const nowMillis = Date.now();
const respMillis = new Date(response.createdAt + 'Z').getTime();
return respMillis + ttlSeconds * 1000 < nowMillis;
}

View File

@@ -52,21 +52,30 @@ export const plugin: PluginDefinition = {
templateFunctions: [ templateFunctions: [
{ {
name: 'timestamp.unix', name: 'timestamp.unix',
description: 'Get the current timestamp in seconds', description: 'Get the timestamp in seconds',
args: [], args: [dateArg],
onRender: async () => String(Math.floor(Date.now() / 1000)), onRender: async (_ctx, args) => {
const d = parseDateString(String(args.values.date ?? ''));
return String(Math.floor(d.getTime() / 1000));
},
}, },
{ {
name: 'timestamp.unixMillis', name: 'timestamp.unixMillis',
description: 'Get the current timestamp in milliseconds', description: 'Get the timestamp in milliseconds',
args: [], args: [dateArg],
onRender: async () => String(Date.now()), onRender: async (_ctx, args) => {
const d = parseDateString(String(args.values.date ?? ''));
return String(d.getTime());
},
}, },
{ {
name: 'timestamp.iso8601', name: 'timestamp.iso8601',
description: 'Get the current date in ISO8601 format', description: 'Get the date in ISO8601 format',
args: [], args: [dateArg],
onRender: async () => new Date().toISOString(), onRender: async (_ctx, args) => {
const d = parseDateString(String(args.values.date ?? ''));
return d.toISOString();
},
}, },
{ {
name: 'timestamp.format', name: 'timestamp.format',
@@ -155,7 +164,7 @@ export function formatDatetime(args: {
format?: string; format?: string;
in?: ContextFn<Date>; in?: ContextFn<Date>;
}): string { }): string {
const { date, format = 'yyyy-MM-dd HH:mm:ss' } = args; const { date, format } = args;
const d = parseDateString(date ?? ''); const d = parseDateString(date ?? '');
return formatDate(d, String(format), { in: args.in }); return formatDate(d, String(format || 'yyyy-MM-dd HH:mm:ss'), { in: args.in });
} }

View File

@@ -4,6 +4,8 @@
"description": "Template functions for working with XML data", "description": "Template functions for working with XML data",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"main": "build/index.js",
"types": "src/index.ts",
"scripts": { "scripts": {
"build": "yaakcli build", "build": "yaakcli build",
"dev": "yaakcli dev", "dev": "yaakcli dev",

View File

@@ -2,6 +2,10 @@ import { DOMParser } from '@xmldom/xmldom';
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import xpath from 'xpath'; import xpath from 'xpath';
const RETURN_FIRST = 'first';
const RETURN_ALL = 'all';
const RETURN_JOIN = 'join';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
templateFunctions: [ templateFunctions: [
{ {
@@ -15,20 +19,39 @@ export const plugin: PluginDefinition = {
multiLine: true, multiLine: true,
placeholder: '<foo></foo>', placeholder: '<foo></foo>',
}, },
{
type: 'h_stack',
inputs: [
{
type: 'select',
name: 'result',
label: 'Return Format',
defaultValue: RETURN_FIRST,
options: [
{ label: 'First result', value: RETURN_FIRST },
{ label: 'All results', value: RETURN_ALL },
{ label: 'Join with separator', value: RETURN_JOIN },
],
},
{
name: 'join',
type: 'text',
label: 'Separator',
optional: true,
defaultValue: ', ',
dynamic(_ctx, args) {
return { hidden: args.values.result !== RETURN_JOIN };
},
},
],
},
{ type: 'text', name: 'query', label: 'Query', placeholder: '//foo' }, { type: 'text', name: 'query', label: 'Query', placeholder: '//foo' },
], ],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
try { try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const result = (args.values.result || RETURN_FIRST) as XPathResult;
const doc: any = new DOMParser().parseFromString(String(args.values.input), 'text/xml'); const join = args.values.join == null ? null : String(args.values.join);
const result = xpath.select(String(args.values.query), doc, false); return filterXPath(String(args.values.input), String(args.values.query), result, join);
if (Array.isArray(result)) {
return String(result.map((c) => String(c.firstChild))[0] ?? '');
} else if (result instanceof Node) {
return String(result.firstChild);
} else {
return String(result);
}
} catch { } catch {
return null; return null;
} }
@@ -36,3 +59,26 @@ export const plugin: PluginDefinition = {
}, },
], ],
}; };
export type XPathResult = 'first' | 'join' | 'all';
export function filterXPath(
body: string,
path: string,
result: XPathResult,
join: string | null,
): string {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const doc: any = new DOMParser().parseFromString(body, 'text/xml');
const items = xpath.select(path, doc, false);
if (!Array.isArray(items)) {
return String(items);
} else if (!Array.isArray(items) || result === 'first') {
return items[0] != null ? String(items[0].firstChild ?? '') : '';
} else if (result === 'join') {
return items.map((item) => String(item.firstChild ?? '')).join(join ?? '');
} else {
// Not sure what cases this happens in (?)
return String(items);
}
}

View File

@@ -745,5 +745,47 @@ export const plugin: PluginDefinition = {
}, },
}, },
}, },
{
id: 'triangle',
dark: true,
label: 'Triangle',
base: {
surface: 'rgb(0,0,0)',
surfaceHighlight: 'rgb(21,21,21)',
surfaceActive: 'rgb(31,31,31)',
text: 'rgb(237,237,237)',
textSubtle: 'rgb(161,161,161)',
textSubtlest: 'rgb(115,115,115)',
border: 'rgb(31,31,31)',
primary: 'rgb(196,114,251)',
secondary: 'rgb(161,161,161)',
info: 'rgb(71,168,255)',
success: 'rgb(0,202,81)',
notice: 'rgb(255,175,0)',
warning: '#FF4C8D',
danger: '#fd495a',
},
components: {
editor: {
danger: '#FF4C8D',
warning: '#fd495a',
},
dialog: {
surface: 'rgb(10,10,10)',
border: 'rgb(31,31,31)',
},
sidebar: {
border: 'rgb(31,31,31)',
},
responsePane: {
surface: 'rgb(10,10,10)',
border: 'rgb(31,31,31)',
},
appHeader: {
surface: 'rgb(10,10,10)',
border: 'rgb(31,31,31)',
},
},
},
], ],
}; };

View File

@@ -1,6 +1,5 @@
const { readdirSync, cpSync } = require('node:fs'); const { readdirSync, cpSync } = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const { execSync } = require('node:child_process');
const pluginsDir = path.join(__dirname, '..', 'plugins'); const pluginsDir = path.join(__dirname, '..', 'plugins');
@@ -9,7 +8,6 @@ console.log('Copying Yaak plugins to', pluginsDir);
for (const name of readdirSync(pluginsDir)) { for (const name of readdirSync(pluginsDir)) {
const dir = path.join(pluginsDir, name); const dir = path.join(pluginsDir, name);
if (name.startsWith('.')) continue; if (name.startsWith('.')) continue;
execSync('npm run build', { cwd: dir });
const destDir = path.join(__dirname, '../src-tauri/vendored/plugins/', name); const destDir = path.join(__dirname, '../src-tauri/vendored/plugins/', name);
console.log(`Copying ${name} to ${destDir}`); console.log(`Copying ${name} to ${destDir}`);
cpSync(path.join(dir, 'package.json'), path.join(destDir, 'package.json')); cpSync(path.join(dir, 'package.json'), path.join(destDir, 'package.json'));

339
src-tauri/Cargo.lock generated
View File

@@ -2,15 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.0" version = "2.0.0"
@@ -85,12 +76,6 @@ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
] ]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]] [[package]]
name = "android_log-sys" name = "android_log-sys"
version = "0.3.2" version = "0.3.2"
@@ -428,21 +413,6 @@ dependencies = [
"tower-service", "tower-service",
] ]
[[package]]
name = "backtrace"
version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "base32" name = "base32"
version = "0.5.1" version = "0.5.1"
@@ -717,7 +687,7 @@ dependencies = [
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.12", "thiserror 2.0.17",
] ]
[[package]] [[package]]
@@ -816,17 +786,16 @@ dependencies = [
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.41" version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [ dependencies = [
"android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde", "serde",
"wasm-bindgen", "wasm-bindgen",
"windows-link", "windows-link 0.2.1",
] ]
[[package]] [[package]]
@@ -1290,7 +1259,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users", "redox_users",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -2002,12 +1971,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]] [[package]]
name = "gio" name = "gio"
version = "0.18.4" version = "0.18.4"
@@ -2389,9 +2352,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.14" version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@@ -2405,7 +2368,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.6.1",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
@@ -2827,7 +2790,7 @@ dependencies = [
"dbus-secret-service", "dbus-secret-service",
"log", "log",
"security-framework 2.11.1", "security-framework 2.11.1",
"security-framework 3.2.0", "security-framework 3.5.1",
"windows-sys 0.60.2", "windows-sys 0.60.2",
"zeroize", "zeroize",
] ]
@@ -3046,9 +3009,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.27" version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
dependencies = [ dependencies = [
"value-bag", "value-bag",
] ]
@@ -3205,7 +3168,7 @@ dependencies = [
"once_cell", "once_cell",
"png", "png",
"serde", "serde",
"thiserror 2.0.12", "thiserror 2.0.17",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -3628,15 +3591,6 @@ dependencies = [
"objc2-security", "objc2-security",
] ]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
@@ -3782,7 +3736,7 @@ dependencies = [
"objc2-osa-kit", "objc2-osa-kit",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.12", "thiserror 2.0.17",
] ]
[[package]] [[package]]
@@ -4334,8 +4288,8 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2", "socket2 0.5.10",
"thiserror 2.0.12", "thiserror 2.0.17",
"tokio", "tokio",
"tracing", "tracing",
"web-time", "web-time",
@@ -4356,7 +4310,7 @@ dependencies = [
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"slab", "slab",
"thiserror 2.0.12", "thiserror 2.0.17",
"tinyvec", "tinyvec",
"tracing", "tracing",
"web-time", "web-time",
@@ -4371,7 +4325,7 @@ dependencies = [
"cfg_aliases", "cfg_aliases",
"libc", "libc",
"once_cell", "once_cell",
"socket2", "socket2 0.5.10",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -4552,7 +4506,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [ dependencies = [
"getrandom 0.2.16", "getrandom 0.2.16",
"libredox", "libredox",
"thiserror 2.0.12", "thiserror 2.0.17",
] ]
[[package]] [[package]]
@@ -4766,12 +4720,6 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.1" version = "2.1.1"
@@ -4815,9 +4763,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.27" version = "0.23.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"ring", "ring",
@@ -4836,7 +4784,7 @@ dependencies = [
"openssl-probe", "openssl-probe",
"rustls-pki-types", "rustls-pki-types",
"schannel", "schannel",
"security-framework 3.2.0", "security-framework 3.5.1",
] ]
[[package]] [[package]]
@@ -4851,9 +4799,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-platform-verifier" name = "rustls-platform-verifier"
version = "0.6.0" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda84358ed17f1f354cf4b1909ad346e6c7bc2513e8c40eb08e0157aa13a9070" checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [ dependencies = [
"core-foundation 0.10.1", "core-foundation 0.10.1",
"core-foundation-sys", "core-foundation-sys",
@@ -4864,10 +4812,10 @@ dependencies = [
"rustls-native-certs", "rustls-native-certs",
"rustls-platform-verifier-android", "rustls-platform-verifier-android",
"rustls-webpki", "rustls-webpki",
"security-framework 3.2.0", "security-framework 3.5.1",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -4878,9 +4826,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.3" version = "0.103.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
dependencies = [ dependencies = [
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@@ -4981,7 +4929,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.101", "syn 2.0.101",
"thiserror 2.0.12", "thiserror 2.0.17",
] ]
[[package]] [[package]]
@@ -5015,9 +4963,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "3.2.0" version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"core-foundation 0.10.1", "core-foundation 0.10.1",
@@ -5028,9 +4976,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.14.0" version = "2.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -5065,9 +5013,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.226" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [ dependencies = [
"serde_core", "serde_core",
"serde_derive", "serde_derive",
@@ -5107,18 +5055,18 @@ dependencies = [
[[package]] [[package]]
name = "serde_core" name = "serde_core"
version = "1.0.226" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.226" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5138,14 +5086,15 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.140" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
"ryu", "ryu",
"serde", "serde",
"serde_core",
] ]
[[package]] [[package]]
@@ -5392,6 +5341,16 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "socket2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]] [[package]]
name = "softbuffer" name = "softbuffer"
version = "0.4.6" version = "0.4.6"
@@ -5587,9 +5546,9 @@ dependencies = [
[[package]] [[package]]
name = "tao" name = "tao"
version = "0.34.3" version = "0.34.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7" checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"block2 0.6.1", "block2 0.6.1",
@@ -5661,9 +5620,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.8.5" version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c" checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5701,11 +5660,10 @@ dependencies = [
"tauri-runtime", "tauri-runtime",
"tauri-runtime-wry", "tauri-runtime-wry",
"tauri-utils", "tauri-utils",
"thiserror 2.0.12", "thiserror 2.0.17",
"tokio", "tokio",
"tray-icon", "tray-icon",
"url", "url",
"urlpattern",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"window-vibrancy", "window-vibrancy",
@@ -5714,9 +5672,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.4.1" version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" checksum = "a924b6c50fe83193f0f8b14072afa7c25b7a72752a2a73d9549b463f5fe91a38"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@@ -5736,9 +5694,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "2.4.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a" checksum = "6c1fe64c74cc40f90848281a90058a6db931eb400b60205840e09801ee30f190"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"brotli", "brotli",
@@ -5754,7 +5712,7 @@ dependencies = [
"sha2", "sha2",
"syn 2.0.101", "syn 2.0.101",
"tauri-utils", "tauri-utils",
"thiserror 2.0.12", "thiserror 2.0.17",
"time", "time",
"url", "url",
"uuid", "uuid",
@@ -5763,9 +5721,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "2.4.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e" checksum = "260c5d2eb036b76206b9fca20b7be3614cfd21046c5396f7959e0e64a4b07f2f"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -5777,9 +5735,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin" name = "tauri-plugin"
version = "2.4.0" version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glob", "glob",
@@ -5804,7 +5762,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.12", "thiserror 2.0.17",
] ]
[[package]] [[package]]
@@ -5821,7 +5779,7 @@ dependencies = [
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"tauri-utils", "tauri-utils",
"thiserror 2.0.12", "thiserror 2.0.17",
"tracing", "tracing",
"url", "url",
"windows-registry", "windows-registry",
@@ -5830,9 +5788,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.4.0" version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19"
dependencies = [ dependencies = [
"log", "log",
"raw-window-handle", "raw-window-handle",
@@ -5842,15 +5800,15 @@ dependencies = [
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"tauri-plugin-fs", "tauri-plugin-fs",
"thiserror 2.0.12", "thiserror 2.0.17",
"url", "url",
] ]
[[package]] [[package]]
name = "tauri-plugin-fs" name = "tauri-plugin-fs"
version = "2.4.2" version = "2.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dunce", "dunce",
@@ -5863,7 +5821,7 @@ dependencies = [
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"tauri-utils", "tauri-utils",
"thiserror 2.0.12", "thiserror 2.0.17",
"toml 0.9.5", "toml 0.9.5",
"url", "url",
] ]
@@ -5886,7 +5844,7 @@ dependencies = [
"swift-rs", "swift-rs",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.12", "thiserror 2.0.17",
"time", "time",
] ]
@@ -5906,7 +5864,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.12", "thiserror 2.0.17",
"url", "url",
"windows", "windows",
"zbus", "zbus",
@@ -5927,14 +5885,14 @@ dependencies = [
"sys-locale", "sys-locale",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.12", "thiserror 2.0.17",
] ]
[[package]] [[package]]
name = "tauri-plugin-shell" name = "tauri-plugin-shell"
version = "2.3.1" version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54777d0c0d8add34eea3ced84378619ef5b97996bd967d3038c668feefd21071" checksum = "c374b6db45f2a8a304f0273a15080d98c70cde86178855fc24653ba657a1144c"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
"log", "log",
@@ -5947,7 +5905,7 @@ dependencies = [
"shared_child", "shared_child",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.12", "thiserror 2.0.17",
"tokio", "tokio",
] ]
@@ -5961,7 +5919,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin-deep-link", "tauri-plugin-deep-link",
"thiserror 2.0.12", "thiserror 2.0.17",
"tracing", "tracing",
"windows-sys 0.60.2", "windows-sys 0.60.2",
"zbus", "zbus",
@@ -5991,7 +5949,7 @@ dependencies = [
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"tempfile", "tempfile",
"thiserror 2.0.12", "thiserror 2.0.17",
"time", "time",
"tokio", "tokio",
"url", "url",
@@ -6011,14 +5969,14 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.12", "thiserror 2.0.17",
] ]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.8.0" version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846" checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926"
dependencies = [ dependencies = [
"cookie", "cookie",
"dpi", "dpi",
@@ -6032,7 +5990,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"thiserror 2.0.12", "thiserror 2.0.17",
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
@@ -6041,9 +5999,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "2.8.1" version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807" checksum = "929f5df216f5c02a9e894554401bcdab6eec3e39ec6a4a7731c7067fc8688a93"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
@@ -6068,9 +6026,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.7.0" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212" checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"brotli", "brotli",
@@ -6096,7 +6054,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_with", "serde_with",
"swift-rs", "swift-rs",
"thiserror 2.0.12", "thiserror 2.0.17",
"toml 0.9.5", "toml 0.9.5",
"url", "url",
"urlpattern", "urlpattern",
@@ -6159,11 +6117,11 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.12" version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [ dependencies = [
"thiserror-impl 2.0.12", "thiserror-impl 2.0.17",
] ]
[[package]] [[package]]
@@ -6179,9 +6137,9 @@ dependencies = [
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "2.0.12" version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -6268,27 +6226,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.45.1" version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [ dependencies = [
"backtrace",
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2 0.6.1",
"tokio-macros", "tokio-macros",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.5.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -6478,7 +6435,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"pin-project", "pin-project",
"prost", "prost",
"socket2", "socket2 0.5.10",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tower 0.4.13", "tower 0.4.13",
@@ -6614,7 +6571,7 @@ dependencies = [
"once_cell", "once_cell",
"png", "png",
"serde", "serde",
"thiserror 2.0.12", "thiserror 2.0.17",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -6645,21 +6602,21 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "ts-rs" name = "ts-rs"
version = "11.0.1" version = "11.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be" checksum = "4994acea2522cd2b3b85c1d9529a55991e3ad5e25cdcd3de9d505972c4379424"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde_json", "serde_json",
"thiserror 2.0.12", "thiserror 2.0.17",
"ts-rs-macros", "ts-rs-macros",
] ]
[[package]] [[package]]
name = "ts-rs-macros" name = "ts-rs-macros"
version = "11.0.1" version = "11.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a" checksum = "ee6ff59666c9cbaec3533964505d39154dc4e0a56151fdea30a09ed0301f62e2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -6682,7 +6639,7 @@ dependencies = [
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"sha1", "sha1",
"thiserror 2.0.12", "thiserror 2.0.17",
"utf-8", "utf-8",
] ]
@@ -7204,7 +7161,7 @@ version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
dependencies = [ dependencies = [
"thiserror 2.0.12", "thiserror 2.0.17",
"windows", "windows",
"windows-core", "windows-core",
] ]
@@ -7270,7 +7227,7 @@ dependencies = [
"windows-collections", "windows-collections",
"windows-core", "windows-core",
"windows-future", "windows-future",
"windows-link", "windows-link 0.1.1",
"windows-numerics", "windows-numerics",
] ]
@@ -7291,7 +7248,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [ dependencies = [
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
"windows-link", "windows-link 0.1.1",
"windows-result", "windows-result",
"windows-strings", "windows-strings",
] ]
@@ -7303,7 +7260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [ dependencies = [
"windows-core", "windows-core",
"windows-link", "windows-link 0.1.1",
"windows-threading", "windows-threading",
] ]
@@ -7335,6 +7292,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]] [[package]]
name = "windows-numerics" name = "windows-numerics"
version = "0.2.0" version = "0.2.0"
@@ -7342,7 +7305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [ dependencies = [
"windows-core", "windows-core",
"windows-link", "windows-link 0.1.1",
] ]
[[package]] [[package]]
@@ -7351,7 +7314,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.1",
"windows-result", "windows-result",
"windows-strings", "windows-strings",
] ]
@@ -7362,7 +7325,7 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.1",
] ]
[[package]] [[package]]
@@ -7371,7 +7334,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.1",
] ]
[[package]] [[package]]
@@ -7410,6 +7373,15 @@ dependencies = [
"windows-targets 0.53.2", "windows-targets 0.53.2",
] ]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link 0.2.1",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.42.2" version = "0.42.2"
@@ -7478,7 +7450,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.1",
] ]
[[package]] [[package]]
@@ -7487,7 +7459,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.1",
] ]
[[package]] [[package]]
@@ -7718,7 +7690,7 @@ dependencies = [
"os_pipe", "os_pipe",
"rustix 0.38.44", "rustix 0.38.44",
"tempfile", "tempfile",
"thiserror 2.0.12", "thiserror 2.0.17",
"tree_magic_mini", "tree_magic_mini",
"wayland-backend", "wayland-backend",
"wayland-client", "wayland-client",
@@ -7734,9 +7706,9 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]] [[package]]
name = "wry" name = "wry"
version = "0.53.3" version = "0.53.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90" checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"block2 0.6.1", "block2 0.6.1",
@@ -7766,7 +7738,7 @@ dependencies = [
"sha2", "sha2",
"soup3", "soup3",
"tao-macros", "tao-macros",
"thiserror 2.0.12", "thiserror 2.0.17",
"url", "url",
"webkit2gtk", "webkit2gtk",
"webkit2gtk-sys", "webkit2gtk-sys",
@@ -7865,7 +7837,7 @@ dependencies = [
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
"tauri-plugin-updater", "tauri-plugin-updater",
"tauri-plugin-window-state", "tauri-plugin-window-state",
"thiserror 2.0.12", "thiserror 2.0.17",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"ts-rs", "ts-rs",
@@ -7894,7 +7866,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"tauri", "tauri",
"thiserror 2.0.12", "thiserror 2.0.17",
] ]
[[package]] [[package]]
@@ -7909,7 +7881,7 @@ dependencies = [
"serde", "serde",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.12", "thiserror 2.0.17",
"yaak-models", "yaak-models",
] ]
@@ -7921,7 +7893,7 @@ dependencies = [
"serde", "serde",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.12", "thiserror 2.0.17",
"ts-rs", "ts-rs",
] ]
@@ -7937,7 +7909,7 @@ dependencies = [
"serde_yaml", "serde_yaml",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.12", "thiserror 2.0.17",
"ts-rs", "ts-rs",
"yaak-models", "yaak-models",
"yaak-sync", "yaak-sync",
@@ -7973,9 +7945,18 @@ dependencies = [
name = "yaak-http" name = "yaak-http"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"hyper-util",
"log",
"regex", "regex",
"reqwest",
"reqwest_cookie_store",
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
"serde",
"tauri",
"thiserror 2.0.17",
"tokio",
"tower-service",
"urlencoding", "urlencoding",
"yaak-models", "yaak-models",
] ]
@@ -7991,7 +7972,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.12", "thiserror 2.0.17",
"ts-rs", "ts-rs",
"yaak-common", "yaak-common",
"yaak-models", "yaak-models",
@@ -8030,9 +8011,9 @@ dependencies = [
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"tauri-plugin-dialog", "tauri-plugin-dialog",
"thiserror 2.0.12", "thiserror 2.0.17",
"tokio",
"ts-rs", "ts-rs",
"yaak-common",
] ]
[[package]] [[package]]
@@ -8057,7 +8038,7 @@ dependencies = [
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"tauri-plugin-shell", "tauri-plugin-shell",
"thiserror 2.0.12", "thiserror 2.0.17",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"ts-rs", "ts-rs",
@@ -8091,7 +8072,7 @@ dependencies = [
"sha1", "sha1",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.12", "thiserror 2.0.17",
"tokio", "tokio",
"ts-rs", "ts-rs",
"yaak-models", "yaak-models",
@@ -8106,7 +8087,7 @@ dependencies = [
"serde", "serde",
"serde-wasm-bindgen", "serde-wasm-bindgen",
"serde_json", "serde_json",
"thiserror 2.0.12", "thiserror 2.0.17",
"tokio", "tokio",
"ts-rs", "ts-rs",
"wasm-bindgen", "wasm-bindgen",
@@ -8124,7 +8105,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.12", "thiserror 2.0.17",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"yaak-http", "yaak-http",
@@ -8345,7 +8326,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aed5f10c571472911e37d8f7601a8dfba52b4f7f73a344015291b82ab292faf6" checksum = "aed5f10c571472911e37d8f7601a8dfba52b4f7f73a344015291b82ab292faf6"
dependencies = [ dependencies = [
"log", "log",
"thiserror 2.0.12", "thiserror 2.0.17",
"zip", "zip",
] ]

View File

@@ -37,7 +37,7 @@ updater = []
license = ["yaak-license"] license = ["yaak-license"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.4.1", features = [] } tauri-build = { version = "2.5.0", features = [] }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
@@ -48,7 +48,7 @@ chrono = { workspace = true, features = ["serde"] }
cookie = "0.18.1" cookie = "0.18.1"
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" } eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
http = { version = "1.2.0", default-features = false } http = { version = "1.2.0", default-features = false }
log = "0.4.27" log = { workspace = true }
md5 = "0.8.0" md5 = "0.8.0"
mime_guess = "2.0.5" mime_guess = "2.0.5"
rand = "0.9.0" rand = "0.9.0"
@@ -89,23 +89,24 @@ yaak-templates = { workspace = true }
yaak-ws = { path = "yaak-ws" } yaak-ws = { path = "yaak-ws" }
[workspace.dependencies] [workspace.dependencies]
chrono = "0.4.41" chrono = "0.4.42"
hex = "0.4.3" hex = "0.4.3"
keyring = "3.6.3" keyring = "3.6.3"
reqwest = "0.12.20" reqwest = "0.12.20"
reqwest_cookie_store = "0.8.0" reqwest_cookie_store = "0.8.0"
rustls = { version = "0.23.27", default-features = false } rustls = { version = "0.23.34", default-features = false }
rustls-platform-verifier = "0.6.0" rustls-platform-verifier = "0.6.2"
serde = "1.0.219" serde = "1.0.228"
serde_json = "1.0.140" serde_json = "1.0.145"
sha2 = "0.10.9" sha2 = "0.10.9"
tauri = "2.8.5" log = "0.4.28"
tauri-plugin = "2.4.0" tauri = "2.9.2"
tauri-plugin-dialog = "2.4.0" tauri-plugin = "2.5.1"
tauri-plugin-shell = "2.3.1" tauri-plugin-dialog = "2.4.2"
thiserror = "2.0.12" tauri-plugin-shell = "2.3.3"
tokio = "1.45.1" thiserror = "2.0.17"
ts-rs = "11.0.1" tokio = "1.48.0"
ts-rs = "11.1.0"
yaak-common = { path = "yaak-common" } yaak-common = { path = "yaak-common" }
yaak-crypto = { path = "yaak-crypto" } yaak-crypto = { path = "yaak-crypto" }
yaak-fonts = { path = "yaak-fonts" } yaak-fonts = { path = "yaak-fonts" }

View File

@@ -2,7 +2,7 @@ use crate::error::Result;
use tauri::{command, AppHandle, Manager, Runtime, State, WebviewWindow}; use tauri::{command, AppHandle, Manager, Runtime, State, WebviewWindow};
use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
use yaak_crypto::manager::EncryptionManagerExt; use yaak_crypto::manager::EncryptionManagerExt;
use yaak_plugins::events::{GetThemesResponse, PluginWindowContext}; use yaak_plugins::events::{GetThemesResponse, PluginContext};
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
use yaak_plugins::native_template_functions::{ use yaak_plugins::native_template_functions::{
decrypt_secure_template_function, encrypt_secure_template_function, decrypt_secure_template_function, encrypt_secure_template_function,
@@ -28,8 +28,8 @@ pub(crate) async fn cmd_decrypt_template<R: Runtime>(
template: &str, template: &str,
) -> Result<String> { ) -> Result<String> {
let app_handle = window.app_handle(); let app_handle = window.app_handle();
let window_context = &PluginWindowContext::new(&window); let plugin_context = &PluginContext::new(&window);
Ok(decrypt_secure_template_function(&app_handle, window_context, template)?) Ok(decrypt_secure_template_function(&app_handle, plugin_context, template)?)
} }
#[command] #[command]
@@ -38,8 +38,8 @@ pub(crate) async fn cmd_secure_template<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
template: &str, template: &str,
) -> Result<String> { ) -> Result<String> {
let window_context = &PluginWindowContext::new(&window); let plugin_context = &PluginContext::new(&window);
Ok(encrypt_secure_template_function(&app_handle, window_context, template)?) Ok(encrypt_secure_template_function(&app_handle, plugin_context, template)?)
} }
#[command] #[command]

View File

@@ -1,5 +1,4 @@
use log::debug; use mime_guess::{mime, Mime};
use mime_guess::{Mime, mime};
use std::path::Path; use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use tokio::fs; use tokio::fs;
@@ -7,14 +6,8 @@ use tokio::fs;
pub async fn read_response_body(body_path: impl AsRef<Path>, content_type: &str) -> Option<String> { pub async fn read_response_body(body_path: impl AsRef<Path>, content_type: &str) -> Option<String> {
let body = fs::read(body_path).await.ok()?; let body = fs::read(body_path).await.ok()?;
let body_charset = parse_charset(content_type).unwrap_or("utf-8".to_string()); let body_charset = parse_charset(content_type).unwrap_or("utf-8".to_string());
debug!("body_charset: {}", body_charset);
if let Some(decoder) = charset::Charset::for_label(body_charset.as_bytes()) { if let Some(decoder) = charset::Charset::for_label(body_charset.as_bytes()) {
debug!("Using decoder for charset: {}", body_charset); let (cow, _real_encoding, _exist_replace) = decoder.decode(&body);
let (cow, real_encoding, exist_replace) = decoder.decode(&body);
debug!(
"Decoded body with charset: {}, real_encoding: {:?}, exist_replace: {}",
body_charset, real_encoding, exist_replace
);
return cow.into_owned().into(); return cow.into_owned().into();
} }

View File

@@ -16,6 +16,9 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
CryptoError(#[from] yaak_crypto::error::Error), CryptoError(#[from] yaak_crypto::error::Error),
#[error(transparent)]
HttpError(#[from] yaak_http::error::Error),
#[error(transparent)] #[error(transparent)]
GitError(#[from] yaak_git::error::Error), GitError(#[from] yaak_git::error::Error),
@@ -35,6 +38,9 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
CommonError(#[from] yaak_common::error::Error), CommonError(#[from] yaak_common::error::Error),
#[error(transparent)]
ClipboardError(#[from] tauri_plugin_clipboard_manager::Error),
#[error("Updater error: {0}")] #[error("Updater error: {0}")]
UpdaterError(#[from] tauri_plugin_updater::Error), UpdaterError(#[from] tauri_plugin_updater::Error),

View File

@@ -6,7 +6,7 @@ use tauri::{Manager, Runtime, WebviewWindow};
use yaak_grpc::{KeyAndValueRef, MetadataMap}; use yaak_grpc::{KeyAndValueRef, MetadataMap};
use yaak_models::models::GrpcRequest; use yaak_models::models::GrpcRequest;
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader}; use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, PluginContext};
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String> { pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String> {
@@ -81,7 +81,12 @@ pub(crate) async fn build_metadata<R: Runtime>(
.collect(), .collect(),
}; };
let plugin_result = plugin_manager let plugin_result = plugin_manager
.call_http_authentication(&window, &authentication_type, plugin_req) .call_http_authentication(
&window,
&authentication_type,
plugin_req,
&PluginContext::new(window),
)
.await?; .await?;
for header in plugin_result.set_headers.unwrap_or_default() { for header in plugin_result.set_headers.unwrap_or_default() {
metadata.insert(header.name, header.value); metadata.insert(header.name, header.value);

View File

@@ -1,48 +1,74 @@
use chrono::{NaiveDateTime, Utc};
use log::debug;
use std::sync::OnceLock;
use tauri::{AppHandle, Runtime}; use tauri::{AppHandle, Runtime};
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
const NAMESPACE: &str = "analytics"; const NAMESPACE: &str = "analytics";
const NUM_LAUNCHES_KEY: &str = "num_launches"; const NUM_LAUNCHES_KEY: &str = "num_launches";
const LAST_VERSION_KEY: &str = "last_tracked_version";
const PREV_VERSION_KEY: &str = "last_tracked_version_prev";
const VERSION_SINCE_KEY: &str = "last_tracked_version_since";
#[derive(Default, Debug)] #[derive(Default, Debug, Clone)]
pub struct LaunchEventInfo { pub struct LaunchEventInfo {
pub current_version: String, pub current_version: String,
pub previous_version: String, pub previous_version: String,
pub launched_after_update: bool, pub launched_after_update: bool,
pub version_since: NaiveDateTime,
pub user_since: NaiveDateTime,
pub num_launches: i32, pub num_launches: i32,
} }
pub async fn store_launch_history<R: Runtime>(app_handle: &AppHandle<R>) -> LaunchEventInfo { static LAUNCH_INFO: OnceLock<LaunchEventInfo> = OnceLock::new();
let last_tracked_version_key = "last_tracked_version";
let mut info = LaunchEventInfo::default(); pub fn get_or_upsert_launch_info<R: Runtime>(app_handle: &AppHandle<R>) -> &LaunchEventInfo {
LAUNCH_INFO.get_or_init(|| {
let now = Utc::now().naive_utc();
let mut info = LaunchEventInfo {
version_since: app_handle.db().get_key_value_dte(NAMESPACE, VERSION_SINCE_KEY, now),
current_version: app_handle.package_info().version.to_string(),
user_since: app_handle.db().get_settings().created_at,
num_launches: app_handle.db().get_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, 0) + 1,
info.num_launches = get_num_launches(app_handle).await + 1; // The rest will be set below
info.current_version = app_handle.package_info().version.to_string(); ..Default::default()
};
app_handle app_handle
.with_tx(|tx| { .with_tx(|tx| {
info.previous_version = // Load the previously tracked version
tx.get_key_value_string(NAMESPACE, last_tracked_version_key, ""); let curr_db = tx.get_key_value_str(NAMESPACE, LAST_VERSION_KEY, "");
let prev_db = tx.get_key_value_str(NAMESPACE, PREV_VERSION_KEY, "");
if !info.previous_version.is_empty() { // We just updated if the app version is different from the last tracked version we stored
info.launched_after_update = info.current_version != info.previous_version; if !curr_db.is_empty() && info.current_version != curr_db {
}; info.launched_after_update = true;
}
// Update key values // If we just updated, track the previous version as the "previous" current version
if info.launched_after_update {
info.previous_version = curr_db.clone();
info.version_since = now;
} else {
info.previous_version = prev_db.clone();
}
let source = &UpdateSource::Background; // Rotate stored versions: move previous into the "prev" slot before overwriting
let version = info.current_version.as_str(); let source = &UpdateSource::Background;
tx.set_key_value_string(NAMESPACE, last_tracked_version_key, version, source);
tx.set_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, source);
Ok(())
})
.unwrap();
info tx.set_key_value_str(NAMESPACE, PREV_VERSION_KEY, &info.previous_version, source);
} tx.set_key_value_str(NAMESPACE, LAST_VERSION_KEY, &info.current_version, source);
tx.set_key_value_dte(NAMESPACE, VERSION_SINCE_KEY, info.version_since, source);
pub async fn get_num_launches<R: Runtime>(app_handle: &AppHandle<R>) -> i32 { tx.set_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, source);
app_handle.db().get_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, 0)
Ok(())
})
.unwrap();
debug!("Initialized launch info");
info
})
} }

View File

@@ -6,9 +6,9 @@ use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue}; use http::{HeaderMap, HeaderName, HeaderValue};
use log::{debug, error, warn}; use log::{debug, error, warn};
use mime_guess::Mime; use mime_guess::Mime;
use reqwest::redirect::Policy; use reqwest::{Method, Response};
use reqwest::{Method, NoProxy, Response}; use reqwest::{Url, multipart};
use reqwest::{Proxy, Url, multipart}; use reqwest_cookie_store::{CookieStore, CookieStoreMutex};
use serde_json::Value; use serde_json::Value;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::PathBuf; use std::path::PathBuf;
@@ -21,6 +21,10 @@ use tokio::fs::{File, create_dir_all};
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio::sync::watch::Receiver; use tokio::sync::watch::Receiver;
use tokio::sync::{Mutex, oneshot}; use tokio::sync::{Mutex, oneshot};
use yaak_http::client::{
HttpConnectionOptions, HttpConnectionProxySetting, HttpConnectionProxySettingAuth,
};
use yaak_http::manager::HttpConnectionManager;
use yaak_models::models::{ use yaak_models::models::{
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader, Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader,
HttpResponseState, ProxySetting, ProxySettingAuth, HttpResponseState, ProxySetting, ProxySettingAuth,
@@ -28,7 +32,7 @@ use yaak_models::models::{
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
use yaak_plugins::events::{ use yaak_plugins::events::{
CallHttpAuthenticationRequest, HttpHeader, PluginWindowContext, RenderPurpose, CallHttpAuthenticationRequest, HttpHeader, PluginContext, RenderPurpose,
}; };
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_plugins::template_callback::PluginTemplateCallback;
@@ -41,9 +45,31 @@ pub async fn send_http_request<R: Runtime>(
environment: Option<Environment>, environment: Option<Environment>,
cookie_jar: Option<CookieJar>, cookie_jar: Option<CookieJar>,
cancelled_rx: &mut Receiver<bool>, cancelled_rx: &mut Receiver<bool>,
) -> Result<HttpResponse> {
send_http_request_with_context(
window,
unrendered_request,
og_response,
environment,
cookie_jar,
cancelled_rx,
&PluginContext::new(window),
)
.await
}
pub async fn send_http_request_with_context<R: Runtime>(
window: &WebviewWindow<R>,
unrendered_request: &HttpRequest,
og_response: &HttpResponse,
environment: Option<Environment>,
cookie_jar: Option<CookieJar>,
cancelled_rx: &mut Receiver<bool>,
plugin_context: &PluginContext,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let app_handle = window.app_handle().clone(); let app_handle = window.app_handle().clone();
let plugin_manager = app_handle.state::<PluginManager>(); let plugin_manager = app_handle.state::<PluginManager>();
let connection_manager = app_handle.state::<HttpConnectionManager>();
let settings = window.db().get_settings(); let settings = window.db().get_settings();
let workspace = window.db().get_workspace(&unrendered_request.workspace_id)?; let workspace = window.db().get_workspace(&unrendered_request.workspace_id)?;
let environment_id = environment.map(|e| e.id); let environment_id = environment.map(|e| e.id);
@@ -71,11 +97,7 @@ pub async fn send_http_request<R: Runtime>(
} }
}; };
let cb = PluginTemplateCallback::new( let cb = PluginTemplateCallback::new(window.app_handle(), &plugin_context, RenderPurpose::Send);
window.app_handle(),
&PluginWindowContext::new(window),
RenderPurpose::Send,
);
let opt = RenderOptions { let opt = RenderOptions {
error_behavior: RenderErrorBehavior::Throw, error_behavior: RenderErrorBehavior::Throw,
@@ -101,64 +123,33 @@ pub async fn send_http_request<R: Runtime>(
} }
debug!("Sending request to {} {url_string}", request.method); debug!("Sending request to {} {url_string}", request.method);
let mut client_builder = reqwest::Client::builder() let proxy_setting = match settings.proxy {
.redirect(match workspace.setting_follow_redirects { None => HttpConnectionProxySetting::System,
true => Policy::limited(10), // TODO: Handle redirects natively Some(ProxySetting::Disabled) => HttpConnectionProxySetting::Disabled,
false => Policy::none(),
})
.connection_verbose(true)
.gzip(true)
.brotli(true)
.deflate(true)
.referer(false)
.tls_info(true);
let tls_config = yaak_http::tls::get_config(workspace.setting_validate_certificates, true);
client_builder = client_builder.use_preconfigured_tls(tls_config);
match settings.proxy {
Some(ProxySetting::Disabled) => client_builder = client_builder.no_proxy(),
Some(ProxySetting::Enabled { Some(ProxySetting::Enabled {
http, http,
https, https,
auth, auth,
disabled,
bypass, bypass,
}) if !disabled => { disabled,
debug!("Using proxy http={http} https={https} bypass={bypass}"); }) => {
if !http.is_empty() { if disabled {
match Proxy::http(http) { HttpConnectionProxySetting::System
Ok(mut proxy) => { } else {
if let Some(ProxySettingAuth { user, password }) = auth.clone() { HttpConnectionProxySetting::Enabled {
debug!("Using http proxy auth"); http,
proxy = proxy.basic_auth(user.as_str(), password.as_str()); https,
bypass,
auth: match auth {
None => None,
Some(ProxySettingAuth { user, password }) => {
Some(HttpConnectionProxySettingAuth { user, password })
} }
proxy = proxy.no_proxy(NoProxy::from_string(&bypass)); },
client_builder = client_builder.proxy(proxy); }
}
Err(e) => {
warn!("Failed to apply http proxy {e:?}");
}
};
}
if !https.is_empty() {
match Proxy::https(https) {
Ok(mut proxy) => {
if let Some(ProxySettingAuth { user, password }) = auth {
debug!("Using https proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
proxy = proxy.no_proxy(NoProxy::from_string(&bypass));
client_builder = client_builder.proxy(proxy);
}
Err(e) => {
warn!("Failed to apply https proxy {e:?}");
}
};
} }
} }
_ => {} // Nothing to do for this one, as it is the default };
}
// Add cookie store if specified // Add cookie store if specified
let maybe_cookie_manager = match cookie_jar.clone() { let maybe_cookie_manager = match cookie_jar.clone() {
@@ -177,23 +168,33 @@ pub async fn send_http_request<R: Runtime>(
.map(|c| Ok(c)) .map(|c| Ok(c))
.collect::<Vec<Result<_>>>(); .collect::<Vec<Result<_>>>();
let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true)?; let cookie_store = CookieStore::from_cookies(cookies, true)?;
let cookie_store = reqwest_cookie_store::CookieStoreMutex::new(store); let cookie_store = CookieStoreMutex::new(cookie_store);
let cookie_store = Arc::new(cookie_store); let cookie_store = Arc::new(cookie_store);
client_builder = client_builder.cookie_provider(Arc::clone(&cookie_store)); let cookie_provider = Arc::clone(&cookie_store);
Some((cookie_provider, cj))
Some((cookie_store, cj))
} }
None => None, None => None,
}; };
if workspace.setting_request_timeout > 0 { let client = connection_manager
client_builder = client_builder.timeout(Duration::from_millis( .get_client(
workspace.setting_request_timeout.unsigned_abs() as u64, &plugin_context.id,
)); &HttpConnectionOptions {
} follow_redirects: workspace.setting_follow_redirects,
validate_certificates: workspace.setting_validate_certificates,
let client = client_builder.build()?; proxy: proxy_setting,
cookie_provider: maybe_cookie_manager.as_ref().map(|(p, _)| Arc::clone(&p)),
timeout: if workspace.setting_request_timeout > 0 {
Some(Duration::from_millis(
workspace.setting_request_timeout.unsigned_abs() as u64
))
} else {
None
},
},
)
.await?;
// Render query parameters // Render query parameters
let mut query_params = Vec::new(); let mut query_params = Vec::new();
@@ -467,8 +468,9 @@ pub async fn send_http_request<R: Runtime>(
}) })
.collect(), .collect(),
}; };
let auth_result = let auth_result = plugin_manager
plugin_manager.call_http_authentication(&window, &authentication_type, req).await; .call_http_authentication(&window, &authentication_type, req, plugin_context)
.await;
let plugin_result = match auth_result { let plugin_result = match auth_result {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {

View File

@@ -28,7 +28,7 @@ pub(crate) async fn import_data<R: Runtime>(
.workspaces .workspaces
.into_iter() .into_iter()
.map(|mut v| { .map(|mut v| {
v.id = maybe_gen_id::<Workspace>(v.id.as_str(), &mut id_map); v.id = maybe_gen_id::<Workspace, R>(window, v.id.as_str(), &mut id_map);
v v
}) })
.collect(); .collect();
@@ -37,11 +37,12 @@ pub(crate) async fn import_data<R: Runtime>(
.environments .environments
.into_iter() .into_iter()
.map(|mut v| { .map(|mut v| {
v.id = maybe_gen_id::<Environment>(v.id.as_str(), &mut id_map); v.id = maybe_gen_id::<Environment, R>(window, v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map); v.workspace_id =
maybe_gen_id::<Workspace, R>(window, v.workspace_id.as_str(), &mut id_map);
match (v.parent_model.as_str(), v.parent_id.clone().as_deref()) { match (v.parent_model.as_str(), v.parent_id.clone().as_deref()) {
("folder", Some(parent_id)) => { ("folder", Some(parent_id)) => {
v.parent_id = Some(maybe_gen_id::<Folder>(&parent_id, &mut id_map)); v.parent_id = Some(maybe_gen_id::<Folder, R>(window, &parent_id, &mut id_map));
} }
("", _) => { ("", _) => {
// Fix any empty ones // Fix any empty ones
@@ -60,9 +61,10 @@ pub(crate) async fn import_data<R: Runtime>(
.folders .folders
.into_iter() .into_iter()
.map(|mut v| { .map(|mut v| {
v.id = maybe_gen_id::<Folder>(v.id.as_str(), &mut id_map); v.id = maybe_gen_id::<Folder, R>(window, v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map); v.workspace_id =
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map); maybe_gen_id::<Workspace, R>(window, v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder, R>(window, v.folder_id, &mut id_map);
v v
}) })
.collect(); .collect();
@@ -71,9 +73,10 @@ pub(crate) async fn import_data<R: Runtime>(
.http_requests .http_requests
.into_iter() .into_iter()
.map(|mut v| { .map(|mut v| {
v.id = maybe_gen_id::<HttpRequest>(v.id.as_str(), &mut id_map); v.id = maybe_gen_id::<HttpRequest, R>(window, v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map); v.workspace_id =
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map); maybe_gen_id::<Workspace, R>(window, v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder, R>(window, v.folder_id, &mut id_map);
v v
}) })
.collect(); .collect();
@@ -82,9 +85,10 @@ pub(crate) async fn import_data<R: Runtime>(
.grpc_requests .grpc_requests
.into_iter() .into_iter()
.map(|mut v| { .map(|mut v| {
v.id = maybe_gen_id::<GrpcRequest>(v.id.as_str(), &mut id_map); v.id = maybe_gen_id::<GrpcRequest, R>(window, v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map); v.workspace_id =
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map); maybe_gen_id::<Workspace, R>(window, v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder, R>(window, v.folder_id, &mut id_map);
v v
}) })
.collect(); .collect();
@@ -93,9 +97,10 @@ pub(crate) async fn import_data<R: Runtime>(
.websocket_requests .websocket_requests
.into_iter() .into_iter()
.map(|mut v| { .map(|mut v| {
v.id = maybe_gen_id::<WebsocketRequest>(v.id.as_str(), &mut id_map); v.id = maybe_gen_id::<WebsocketRequest, R>(window, v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map); v.workspace_id =
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map); maybe_gen_id::<Workspace, R>(window, v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder, R>(window, v.folder_id, &mut id_map);
v v
}) })
.collect(); .collect();

View File

@@ -1,6 +1,7 @@
extern crate core; extern crate core;
use crate::encoding::read_response_body; use crate::encoding::read_response_body;
use crate::error::Error::GenericError; use crate::error::Error::GenericError;
use crate::error::Result;
use crate::grpc::{build_metadata, metadata_to_map, resolve_grpc_request}; use crate::grpc::{build_metadata, metadata_to_map, resolve_grpc_request};
use crate::http_request::{resolve_http_request, send_http_request}; use crate::http_request::{resolve_http_request, send_http_request};
use crate::import::import_data; use crate::import::import_data;
@@ -22,7 +23,7 @@ use tauri::{Listener, Runtime};
use tauri::{Manager, WindowEvent}; use tauri::{Manager, WindowEvent};
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_log::fern::colors::ColoredLevelConfig; use tauri_plugin_log::fern::colors::ColoredLevelConfig;
use tauri_plugin_log::{Builder, Target, TargetKind}; use tauri_plugin_log::{Builder, Target, TargetKind, log};
use tauri_plugin_window_state::{AppHandleExt, StateFlags}; use tauri_plugin_window_state::{AppHandleExt, StateFlags};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::task::block_in_place; use tokio::task::block_in_place;
@@ -41,15 +42,16 @@ use yaak_plugins::events::{
CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs,
CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse, CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent, GetHttpRequestActionsResponse, GetTemplateFunctionConfigResponse,
InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest, GetTemplateFunctionSummaryResponse, InternalEvent, InternalEventPayload, JsonPrimitive,
PluginContext, RenderPurpose, ShowToastRequest,
}; };
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_meta::PluginMetadata; use yaak_plugins::plugin_meta::PluginMetadata;
use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_plugins::template_callback::PluginTemplateCallback;
use yaak_sse::sse::ServerSentEvent; use yaak_sse::sse::ServerSentEvent;
use yaak_templates::format::format_json; use yaak_templates::format_json::format_json;
use yaak_templates::{Tokens, transform_args, RenderOptions, RenderErrorBehavior}; use yaak_templates::{RenderErrorBehavior, RenderOptions, Tokens, transform_args};
mod commands; mod commands;
mod encoding; mod encoding;
@@ -101,7 +103,7 @@ async fn cmd_template_tokens_to_string<R: Runtime>(
) -> YaakResult<String> { ) -> YaakResult<String> {
let cb = PluginTemplateCallback::new( let cb = PluginTemplateCallback::new(
&app_handle, &app_handle,
&PluginWindowContext::new(&window), &PluginContext::new(&window),
RenderPurpose::Preview, RenderPurpose::Preview,
); );
let new_tokens = transform_args(tokens, &cb)?; let new_tokens = transform_args(tokens, &cb)?;
@@ -115,6 +117,7 @@ async fn cmd_render_template<R: Runtime>(
template: &str, template: &str,
workspace_id: &str, workspace_id: &str,
environment_id: Option<&str>, environment_id: Option<&str>,
purpose: Option<RenderPurpose>,
) -> YaakResult<String> { ) -> YaakResult<String> {
let environment_chain = let environment_chain =
app_handle.db().resolve_environments(workspace_id, None, environment_id)?; app_handle.db().resolve_environments(workspace_id, None, environment_id)?;
@@ -123,8 +126,8 @@ async fn cmd_render_template<R: Runtime>(
environment_chain, environment_chain,
&PluginTemplateCallback::new( &PluginTemplateCallback::new(
&app_handle, &app_handle,
&PluginWindowContext::new(&window), &PluginContext::new(&window),
RenderPurpose::Preview, purpose.unwrap_or(RenderPurpose::Preview),
), ),
&RenderOptions { &RenderOptions {
error_behavior: RenderErrorBehavior::Throw, error_behavior: RenderErrorBehavior::Throw,
@@ -167,7 +170,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
environment_chain, environment_chain,
&PluginTemplateCallback::new( &PluginTemplateCallback::new(
&app_handle, &app_handle,
&PluginWindowContext::new(&window), &PluginContext::new(&window),
RenderPurpose::Send, RenderPurpose::Send,
), ),
&RenderOptions { &RenderOptions {
@@ -216,7 +219,7 @@ async fn cmd_grpc_go<R: Runtime>(
environment_chain.clone(), environment_chain.clone(),
&PluginTemplateCallback::new( &PluginTemplateCallback::new(
&app_handle, &app_handle,
&PluginWindowContext::new(&window), &PluginContext::new(&window),
RenderPurpose::Send, RenderPurpose::Send,
), ),
&RenderOptions { &RenderOptions {
@@ -341,7 +344,7 @@ async fn cmd_grpc_go<R: Runtime>(
environment_chain, environment_chain,
&PluginTemplateCallback::new( &PluginTemplateCallback::new(
&app_handle, &app_handle,
&PluginWindowContext::new(&window), &PluginContext::new(&window),
RenderPurpose::Send, RenderPurpose::Send,
), ),
&RenderOptions { &RenderOptions {
@@ -413,7 +416,7 @@ async fn cmd_grpc_go<R: Runtime>(
environment_chain, environment_chain,
&PluginTemplateCallback::new( &PluginTemplateCallback::new(
&app_handle, &app_handle,
&PluginWindowContext::new(&window), &PluginContext::new(&window),
RenderPurpose::Send, RenderPurpose::Send,
), ),
&RenderOptions { &RenderOptions {
@@ -826,11 +829,40 @@ async fn cmd_grpc_request_actions<R: Runtime>(
} }
#[tauri::command] #[tauri::command]
async fn cmd_template_functions<R: Runtime>( async fn cmd_template_function_summaries<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>, plugin_manager: State<'_, PluginManager>,
) -> YaakResult<Vec<GetTemplateFunctionsResponse>> { ) -> YaakResult<Vec<GetTemplateFunctionSummaryResponse>> {
Ok(plugin_manager.get_template_functions(&window).await?) let results = plugin_manager.get_template_function_summaries(&window).await?;
Ok(results)
}
#[tauri::command]
async fn cmd_template_function_config<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
function_name: &str,
values: HashMap<String, JsonPrimitive>,
model: AnyModel,
environment_id: Option<&str>,
) -> YaakResult<GetTemplateFunctionConfigResponse> {
let (workspace_id, folder_id) = match model.clone() {
AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::Folder(m) => (m.workspace_id, m.folder_id),
AnyModel::Workspace(m) => (m.id, None),
m => {
return Err(GenericError(format!(
"Unsupported model to call template functions {m:?}"
)));
}
};
let environment_chain =
window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?;
Ok(plugin_manager
.get_template_function_config(&window, function_name, environment_chain, values, model.id())
.await?)
} }
#[tauri::command] #[tauri::command]
@@ -848,10 +880,10 @@ async fn cmd_get_http_authentication_config<R: Runtime>(
plugin_manager: State<'_, PluginManager>, plugin_manager: State<'_, PluginManager>,
auth_name: &str, auth_name: &str,
values: HashMap<String, JsonPrimitive>, values: HashMap<String, JsonPrimitive>,
request: AnyModel, model: AnyModel,
environment_id: Option<&str>, environment_id: Option<&str>,
) -> YaakResult<GetHttpAuthenticationConfigResponse> { ) -> YaakResult<GetHttpAuthenticationConfigResponse> {
let (workspace_id, folder_id) = match request.clone() { let (workspace_id, folder_id) = match model.clone() {
AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id), AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id), AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id), AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id),
@@ -866,7 +898,7 @@ async fn cmd_get_http_authentication_config<R: Runtime>(
window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?; window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?;
Ok(plugin_manager Ok(plugin_manager
.get_http_authentication_config(&window, environment_chain, auth_name, values, request.id()) .get_http_authentication_config(&window, environment_chain, auth_name, values, model.id())
.await?) .await?)
} }
@@ -1006,6 +1038,35 @@ async fn cmd_save_response<R: Runtime>(
Ok(()) Ok(())
} }
#[tauri::command]
async fn cmd_send_folder<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
environment_id: Option<String>,
cookie_jar_id: Option<String>,
folder_id: &str,
) -> YaakResult<()> {
let requests = app_handle.db().list_http_requests_for_folder_recursive(folder_id)?;
for request in requests {
let app_handle = app_handle.clone();
let window = window.clone();
let environment_id = environment_id.clone();
let cookie_jar_id = cookie_jar_id.clone();
tokio::spawn(async move {
let _ = cmd_send_http_request(
app_handle,
window,
environment_id.as_deref(),
cookie_jar_id.as_deref(),
request,
)
.await;
});
}
Ok(())
}
#[tauri::command] #[tauri::command]
async fn cmd_send_http_request<R: Runtime>( async fn cmd_send_http_request<R: Runtime>(
app_handle: AppHandle<R>, app_handle: AppHandle<R>,
@@ -1101,7 +1162,7 @@ async fn cmd_install_plugin<R: Runtime>(
app_handle: AppHandle<R>, app_handle: AppHandle<R>,
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> YaakResult<Plugin> { ) -> YaakResult<Plugin> {
plugin_manager.add_plugin_by_dir(&PluginWindowContext::new(&window), &directory).await?; plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &directory).await?;
Ok(app_handle.db().upsert_plugin( Ok(app_handle.db().upsert_plugin(
&Plugin { &Plugin {
@@ -1117,7 +1178,7 @@ async fn cmd_install_plugin<R: Runtime>(
async fn cmd_create_grpc_request<R: Runtime>( async fn cmd_create_grpc_request<R: Runtime>(
workspace_id: &str, workspace_id: &str,
name: &str, name: &str,
sort_priority: f32, sort_priority: f64,
folder_id: Option<&str>, folder_id: Option<&str>,
app_handle: AppHandle<R>, app_handle: AppHandle<R>,
window: WebviewWindow<R>, window: WebviewWindow<R>,
@@ -1140,7 +1201,7 @@ async fn cmd_reload_plugins<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>, plugin_manager: State<'_, PluginManager>,
) -> YaakResult<()> { ) -> YaakResult<()> {
plugin_manager.initialize_all_plugins(&app_handle, &PluginWindowContext::new(&window)).await?; plugin_manager.initialize_all_plugins(&app_handle, &PluginContext::new(&window)).await?;
Ok(()) Ok(())
} }
@@ -1281,7 +1342,6 @@ pub fn run() {
.plugin(tauri_plugin_window_state::Builder::default().build()) .plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_updater::Builder::default().build())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
@@ -1291,6 +1351,7 @@ pub fn run() {
.plugin(yaak_crypto::init()) .plugin(yaak_crypto::init())
.plugin(yaak_fonts::init()) .plugin(yaak_fonts::init())
.plugin(yaak_git::init()) .plugin(yaak_git::init())
.plugin(yaak_http::init())
.plugin(yaak_ws::init()) .plugin(yaak_ws::init())
.plugin(yaak_sync::init()); .plugin(yaak_sync::init());
@@ -1299,6 +1360,11 @@ pub fn run() {
builder = builder.plugin(yaak_license::init()); builder = builder.plugin(yaak_license::init());
} }
#[cfg(feature = "updater")]
{
builder = builder.plugin(tauri_plugin_updater::Builder::default().build());
}
builder builder
.setup(|app| { .setup(|app| {
{ {
@@ -1381,7 +1447,9 @@ pub fn run() {
cmd_save_response, cmd_save_response,
cmd_send_ephemeral_request, cmd_send_ephemeral_request,
cmd_send_http_request, cmd_send_http_request,
cmd_template_functions, cmd_send_folder,
cmd_template_function_config,
cmd_template_function_summaries,
cmd_template_tokens_to_string, cmd_template_tokens_to_string,
// //
// //
@@ -1399,7 +1467,7 @@ pub fn run() {
let _ = window::create_main_window(app_handle, "/"); let _ = window::create_main_window(app_handle, "/");
let h = app_handle.clone(); let h = app_handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let info = history::store_launch_history(&h).await; let info = history::get_or_upsert_launch_info(&h);
debug!("Launched Yaak {:?}", info); debug!("Launched Yaak {:?}", info);
}); });
@@ -1499,7 +1567,30 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
// We might have recursive back-and-forth calls between app and plugin, so we don't // We might have recursive back-and-forth calls between app and plugin, so we don't
// want to block here // want to block here
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
plugin_events::handle_plugin_event(&app_handle, &event, &plugin).await; let ev = plugin_events::handle_plugin_event(&app_handle, &event, &plugin).await;
let ev = match ev {
Ok(Some(ev)) => ev,
Ok(None) => return,
Err(e) => {
warn!("Failed to handle plugin event: {e:?}");
let _ = app_handle.emit(
"show_toast",
InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: e.to_string(),
color: Some(Color::Danger),
icon: None,
timeout: Some(30000),
}),
);
return;
}
};
let plugin_manager: State<'_, PluginManager> = app_handle.state();
if let Err(e) = plugin_manager.reply(&event, &ev).await {
warn!("Failed to reply to plugin manager: {:?}", e)
}
}); });
} }
plugin_manager.unsubscribe(rx_id.as_str()).await; plugin_manager.unsubscribe(rx_id.as_str()).await;
@@ -1531,14 +1622,19 @@ async fn call_frontend<R: Runtime>(
v.to_owned() v.to_owned()
} }
fn get_window_from_window_context<R: Runtime>( fn get_window_from_plugin_context<R: Runtime>(
app_handle: &AppHandle<R>, app_handle: &AppHandle<R>,
window_context: &PluginWindowContext, plugin_context: &PluginContext,
) -> Option<WebviewWindow<R>> { ) -> Result<WebviewWindow<R>> {
let label = match window_context { let label = match &plugin_context.label {
PluginWindowContext::Label { label, .. } => label, Some(label) => label,
PluginWindowContext::None => { None => {
return app_handle.webview_windows().iter().next().map(|(_, w)| w.to_owned()); return app_handle
.webview_windows()
.iter()
.next()
.map(|(_, w)| w.to_owned())
.ok_or(GenericError("No windows open".to_string()));
} }
}; };
@@ -1548,10 +1644,10 @@ fn get_window_from_window_context<R: Runtime>(
.find_map(|(_, w)| if w.label() == label { Some(w.to_owned()) } else { None }); .find_map(|(_, w)| if w.label() == label { Some(w.to_owned()) } else { None });
if window.is_none() { if window.is_none() {
error!("Failed to find window by {window_context:?}"); error!("Failed to find window by {plugin_context:?}");
} }
window Ok(window.ok_or(GenericError(format!("Failed to find window for {}", label)))?)
} }
fn workspace_from_window<R: Runtime>(window: &WebviewWindow<R>) -> Option<Workspace> { fn workspace_from_window<R: Runtime>(window: &WebviewWindow<R>) -> Option<Workspace> {

View File

@@ -1,9 +1,9 @@
use std::time::SystemTime; use std::time::SystemTime;
use crate::error::Result; use crate::error::Result;
use crate::history::get_num_launches; use crate::history::get_or_upsert_launch_info;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use log::debug; use log::{debug, info};
use reqwest::Method; use reqwest::Method;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow}; use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
@@ -77,6 +77,13 @@ impl YaakNotifier {
self.last_check = SystemTime::now(); self.last_check = SystemTime::now();
if !app_handle.db().get_settings().check_notifications {
info!("Notifications are disabled. Skipping check.");
return Ok(());
}
debug!("Checking for notifications");
#[cfg(feature = "license")] #[cfg(feature = "license")]
let license_check = { let license_check = {
use yaak_license::{LicenseCheckStatus, check_license}; use yaak_license::{LicenseCheckStatus, check_license};
@@ -91,17 +98,17 @@ impl YaakNotifier {
#[cfg(not(feature = "license"))] #[cfg(not(feature = "license"))]
let license_check = "disabled".to_string(); let license_check = "disabled".to_string();
let settings = window.db().get_settings(); let launch_info = get_or_upsert_launch_info(app_handle);
let num_launches = get_num_launches(app_handle).await;
let info = app_handle.package_info().clone();
let req = yaak_api_client(app_handle)? let req = yaak_api_client(app_handle)?
.request(Method::GET, "https://notify.yaak.app/notifications") .request(Method::GET, "https://notify.yaak.app/notifications")
.query(&[ .query(&[
("version", info.version.to_string().as_str()), ("version", &launch_info.current_version),
("launches", num_launches.to_string().as_str()), ("version_prev", &launch_info.previous_version),
("installed", settings.created_at.format("%Y-%m-%d").to_string().as_str()), ("launches", &launch_info.num_launches.to_string()),
("installed", &launch_info.user_since.format("%Y-%m-%d").to_string()),
("license", &license_check), ("license", &license_check),
("platform", get_os()), ("updates", &get_updater_status(app_handle).to_string()),
("platform", &get_os().to_string()),
]); ]);
let resp = req.send().await?; let resp = req.send().await?;
if resp.status() != 200 { if resp.status() != 200 {
@@ -131,3 +138,33 @@ async fn get_kv<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Vec<String>> {
Some(v) => Ok(serde_json::from_str(&v.value)?), Some(v) => Ok(serde_json::from_str(&v.value)?),
} }
} }
#[allow(unused)]
fn get_updater_status<R: Runtime>(app_handle: &AppHandle<R>) -> &'static str {
#[cfg(not(feature = "updater"))]
{
// Updater is not enabled as a Rust feature
return "missing";
}
#[cfg(all(feature = "updater", target_os = "linux"))]
{
let settings = app_handle.db().get_settings();
if !settings.autoupdate {
// Updates are explicitly disabled
"disabled"
} else if std::env::var("APPIMAGE").is_err() {
// Updates are enabled, but unsupported
"unsupported"
} else {
// Updates are enabled and supported
"enabled"
}
}
#[cfg(all(feature = "updater", not(target_os = "linux")))]
{
let settings = app_handle.db().get_settings();
if settings.autoupdate { "enabled" } else { "disabled" }
}
}

View File

@@ -1,26 +1,29 @@
use crate::http_request::send_http_request; use crate::error::Result;
use crate::http_request::send_http_request_with_context;
use crate::render::{render_grpc_request, render_http_request, render_json_value}; use crate::render::{render_grpc_request, render_http_request, render_json_value};
use crate::window::{CreateWindowConfig, create_window}; use crate::window::{CreateWindowConfig, create_window};
use crate::{ use crate::{
call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_window_context, call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_plugin_context,
workspace_from_window, workspace_from_window,
}; };
use chrono::Utc; use chrono::Utc;
use cookie::Cookie; use cookie::Cookie;
use log::{error, warn}; use log::error;
use tauri::{AppHandle, Emitter, Manager, Runtime, State}; use tauri::{AppHandle, Emitter, Manager, Runtime};
use tauri_plugin_clipboard_manager::ClipboardExt; use tauri_plugin_clipboard_manager::ClipboardExt;
use yaak_common::window::WorkspaceWindowTrait;
use yaak_models::models::{HttpResponse, Plugin}; use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::queries::any_request::AnyRequest;
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
use yaak_plugins::error::Error::PluginErr;
use yaak_plugins::events::{ use yaak_plugins::events::{
Color, DeleteKeyValueResponse, EmptyPayload, ErrorResponse, FindHttpResponsesResponse, Color, DeleteKeyValueResponse, EmptyPayload, ErrorResponse, FindHttpResponsesResponse,
GetCookieValueResponse, GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, GetCookieValueResponse, GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent,
InternalEventPayload, ListCookieNamesResponse, PluginWindowContext, RenderGrpcRequestResponse, InternalEventPayload, ListCookieNamesResponse, RenderGrpcRequestResponse,
RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest, RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest,
TemplateRenderResponse, WindowNavigateEvent, TemplateRenderResponse, WindowInfoResponse, WindowNavigateEvent,
}; };
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle; use yaak_plugins::plugin_handle::PluginHandle;
use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_plugins::template_callback::PluginTemplateCallback;
use yaak_templates::{RenderErrorBehavior, RenderOptions}; use yaak_templates::{RenderErrorBehavior, RenderOptions};
@@ -29,114 +32,114 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
app_handle: &AppHandle<R>, app_handle: &AppHandle<R>,
event: &InternalEvent, event: &InternalEvent,
plugin_handle: &PluginHandle, plugin_handle: &PluginHandle,
) { ) -> Result<Option<InternalEventPayload>> {
// debug!("Got event to app {event:?}"); // log::debug!("Got event to app {event:?}");
let window_context = event.window_context.to_owned(); let plugin_context = event.context.to_owned();
let response_event: Option<InternalEventPayload> = match event.clone().payload { match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => { InternalEventPayload::CopyTextRequest(req) => {
app_handle app_handle.clipboard().write_text(req.text.as_str())?;
.clipboard() Ok(Some(InternalEventPayload::CopyTextResponse(EmptyPayload {})))
.write_text(req.text.as_str())
.expect("Failed to write text to clipboard");
Some(InternalEventPayload::CopyTextResponse(EmptyPayload {}))
} }
InternalEventPayload::ShowToastRequest(req) => { InternalEventPayload::ShowToastRequest(req) => {
match window_context { match plugin_context.label {
PluginWindowContext::Label { label, .. } => app_handle Some(label) => app_handle.emit_to(label, "show_toast", req)?,
.emit_to(label, "show_toast", req) None => app_handle.emit("show_toast", req)?,
.expect("Failed to emit show_toast to window"),
_ => app_handle.emit("show_toast", req).expect("Failed to emit show_toast"),
}; };
Some(InternalEventPayload::ShowToastResponse(EmptyPayload {})) Ok(Some(InternalEventPayload::ShowToastResponse(EmptyPayload {})))
} }
InternalEventPayload::PromptTextRequest(_) => { InternalEventPayload::PromptTextRequest(_) => {
let window = get_window_from_window_context(app_handle, &window_context) let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
.expect("Failed to find window for render"); Ok(call_frontend(&window, event).await)
call_frontend(&window, event).await
} }
InternalEventPayload::FindHttpResponsesRequest(req) => { InternalEventPayload::FindHttpResponsesRequest(req) => {
let http_responses = app_handle let http_responses = app_handle
.db() .db()
.list_http_responses_for_request(&req.request_id, req.limit.map(|l| l as u64)) .list_http_responses_for_request(&req.request_id, req.limit.map(|l| l as u64))
.unwrap_or_default(); .unwrap_or_default();
Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse { Ok(Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses, http_responses,
})) })))
} }
InternalEventPayload::GetHttpRequestByIdRequest(req) => { InternalEventPayload::GetHttpRequestByIdRequest(req) => {
let http_request = app_handle.db().get_http_request(&req.id).ok(); let http_request = app_handle.db().get_http_request(&req.id).ok();
Some(InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse { Ok(Some(InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse {
http_request, http_request,
})) })))
} }
InternalEventPayload::RenderGrpcRequestRequest(req) => { InternalEventPayload::RenderGrpcRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context) let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
.expect("Failed to find window for render grpc request");
let workspace = let workspace =
workspace_from_window(&window).expect("Failed to get workspace_id from window URL"); workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
let environment_id = environment_from_window(&window).map(|e| e.id); let environment_id = environment_from_window(&window).map(|e| e.id);
let environment_chain = window let environment_chain = window.db().resolve_environments(
.db() &workspace.id,
.resolve_environments(&workspace.id, None, environment_id.as_deref()) req.grpc_request.folder_id.as_deref(),
.expect("Failed to resolve environments"); environment_id.as_deref(),
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose); )?;
let cb = PluginTemplateCallback::new(app_handle, &plugin_context, req.purpose);
let opt = RenderOptions { let opt = RenderOptions {
error_behavior: RenderErrorBehavior::Throw, error_behavior: RenderErrorBehavior::Throw,
}; };
let grpc_request = render_grpc_request(&req.grpc_request, environment_chain, &cb, &opt) let grpc_request =
.await render_grpc_request(&req.grpc_request, environment_chain, &cb, &opt).await?;
.expect("Failed to render grpc request"); Ok(Some(InternalEventPayload::RenderGrpcRequestResponse(RenderGrpcRequestResponse {
Some(InternalEventPayload::RenderGrpcRequestResponse(RenderGrpcRequestResponse {
grpc_request, grpc_request,
})) })))
} }
InternalEventPayload::RenderHttpRequestRequest(req) => { InternalEventPayload::RenderHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context) let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
.expect("Failed to find window for render http request");
let workspace = let workspace =
workspace_from_window(&window).expect("Failed to get workspace_id from window URL"); workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
let environment_id = environment_from_window(&window).map(|e| e.id); let environment_id = environment_from_window(&window).map(|e| e.id);
let environment_chain = window let environment_chain = window.db().resolve_environments(
.db() &workspace.id,
.resolve_environments(&workspace.id, None, environment_id.as_deref()) req.http_request.folder_id.as_deref(),
.expect("Failed to resolve environments"); environment_id.as_deref(),
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose); )?;
let cb = PluginTemplateCallback::new(app_handle, &plugin_context, req.purpose);
let opt = &RenderOptions { let opt = &RenderOptions {
error_behavior: RenderErrorBehavior::Throw, error_behavior: RenderErrorBehavior::Throw,
}; };
let http_request = render_http_request(&req.http_request, environment_chain, &cb, &opt) let http_request =
.await render_http_request(&req.http_request, environment_chain, &cb, &opt).await?;
.expect("Failed to render http request"); Ok(Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request, http_request,
})) })))
} }
InternalEventPayload::TemplateRenderRequest(req) => { InternalEventPayload::TemplateRenderRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context) let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
.expect("Failed to find window for render");
let workspace = let workspace =
workspace_from_window(&window).expect("Failed to get workspace_id from window URL"); workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
let environment_id = environment_from_window(&window).map(|e| e.id); let environment_id = environment_from_window(&window).map(|e| e.id);
let environment_chain = window let folder_id = if let Some(id) = window.request_id() {
.db() match window.db().get_any_request(&id) {
.resolve_environments(&workspace.id, None, environment_id.as_deref()) Ok(AnyRequest::HttpRequest(r)) => r.folder_id,
.expect("Failed to resolve environments"); Ok(AnyRequest::GrpcRequest(r)) => r.folder_id,
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose); Ok(AnyRequest::WebsocketRequest(r)) => r.folder_id,
Err(_) => None,
}
} else {
None
};
let environment_chain = window.db().resolve_environments(
&workspace.id,
folder_id.as_deref(),
environment_id.as_deref(),
)?;
let cb = PluginTemplateCallback::new(app_handle, &plugin_context, req.purpose);
let opt = RenderOptions { let opt = RenderOptions {
error_behavior: RenderErrorBehavior::Throw, error_behavior: RenderErrorBehavior::Throw,
}; };
let data = render_json_value(req.data, environment_chain, &cb, &opt) let data = render_json_value(req.data, environment_chain, &cb, &opt).await?;
.await Ok(Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data })))
.expect("Failed to render template");
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
} }
InternalEventPayload::ErrorResponse(resp) => { InternalEventPayload::ErrorResponse(resp) => {
error!("Plugin error: {}: {:?}", resp.error, resp); error!("Plugin error: {}: {:?}", resp.error, resp);
let toast_event = plugin_handle.build_event_to_send( let toast_event = plugin_handle.build_event_to_send(
&window_context, &plugin_context,
&InternalEventPayload::ShowToastRequest(ShowToastRequest { &InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!( message: format!(
"Plugin error from {}: {}", "Plugin error from {}: {}",
@@ -144,16 +147,15 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
resp.error resp.error
), ),
color: Some(Color::Danger), color: Some(Color::Danger),
timeout: None, timeout: Some(30000),
..Default::default() ..Default::default()
}), }),
None, None,
); );
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await; Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await
None
} }
InternalEventPayload::ReloadResponse(req) => { InternalEventPayload::ReloadResponse(req) => {
let plugins = app_handle.db().list_plugins().unwrap(); let plugins = app_handle.db().list_plugins()?;
for plugin in plugins { for plugin in plugins {
if plugin.directory != plugin_handle.dir { if plugin.directory != plugin_handle.dir {
continue; continue;
@@ -163,13 +165,13 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
updated_at: Utc::now().naive_utc(), // TODO: Add reloaded_at field to use instead updated_at: Utc::now().naive_utc(), // TODO: Add reloaded_at field to use instead
..plugin ..plugin
}; };
app_handle.db().upsert_plugin(&new_plugin, &UpdateSource::Plugin).unwrap(); app_handle.db().upsert_plugin(&new_plugin, &UpdateSource::Plugin)?;
} }
if !req.silent { if !req.silent {
let info = plugin_handle.info(); let info = plugin_handle.info();
let toast_event = plugin_handle.build_event_to_send( let toast_event = plugin_handle.build_event_to_send(
&window_context, &plugin_context,
&InternalEventPayload::ShowToastRequest(ShowToastRequest { &InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!("Reloaded plugin {}@{}", info.name, info.version), message: format!("Reloaded plugin {}@{}", info.name, info.version),
icon: Some(Icon::Info), icon: Some(Icon::Info),
@@ -178,13 +180,13 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
}), }),
None, None,
); );
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await; Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await
} else {
Ok(None)
} }
None
} }
InternalEventPayload::SendHttpRequestRequest(req) => { InternalEventPayload::SendHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context) let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
.expect("Failed to find window for sending HTTP request");
let mut http_request = req.http_request; let mut http_request = req.http_request;
let workspace = let workspace =
workspace_from_window(&window).expect("Failed to get workspace_id from window URL"); workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
@@ -198,37 +200,30 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
let http_response = if http_request.id.is_empty() { let http_response = if http_request.id.is_empty() {
HttpResponse::default() HttpResponse::default()
} else { } else {
window window.db().upsert_http_response(
.db() &HttpResponse {
.upsert_http_response( request_id: http_request.id.clone(),
&HttpResponse { workspace_id: http_request.workspace_id.clone(),
request_id: http_request.id.clone(), ..Default::default()
workspace_id: http_request.workspace_id.clone(), },
..Default::default() &UpdateSource::Plugin,
}, )?
&UpdateSource::Plugin,
)
.unwrap()
}; };
let result = send_http_request( let http_response = send_http_request_with_context(
&window, &window,
&http_request, &http_request,
&http_response, &http_response,
environment, environment,
cookie_jar, cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel &mut tokio::sync::watch::channel(false).1, // No-op cancel channel
&plugin_context,
) )
.await; .await?;
let http_response = match result { Ok(Some(InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse {
Ok(r) => r,
Err(_e) => return,
};
Some(InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse {
http_response, http_response,
})) })))
} }
InternalEventPayload::OpenWindowRequest(req) => { InternalEventPayload::OpenWindowRequest(req) => {
let (navigation_tx, mut navigation_rx) = tokio::sync::mpsc::channel(128); let (navigation_tx, mut navigation_rx) = tokio::sync::mpsc::channel(128);
@@ -245,25 +240,25 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
}; };
if let Err(e) = create_window(app_handle, win_config) { if let Err(e) = create_window(app_handle, win_config) {
let error_event = plugin_handle.build_event_to_send( let error_event = plugin_handle.build_event_to_send(
&window_context, &plugin_context,
&InternalEventPayload::ErrorResponse(ErrorResponse { &InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Failed to create window: {:?}", e), error: format!("Failed to create window: {:?}", e),
}), }),
None, None,
); );
Box::pin(handle_plugin_event(app_handle, &error_event, plugin_handle)).await; return Box::pin(handle_plugin_event(app_handle, &error_event, plugin_handle))
return; .await;
} }
{ {
let event_id = event.id.clone(); let event_id = event.id.clone();
let plugin_handle = plugin_handle.clone(); let plugin_handle = plugin_handle.clone();
let window_context = window_context.clone(); let plugin_context = plugin_context.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
while let Some(url) = navigation_rx.recv().await { while let Some(url) = navigation_rx.recv().await {
let url = url.to_string(); let url = url.to_string();
let event_to_send = plugin_handle.build_event_to_send( let event_to_send = plugin_handle.build_event_to_send(
&window_context, // NOTE: Sending existing context on purpose here &plugin_context, // NOTE: Sending existing context on purpose here
&InternalEventPayload::WindowNavigateEvent(WindowNavigateEvent { url }), &InternalEventPayload::WindowNavigateEvent(WindowNavigateEvent { url }),
Some(event_id.clone()), Some(event_id.clone()),
); );
@@ -275,11 +270,11 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
{ {
let event_id = event.id.clone(); let event_id = event.id.clone();
let plugin_handle = plugin_handle.clone(); let plugin_handle = plugin_handle.clone();
let window_context = window_context.clone(); let plugin_context = plugin_context.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
while let Some(_) = close_rx.recv().await { while let Some(_) = close_rx.recv().await {
let event_to_send = plugin_handle.build_event_to_send( let event_to_send = plugin_handle.build_event_to_send(
&window_context, &plugin_context,
&InternalEventPayload::WindowCloseEvent, &InternalEventPayload::WindowCloseEvent,
Some(event_id.clone()), Some(event_id.clone()),
); );
@@ -288,32 +283,33 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
}); });
} }
None Ok(None)
} }
InternalEventPayload::CloseWindowRequest(req) => { InternalEventPayload::CloseWindowRequest(req) => {
if let Some(window) = app_handle.webview_windows().get(&req.label) { if let Some(window) = app_handle.webview_windows().get(&req.label) {
window.close().expect("Failed to close window"); window.close()?;
} }
None Ok(None)
} }
InternalEventPayload::SetKeyValueRequest(req) => { InternalEventPayload::SetKeyValueRequest(req) => {
let name = plugin_handle.info().name; let name = plugin_handle.info().name;
app_handle.db().set_plugin_key_value(&name, &req.key, &req.value); app_handle.db().set_plugin_key_value(&name, &req.key, &req.value);
Some(InternalEventPayload::SetKeyValueResponse(SetKeyValueResponse {})) Ok(Some(InternalEventPayload::SetKeyValueResponse(SetKeyValueResponse {})))
} }
InternalEventPayload::GetKeyValueRequest(req) => { InternalEventPayload::GetKeyValueRequest(req) => {
let name = plugin_handle.info().name; let name = plugin_handle.info().name;
let value = app_handle.db().get_plugin_key_value(&name, &req.key).map(|v| v.value); let value = app_handle.db().get_plugin_key_value(&name, &req.key).map(|v| v.value);
Some(InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value })) Ok(Some(InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value })))
} }
InternalEventPayload::DeleteKeyValueRequest(req) => { InternalEventPayload::DeleteKeyValueRequest(req) => {
let name = plugin_handle.info().name; let name = plugin_handle.info().name;
let deleted = app_handle.db().delete_plugin_key_value(&name, &req.key).unwrap(); let deleted = app_handle.db().delete_plugin_key_value(&name, &req.key)?;
Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted })) Ok(Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse {
deleted,
})))
} }
InternalEventPayload::ListCookieNamesRequest(_req) => { InternalEventPayload::ListCookieNamesRequest(_req) => {
let window = get_window_from_window_context(app_handle, &window_context) let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
.expect("Failed to find window for listing cookies");
let names = match cookie_jar_from_window(&window) { let names = match cookie_jar_from_window(&window) {
None => Vec::new(), None => Vec::new(),
Some(j) => j Some(j) => j
@@ -322,11 +318,12 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
.filter_map(|c| Cookie::parse(c.raw_cookie).ok().map(|c| c.name().to_string())) .filter_map(|c| Cookie::parse(c.raw_cookie).ok().map(|c| c.name().to_string()))
.collect(), .collect(),
}; };
Some(InternalEventPayload::ListCookieNamesResponse(ListCookieNamesResponse { names })) Ok(Some(InternalEventPayload::ListCookieNamesResponse(ListCookieNamesResponse {
names,
})))
} }
InternalEventPayload::GetCookieValueRequest(req) => { InternalEventPayload::GetCookieValueRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context) let window = get_window_from_plugin_context(app_handle, &plugin_context)?;
.expect("Failed to find window for listing cookies");
let value = match cookie_jar_from_window(&window) { let value = match cookie_jar_from_window(&window) {
None => None, None => None,
Some(j) => j.cookies.into_iter().find_map(|c| match Cookie::parse(c.raw_cookie) { Some(j) => j.cookies.into_iter().find_map(|c| match Cookie::parse(c.raw_cookie) {
@@ -336,15 +333,31 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
_ => None, _ => None,
}), }),
}; };
Some(InternalEventPayload::GetCookieValueResponse(GetCookieValueResponse { value })) Ok(Some(InternalEventPayload::GetCookieValueResponse(GetCookieValueResponse { value })))
} }
_ => None, InternalEventPayload::WindowInfoRequest(req) => {
}; let w = app_handle
.get_webview_window(&req.label)
.ok_or(PluginErr(format!("Failed to find window for {}", req.label)))?;
if let Some(e) = response_event { // Actually look up the data so we never return an invalid ID
let plugin_manager: State<'_, PluginManager> = app_handle.state(); let environment_id = environment_from_window(&w).map(|m| m.id);
if let Err(e) = plugin_manager.reply(&event, &e).await { let workspace_id = workspace_from_window(&w).map(|m| m.id);
warn!("Failed to reply to plugin manager: {:?}", e) let request_id =
match app_handle.db().get_any_request(&w.request_id().unwrap_or_default()) {
Ok(AnyRequest::HttpRequest(r)) => Some(r.id),
Ok(AnyRequest::WebsocketRequest(r)) => Some(r.id),
Ok(AnyRequest::GrpcRequest(r)) => Some(r.id),
Err(_) => None,
};
Ok(Some(InternalEventPayload::WindowInfoResponse(WindowInfoResponse {
label: w.label().to_string(),
request_id,
workspace_id,
environment_id,
})))
} }
_ => Ok(None),
} }
} }

View File

@@ -1,11 +1,11 @@
use serde_json::Value; use serde_json::Value;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use yaak_http::apply_path_placeholders; use yaak_http::path_placeholders::apply_path_placeholders;
use yaak_models::models::{ use yaak_models::models::{
Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter, Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter,
}; };
use yaak_models::render::make_vars_hashmap; use yaak_models::render::make_vars_hashmap;
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw}; use yaak_templates::{parse_and_render, render_json_value_raw, RenderOptions, TemplateCallback};
pub async fn render_template<T: TemplateCallback>( pub async fn render_template<T: TemplateCallback>(
template: &str, template: &str,
@@ -70,6 +70,9 @@ pub async fn render_http_request<T: TemplateCallback>(
let mut url_parameters = Vec::new(); let mut url_parameters = Vec::new();
for p in r.url_parameters.clone() { for p in r.url_parameters.clone() {
if !p.enabled {
continue;
}
url_parameters.push(HttpUrlParameter { url_parameters.push(HttpUrlParameter {
enabled: p.enabled, enabled: p.enabled,
name: parse_and_render(p.name.as_str(), vars, cb, &opt).await?, name: parse_and_render(p.name.as_str(), vars, cb, &opt).await?,
@@ -80,6 +83,9 @@ pub async fn render_http_request<T: TemplateCallback>(
let mut headers = Vec::new(); let mut headers = Vec::new();
for p in r.headers.clone() { for p in r.headers.clone() {
if !p.enabled {
continue;
}
headers.push(HttpRequestHeader { headers.push(HttpRequestHeader {
enabled: p.enabled, enabled: p.enabled,
name: parse_and_render(p.name.as_str(), vars, cb, &opt).await?, name: parse_and_render(p.name.as_str(), vars, cb, &opt).await?,

View File

@@ -1,12 +1,12 @@
use crate::error::Result;
use crate::window_menu::app_menu; use crate::window_menu::app_menu;
use log::{info, warn}; use log::{info, warn};
use rand::random; use rand::random;
use tauri::{ use tauri::{
AppHandle, Emitter, LogicalSize, Manager, Runtime, WebviewUrl, WebviewWindow, WindowEvent, AppHandle, Emitter, LogicalSize, Manager, PhysicalSize, Runtime, WebviewUrl, WebviewWindow, WindowEvent
}; };
use tauri_plugin_opener::OpenerExt; use tauri_plugin_opener::OpenerExt;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::error::Result;
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0; const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0; const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
@@ -49,7 +49,6 @@ pub(crate) fn create_window<R: Runtime>(
.resizable(true) .resizable(true)
.visible(false) // To prevent theme flashing, the frontend code calls show() immediately after configuring the theme .visible(false) // To prevent theme flashing, the frontend code calls show() immediately after configuring the theme
.fullscreen(false) .fullscreen(false)
.disable_drag_drop_handler() // Required for frontend Dnd on windows
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT); .min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
if let Some(key) = config.data_dir_key { if let Some(key) = config.data_dir_key {
@@ -161,6 +160,11 @@ pub(crate) fn create_window<R: Runtime>(
"dev.reset_size" => webview_window "dev.reset_size" => webview_window
.set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)) .set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT))
.unwrap(), .unwrap(),
"dev.reset_size_record" => {
let width = webview_window.outer_size().unwrap().width;
let height = width * 9 / 16;
webview_window.set_size(PhysicalSize::new(width, height)).unwrap()
}
"dev.refresh" => webview_window.eval("location.reload()").unwrap(), "dev.refresh" => webview_window.eval("location.reload()").unwrap(),
"dev.generate_theme_css" => { "dev.generate_theme_css" => {
w.emit("generate_theme_css", true).unwrap(); w.emit("generate_theme_css", true).unwrap();
@@ -216,10 +220,10 @@ pub(crate) fn create_child_window(
) -> Result<WebviewWindow> { ) -> Result<WebviewWindow> {
let app_handle = parent_window.app_handle(); let app_handle = parent_window.app_handle();
let label = format!("{OTHER_WINDOW_PREFIX}_{label}"); let label = format!("{OTHER_WINDOW_PREFIX}_{label}");
let scale_factor = parent_window.scale_factor().unwrap(); let scale_factor = parent_window.scale_factor()?;
let current_pos = parent_window.inner_position().unwrap().to_logical::<f64>(scale_factor); let current_pos = parent_window.inner_position()?.to_logical::<f64>(scale_factor);
let current_size = parent_window.inner_size().unwrap().to_logical::<f64>(scale_factor); let current_size = parent_window.inner_size()?.to_logical::<f64>(scale_factor);
// Position the new window in the middle of the parent // Position the new window in the middle of the parent
let position = ( let position = (

View File

@@ -143,6 +143,8 @@ pub fn app_menu<R: Runtime>(app_handle: &AppHandle<R>) -> tauri::Result<Menu<R>>
.build(app_handle)?, .build(app_handle)?,
&MenuItemBuilder::with_id("dev.reset_size".to_string(), "Reset Size") &MenuItemBuilder::with_id("dev.reset_size".to_string(), "Reset Size")
.build(app_handle)?, .build(app_handle)?,
&MenuItemBuilder::with_id("dev.reset_size_record".to_string(), "Reset Size 16x9")
.build(app_handle)?,
&MenuItemBuilder::with_id( &MenuItemBuilder::with_id(
"dev.generate_theme_css".to_string(), "dev.generate_theme_css".to_string(),
"Generate Theme CSS", "Generate Theme CSS",

View File

@@ -30,7 +30,7 @@
} }
}, },
"bundle": { "bundle": {
"createUpdaterArtifacts": "v1Compatible", "createUpdaterArtifacts": true,
"windows": { "windows": {
"signCommand": "trusted-signing-cli -e https://eus.codesigning.azure.net/ -a Yaak -c yaakapp %1" "signCommand": "trusted-signing-cli -e https://eus.codesigning.azure.net/ -a Yaak -c yaakapp %1"
} }

View File

@@ -1,4 +1,4 @@
pub mod window; pub mod window;
pub mod platform; pub mod platform;
pub mod api_client; pub mod api_client;
pub mod error; pub mod error;

View File

@@ -5,6 +5,7 @@ pub trait WorkspaceWindowTrait {
fn workspace_id(&self) -> Option<String>; fn workspace_id(&self) -> Option<String>;
fn cookie_jar_id(&self) -> Option<String>; fn cookie_jar_id(&self) -> Option<String>;
fn environment_id(&self) -> Option<String>; fn environment_id(&self) -> Option<String>;
fn request_id(&self) -> Option<String>;
} }
impl<R: Runtime> WorkspaceWindowTrait for WebviewWindow<R> { impl<R: Runtime> WorkspaceWindowTrait for WebviewWindow<R> {
@@ -28,4 +29,10 @@ impl<R: Runtime> WorkspaceWindowTrait for WebviewWindow<R> {
let mut query_pairs = url.query_pairs(); let mut query_pairs = url.query_pairs();
query_pairs.find(|(k, _v)| k == "environment_id").map(|(_k, v)| v.to_string()) query_pairs.find(|(k, _v)| k == "environment_id").map(|(_k, v)| v.to_string())
} }
fn request_id(&self) -> Option<String> {
let url = self.url().unwrap();
let mut query_pairs = url.query_pairs();
query_pairs.find(|(k, _v)| k == "request_id").map(|(_k, v)| v.to_string())
}
} }

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