Compare commits

...

238 Commits

Author SHA1 Message Date
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
Gregory Schier
a00b4ae232 Always open new window when clicking current workspace:
https://feedback.yaak.app/p/dont-trigger-workspace-change-when-clicking-same-workspace
2025-10-04 06:47:50 -07:00
Gregory Schier
998b5cf78a Add RenderOptions and RenderErrorBehavior to ensure auth UI still loads with missing variables 2025-10-04 06:29:29 -07:00
Gregory Schier
b4deae6e8d Batch insert environments last, to handle folder case 2025-10-04 05:48:08 -07:00
Gregory Schier
87fdf17010 Fix plugin refresh toasts having no timeout 2025-10-04 05:47:39 -07:00
Gregory Schier
c6975a9e8b Allow toast interaction when dialog is open 2025-10-04 05:47:16 -07:00
Gregory Schier
b44ac55bc2 Fix broken environment migration 2025-10-04 05:47:00 -07:00
Gregory Schier
9c65c95ba9 Fix batch import potentially creating a useless base environment 2025-10-04 05:46:42 -07:00
Gregory Schier
7beb9f4e69 Merge remote-tracking branch 'origin/main' 2025-10-04 05:46:13 -07:00
Gregory Schier
dbecd74f46 Allow selecting confirm text 2025-10-04 05:46:06 -07:00
Gregory Schier
6826ee1672 Revise README for clarity and updated features
Updated the README to enhance clarity and organization, including feature descriptions and removing outdated content.
2025-10-03 10:29:19 -07:00
Gregory Schier
a12ae7ef56 Update README 2025-10-03 09:53:40 -07:00
gschier
dbc100409d Deploying to main from @ mountain-loop/yaak@6b87cd9655 🚀 2025-10-03 16:52:47 +00:00
Gregory Schier
6b87cd9655 Merge remote-tracking branch 'origin/main' 2025-10-03 09:52:31 -07:00
Gregory Schier
7ce2cdc9cc Update tiers 2025-10-03 09:52:26 -07:00
gschier
1f4e38b7a7 Deploying to main from @ mountain-loop/yaak@0013a0797b 🚀 2025-10-03 16:50:09 +00:00
Gregory Schier
0013a0797b Merge remote-tracking branch 'origin/main' 2025-10-03 09:49:45 -07:00
Gregory Schier
5e9b14dc0b Update workflow 2025-10-03 09:49:39 -07:00
gschier
b7cfb0db13 Deploying to main from @ mountain-loop/yaak@8948bfbf45 🚀 2025-10-03 16:46:35 +00:00
Gregory Schier
8948bfbf45 Merge remote-tracking branch 'origin/main' 2025-10-03 09:46:19 -07:00
Gregory Schier
4218e90bf4 Set active-only to true in sponsors workflow 2025-10-03 09:44:56 -07:00
gschier
2172d7ac60 Deploying to main from @ mountain-loop/yaak@5e45cb4908 🚀 2025-10-03 16:42:45 +00:00
Gregory Schier
5e45cb4908 Update sponsors workflow to use SPONSORS_PAT secret 2025-10-03 09:42:21 -07:00
Gregory Schier
d662883fdd Update README 2025-10-03 09:41:17 -07:00
Gregory Schier
f83f3d4682 Refine Yaak description in README
Updated the description of Yaak in the README.
2025-10-03 09:36:22 -07:00
gschier
e03c745093 Deploying to main from @ mountain-loop/yaak@73b9d699ed 🚀 2025-10-03 16:35:10 +00:00
Gregory Schier
73b9d699ed Merge remote-tracking branch 'origin/main' 2025-10-03 09:34:35 -07:00
Gregory Schier
5a7b9aba2f Sponsors workflow 2025-10-03 09:34:22 -07:00
Gregory Schier
cf433b26a5 Revise sponsor section in README.md
Updated sponsor section in README.md to include new sponsor images and links.
2025-10-03 09:32:36 -07:00
gschier
573035b17d Deploying to main from @ mountain-loop/yaak@3844fec968 🚀 2025-10-03 16:31:42 +00:00
Gregory Schier
a267c0c53f Update README 2025-10-03 09:31:21 -07:00
gschier
328563f4e6 Deploying to main from @ mountain-loop/yaak@3844fec968 🚀 2025-10-03 16:30:09 +00:00
Gregory Schier
3844fec968 Merge remote-tracking branch 'origin/main' 2025-10-03 09:29:35 -07:00
Gregory Schier
8557a2477b Sponsors workflow 2025-10-03 09:29:31 -07:00
Gregory Schier
d02519ab74 Update README header 2025-10-03 09:19:17 -07:00
Gregory Schier
1a1751c23e Fix window path issue 2025-10-02 08:25:00 -07:00
Gregory Schier
17de0678b0 Remove unused import from window.rs 2025-10-02 08:05:07 -07:00
Gregory Schier
20bb89de33 Try fix oauth window creation 2025-10-02 07:45:50 -07:00
Gregory Schier
8a634b1056 Add back environment.base (#260) 2025-10-02 06:04:27 -07:00
Gregory Schier
57f231ca00 Add trial status to links 2025-10-01 21:14:26 -07:00
Gregory Schier
cb1c0e4d8c Fix ref 2025-10-01 21:07:44 -07:00
Gregory Schier
2152cf87d7 Tweak license badge and fix keyring dep 2025-10-01 21:01:27 -07:00
Gregory Schier
8662b230e7 Oops, actually fix 2025-10-01 16:54:05 -07:00
Gregory Schier
3a8a6484c7 Fix release windows signing 2025-10-01 16:42:38 -07:00
Gregory Schier
f92594a16d Fix release tauri config 2025-10-01 10:51:26 -07:00
Gregory Schier
7969fcb76c Alias keyring function 2025-10-01 10:22:06 -07:00
Gregory Schier
eafefb1894 Fix setting 2025-10-01 09:44:18 -07:00
Gregory Schier
9a94a15c82 Integrated update experience (#259) 2025-10-01 09:36:36 -07:00
Gregory Schier
757d28c235 License and updater Cargo features (#258) 2025-09-29 22:08:05 -07:00
Gregory Schier
6c79c1ef3f Rework licensing flows to be more friendly 2025-09-29 15:40:15 -07:00
Gregory Schier
7262eccac5 Fix keyring errors 2025-09-29 10:53:20 -07:00
Gregory Schier
4989a5f759 Add back cmd palette icon 2025-09-29 09:39:15 -07:00
Gregory Schier
0b0b05d29c Add keyring template function 2025-09-29 08:56:24 -07:00
Gregory Schier
b3d6d87bee Delete duplicate folder environments on upsert 2025-09-29 07:48:07 -07:00
Gregory Schier
6abbdc8726 Filter out current variable from autocomplete and show sub environment variables in base environment autocomplete 2025-09-29 06:57:04 -07:00
Gregory Schier
b9613591f8 Update resource links in README.md 2025-09-28 15:38:14 -07:00
Gregory Schier
eb555989ac Force grpcurl to posix paths 2025-09-25 08:40:57 -07:00
Gregory Schier
b77f1375fd Fix test with timezone 2025-09-25 08:03:07 -07:00
Gregory Schier
3c438b3da7 Add cmdctrl+Backspace for request delete 2025-09-25 07:28:01 -07:00
Gregory Schier
df15543c80 Explicitly set the request layout (#257) 2025-09-25 07:23:52 -07:00
Gregory Schier
73ad86c6b9 Fix workspace settings scroll with long description 2025-09-25 07:22:42 -07:00
Gregory Schier
615de8b3cc Update importers for folder environment and fix tests 2025-09-25 07:12:50 -07:00
Gregory Schier
2418bd0672 Update README.md 2025-09-24 11:03:23 -07:00
Gregory Schier
b3414ee60f Fix ephemeral response body reading 2025-09-22 14:07:25 -07:00
Gregory Schier
8fe50959b9 Add migrate for base environment to sync logic 2025-09-22 11:15:32 -07:00
Gregory Schier
523e7dcf16 Add bootstrap to release script (to fix lint) 2025-09-22 08:57:05 -07:00
Gregory Schier
7951f3a7bd Tweak light theme, high contrast themes, and fix env null reference 2025-09-22 08:36:40 -07:00
Gregory Schier
c6666b9623 Update tauri and signing deps to try fixing windows signing 2025-09-21 08:40:43 -07:00
Gregory Schier
fa98351e30 Fix lint 2025-09-21 08:04:47 -07:00
Gregory Schier
3c8be3f5b9 Gen models 2025-09-21 08:01:49 -07:00
Gregory Schier
eb3d1c409b Merge pull request #256
* Update environment model to get ready for request/folder environments

* Folder environments in UI

* Folder environments working

* Tweaks and fixes

* Tweak environment encryption UX

* Tweak environment encryption UX

* Address comments

* Update fn name

* Add tsc back to lint rules

* Update src-web/components/EnvironmentEditor.tsx

* Merge remote-tracking branch 'origin/folder-environments' into folder…
2025-09-21 07:54:26 -07:00
Jhonatan Matías
46b049c72b Fix Typos (#255) 2025-09-18 10:40:32 -07:00
Hao Xiang
fec64b5c02 fix http response load when filter (#251) 2025-09-16 13:01:00 -07:00
dependabot[bot]
8c3ed60579 Bump vite-plugin-static-copy from 3.1.1 to 3.1.2 (#252)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-16 11:20:34 -07:00
moshyfawn
907e09a417 [Plugins] [CopyAsCURL] [Auth] [JWT] Include custom bearer prefix to copy CURL (#253)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-09-16 11:20:23 -07:00
dependabot[bot]
28c6af8f94 Bump vite from 7.0.4 to 7.0.7 (#254)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-16 10:25:34 -07:00
Gabriel Oliveira
f8b0510d08 feat(settings): add do not check for updates (#246)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-08-08 13:25:55 -07:00
Gregory Schier
5f99b7df05 Use logger for plugin logs so they actually show
https://feedback.yaak.app/p/log-statements-dont-appear-from-within-plugins
2025-08-08 13:00:38 -07:00
Gregory Schier
158877b355 Fix SVG viewer crashing 2025-08-08 12:59:59 -07:00
Gregory Schier
8b84545b67 Prevent curl copy from using stale body data
https://feedback.yaak.app/p/copy-as-curl-includes-wrong-data-property
2025-08-02 10:03:35 -07:00
Gregory Schier
0e28079965 Fix GraphQL introspection breaking app
https://feedback.yaak.app/p/workspace-crash-when-graphql-introspection-returns-unexpected-response
2025-08-02 09:58:53 -07:00
Gregory Schier
5d5f9cc943 Better iFrame sandboxing
https://feedback.yaak.app/p/completely-white-ui
2025-08-02 09:47:34 -07:00
Gregory Schier
b71bc2cc92 Fix gRPC/WS hang because of ALPN
https://feedback.yaak.app/p/grpc-stalls-at-inspecting-schema-no-timeout-no-manual-proto
2025-08-02 09:37:28 -07:00
Gregory Schier
23191dcfc3 Revert change 2025-07-27 08:54:57 -07:00
Gregory Schier
372b15689d Fix min-size on md code 2025-07-27 08:54:44 -07:00
Gregory Schier
5c6d6fb7e4 Switch to menu right more easily 2025-07-27 08:45:56 -07:00
Gregory Schier
835a2e93e9 Text selection and syntax highlighting to markdown previews
https://feedback.yaak.app/p/enable-text-selection-in-the-info-section
2025-07-27 08:33:44 -07:00
Gregory Schier
93c6f6d611 re-enable http/2 support 2025-07-27 07:38:12 -07:00
Gregory Schier
b445261b32 Some tweaks 2025-07-26 15:48:58 -07:00
Gregory Schier
685b59cee9 Fix error 2025-07-26 14:34:40 -07:00
Gregory Schier
38529cc89e Plugin init/dispose 2025-07-26 14:28:59 -07:00
Gregory Schier
0d98b95b61 Don't prompt for updates on Linux unless APPIMAGE env exists 2025-07-25 15:10:04 -07:00
Gregory Schier
e044dcae3e Add back Rose Pine themes 2025-07-25 09:39:37 -07:00
Gregory Schier
b5b7b1638d Use physical key codes for zoom hotkeys 2025-07-24 09:15:57 -07:00
Gregory Schier
9d6ac8a107 Add template.desktop to make icon work until we can upgrade Tauri CLI 2025-07-24 08:44:55 -07:00
Gregory Schier
6440df492e Generate icons 2025-07-24 08:07:16 -07:00
Gregory Schier
2cdd97cabb Fix header spacing for window controls
https://feedback.yaak.app/p/app-bar-icons-not-aligned-correctly-when-fullscreen
2025-07-24 07:57:38 -07:00
Gregory Schier
20681e5be3 Scoped OAuth 2 tokens 2025-07-23 22:03:03 -07:00
Gregory Schier
a258a80fbd Prevent auth from adding lone ? to URL
https://feedback.yaak.app/p/using-inherited-api-key-causes-a-question-mark-to-be
2025-07-23 17:20:17 -07:00
Gregory Schier
1b90842d30 Regex template function 2025-07-23 13:33:58 -07:00
Carter Costic
f1acb3c925 Merge pull request #245
* Attach cookies to WS Upgrade

* Merge branch 'main' into main

* Move reqwest_cookie_store to workspace dep
2025-07-23 13:14:15 -07:00
Gregory Schier
28630bbb6c Remove template as default value 2025-07-23 12:46:26 -07:00
Gregory Schier
86a09642e7 Rename template-function-datetime 2025-07-23 12:42:54 -07:00
Song
0b38948826 add template-function-datetime (#244) 2025-07-23 12:41:24 -07:00
Gregory Schier
c09083ddec Fix up export dialog 2025-07-21 14:45:13 -07:00
Gregory Schier
44ee020383 Plugins menu item and link to run button 2025-07-21 14:38:29 -07:00
Gregory Schier
c609d0ff0c Fix GraphQL schema getting nuked on codemirror language refresh 2025-07-21 14:17:36 -07:00
Gregory Schier
7eb3f123c6 Add run button link 2025-07-21 07:47:29 -07:00
Gregory Schier
2bd8a50df4 Tweak tab padding 2025-07-21 07:45:11 -07:00
Gregory Schier
178cc88efb Fix Authenticatin typo
https://feedback.yaak.app/p/authentication-misspelled-in-request-auth-tooltip
2025-07-21 07:39:54 -07:00
Gregory Schier
38b2893cbf npm i 2025-07-20 09:48:57 -07:00
Gregory Schier
144faad31f Add API key auth
https://feedback.yaak.app/p/header-as-auth-option
2025-07-20 09:15:03 -07:00
Gregory Schier
947926ca34 Fix deadlock 2025-07-20 08:58:22 -07:00
Gregory Schier
86f23990eb Fixed bugs in Plugin settings pane 2025-07-20 08:28:00 -07:00
Gregory Schier
861b41b5ae JSONPath plugin README 2025-07-20 06:42:33 -07:00
Gregory Schier
7f4ccbe014 OAuth 2 plugin README 2025-07-19 21:47:19 -07:00
Gregory Schier
3b61c836be Merge remote-tracking branch 'origin/main' 2025-07-19 21:39:47 -07:00
Gregory Schier
6616cb67cd JWT plugin README 2025-07-19 21:39:40 -07:00
Song
e5fd4134ba inline url search param and use --data (#239) 2025-07-19 21:28:39 -07:00
Gregory Schier
31b0b14c04 Merge remote-tracking branch 'origin/main' 2025-07-19 21:25:21 -07:00
Gregory Schier
daeaf2a999 Bearer plugin README 2025-07-19 21:25:15 -07:00
Song
ca2fe07265 Optimize request function (#242)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-07-19 09:29:42 -07:00
Song
adca071574 fix padding and hover highlight in tabs (#243) 2025-07-19 09:19:48 -07:00
Gregory Schier
d6057aa1ec Basic auth plugin README 2025-07-19 09:15:06 -07:00
Gregory Schier
60883cc1b9 copy grpcurl readme and fix 2025-07-19 09:10:49 -07:00
Gregory Schier
b32fe466b1 Copy as curl readme 2025-07-19 07:38:46 -07:00
Gregory Schier
f81ff27a9e Don't wrap tab content 2025-07-18 14:52:19 -07:00
Gregory Schier
8f737d799b Pad dynamic form for scrollbar 2025-07-18 14:52:08 -07:00
Gregory Schier
b67ea29aff Better error 2025-07-18 14:49:13 -07:00
Gregory Schier
a657c32445 Better authorization URL handling 2025-07-18 14:48:45 -07:00
Andrew Berezovskyi
5061e17700 Update mimetypes.ts with RDF mime types beyond JSON-LD and N3 (#235) 2025-07-18 14:37:14 -07:00
Song
d9d5c4d564 remove unnecessary semicolon in tailwind config file (#236) 2025-07-18 14:36:28 -07:00
Song
343986c018 make monospace font family follows app setting in auto completion menu (#237)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-07-18 14:35:57 -07:00
Song
0d4b7bb5e2 Improve <details> component (#238)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-07-18 14:28:24 -07:00
Song
4a2fb6ed48 Improve layout resizer (#240)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-07-18 13:35:29 -07:00
Gregory Schier
74b6f4fb42 Fix pair editor creating new entry by clicking value 2025-07-18 08:54:37 -07:00
Gregory Schier
bcde4de4a7 Tweak workspace settings and a bunch of small things 2025-07-18 08:47:14 -07:00
Gregory Schier
4c375ed3e9 Tweak 2025-07-15 07:25:34 -07:00
Gregory Schier
2fcd2a3c07 Fix docs explorer cmd+click 2025-07-15 07:02:08 -07:00
Gregory Schier
0c60d190af Fix lint errors and show docs explorer on Cmd click 2025-07-14 14:52:16 -07:00
Gregory Schier
6f1fd7a254 Fix lint errors after upgrades and narrow tsc 2025-07-14 10:09:08 -07:00
Gregory Schier
5c1fba4b0c Fix Postman import description
https://feedback.yaak.app/p/missing-documentation-info-when-importing-postman-requests
2025-07-14 07:36:04 -07:00
Gregory Schier
6df13c452b Upgrade dependencies 2025-07-14 07:35:37 -07:00
Gregory Schier
209ac45ed2 Fix pop out scroll 2025-07-11 08:52:31 -07:00
Gregory Schier
ad4e073f62 Pop out dynamic form editor into dialog 2025-07-11 08:33:04 -07:00
Gregory Schier
791e5ad486 Fixes for websocket closing 2025-07-11 08:10:14 -07:00
Gregory Schier
fef6cc47f9 Smaller cancel button 2025-07-10 14:37:32 -07:00
Gregory Schier
c94331f454 Support GET GraphQL queries
https://feedback.yaak.app/p/support-get-graphql-queries-out-of-the-box
2025-07-10 14:06:54 -07:00
Gregory Schier
a31f818424 Don't show plugin error for response filter
https://feedback.yaak.app/p/increase-debounce-time-for-jsonpath-xpath-filter
https://feedback.yaak.app/p/possibility-to-cancel-request
2025-07-10 13:49:53 -07:00
Gregory Schier
f63da432b9 Fix split in curl importer
https://feedback.yaak.app/p/import-from-curl-does-not-work-properly-sometimes
2025-07-10 13:13:28 -07:00
Gregory Schier
456c8bd95f Add env key to useRenderTemplate()
https://feedback.yaak.app/p/environment-preview-is-inaccurate
2025-07-10 13:06:00 -07:00
Gregory Schier
b529bab578 Lower large response confirm 2025-07-10 12:59:45 -07:00
Gregory Schier
840f15c997 Always update response if error
https://feedback.yaak.app/p/cant-re-send-request-if-there-is-one-ongoing
2025-07-10 12:51:04 -07:00
Gregory Schier
f745435d26 Add comment 2025-07-10 11:47:26 -07:00
Gregory Schier
4038666986 Update single line filter extension 2025-07-10 11:46:27 -07:00
mooonfly
2b07d1a493 Fix duplicated character when composing text (#234) 2025-07-10 11:37:29 -07:00
Gregory Schier
333b64e7f3 Resolve requests for request actions
https://feedback.yaak.app/p/plugin-cannot-get-inhereted-parameters-when-rendering-a-request
2025-07-10 11:32:03 -07:00
Gregory Schier
9cd430b3de Docs explorer tweaks 2025-07-10 06:35:52 -07:00
Gregory Schier
f0bafb21cc Fix 2025-07-09 14:25:11 -07:00
Gregory Schier
f00adf6fce A bunch of responsiveness fixes 2025-07-09 14:24:29 -07:00
Gregory Schier
d9f9ea4047 Fix state bug 2025-07-09 12:48:40 -07:00
Gregory Schier
036e85d006 Schema filtering and a bunch of fixes 2025-07-09 12:39:27 -07:00
Gregory Schier
a03ec8875c Persist gql docs shown state 2025-07-08 09:29:56 -07:00
Gregory Schier
a3f50a2bb7 Clean up GraphQL explorer 2025-07-08 07:44:50 -07:00
Gregory Schier
6c0f9377cd Fix plugin builds 2025-07-07 14:17:47 -07:00
Gregory Schier
bd2662fbe3 Show implements and fix non-null and list types 2025-07-07 14:12:28 -07:00
Gregory Schier
f5dbff4682 Add docs close button 2025-07-07 13:59:06 -07:00
Gregory Schier
7a11da42af Some fixes 2025-07-07 13:52:54 -07:00
Gregory Schier
01f9c072a7 I think we're good 2025-07-07 13:41:26 -07:00
Gregory Schier
47722643ee Add descriptions to plugins 2025-07-06 12:47:13 -07:00
Gregory Schier
cf35658fea Revert Tauri CLI 2025-07-05 16:45:07 -07:00
Gregory Schier
6330c77948 Fix linux build 2025-07-05 16:16:50 -07:00
Gregory Schier
77d2edd947 Add log 2025-07-05 16:00:46 -07:00
Gregory Schier
4f0f60cb99 Add log 2025-07-05 16:00:20 -07:00
Gregory Schier
dd2b665982 Tweak protos CLI arg generation 2025-07-05 15:58:36 -07:00
Gregory Schier
19ffcd18a6 gRPC request actions and "copy as gRPCurl" (#232) 2025-07-05 15:40:41 -07:00
Gregory Schier
ad4d6d9720 Merge branch 'theme-plugins'
# Conflicts:
#	packages/plugin-runtime-types/src/bindings/gen_events.ts
2025-07-05 06:37:32 -07:00
Gregory Schier
9e98b5f905 Fix macos window theme calculation 2025-07-05 06:37:02 -07:00
Gregory Schier
19c6ad9d97 Theme plugins (#231) 2025-07-03 13:06:30 -07:00
Gregory Schier
a0e5e60803 Fix filter plugin names 2025-07-03 12:28:34 -07:00
Gregory Schier
2a6f139d36 Better plugin reloading and theme parsing 2025-07-03 12:25:22 -07:00
Gregory Schier
36bbb87a5e Mostly working 2025-07-03 11:48:17 -07:00
Gregory Schier
a6979cf37e Print table/col/val when row not found 2025-07-02 08:14:52 -07:00
Gregory Schier
ff26cc1344 Tweaks 2025-07-02 07:47:36 -07:00
Gregory Schier
fa62f88fa4 Allow moving requests and folders to end of list 2025-06-29 08:40:14 -07:00
Gregory Schier
99975c3223 Fix sidebar folder dragging collapse
https://feedback.yaak.app/p/a-folder-may-hide-its-content-if-i-move-a
2025-06-29 08:02:55 -07:00
Gregory Schier
d3cda19be2 Hide large request bodies by default 2025-06-29 07:30:07 -07:00
Gregory Schier
9b0a767ac8 Prevent command palette from jumping with less results 2025-06-28 07:37:15 -07:00
Gregory Schier
81c3de807d Add json.minify 2025-06-28 07:29:24 -07:00
Gregory Schier
9ab02130b0 Fix sync import issues:
https://feedback.yaak.app/p/yaml-error-missing-field-type-at-line-4521-column-1
2025-06-27 13:32:52 -07:00
Gregory Schier
25d50246c0 Revert notification endpoint URL for dev 2025-06-27 11:58:04 -07:00
Gregory Schier
bb0cc16a70 Use API client for notifications/license 2025-06-25 08:17:17 -07:00
Gregory Schier
8817be679b Fix PKCE flow and clean up other flows 2025-06-25 07:10:11 -07:00
Gregory Schier
f476d87613 Add back unsigned memory entitlement 2025-06-24 06:21:07 -07:00
Gregory Schier
1438e8bacc Upgrade eslint and fix issues 2025-06-23 14:09:09 -07:00
Gregory Schier
7be2767527 Fix lint error 2025-06-23 09:51:44 -07:00
Gregory Schier
a1b1eafd39 Add links to plugins 2025-06-23 09:46:54 -07:00
Gregory Schier
1948fb78bd Fix bad import 2025-06-23 08:57:31 -07:00
Gregory Schier
cb7c44cc65 Install plugins from Yaak plugin registry (#230) 2025-06-23 08:55:38 -07:00
Gregory Schier
b5620fcdf3 Merge pull request #227
* Search and install plugins PoC

* Checksum

* Tab sidebar for settings

* Fix nested tabs, and tweaks

* Table for plugin results

* Deep links working

* Focus window during deep links

* Merge branch 'master' into plugin-directory

* More stuff
2025-06-22 07:06:43 -07:00
Mr0Bread
b8e6dbc7c7 GraphQL Documentation explorer (#208) 2025-06-17 17:08:39 -07:00
489 changed files with 22309 additions and 11305 deletions

View File

@@ -1,6 +0,0 @@
node_modules/
dist/
.eslintrc.cjs
.prettierrc.cjs
src-web/postcss.config.cjs
src-web/vite.config.ts

View File

@@ -1,49 +0,0 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:import/recommended',
'plugin:jsx-a11y/recommended',
'plugin:@typescript-eslint/recommended',
'eslint-config-prettier',
],
plugins: ['react-refresh'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: ['./tsconfig.json'],
},
ignorePatterns: [
'scripts/**/*',
'packages/plugin-runtime/**/*',
'packages/plugin-runtime-types/**/*',
'src-tauri/**/*',
'src-web/tailwind.config.cjs',
'src-web/vite.config.ts',
],
settings: {
react: {
version: 'detect',
},
'import/resolver': {
node: {
paths: ['src-web'],
extensions: ['.ts', '.tsx'],
},
},
},
rules: {
'react-refresh/only-export-components': 'error',
'jsx-a11y/no-autofocus': 'off',
'react/react-in-jsx-scope': 'off',
'import/no-unresolved': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
disallowTypeAnnotations: true,
fixStyle: 'separate-type-imports',
},
],
},
};

View File

@@ -62,7 +62,7 @@ jobs:
- name: install dependencies (windows only)
if: matrix.platform == 'windows-latest'
run: cargo install --force trusted-signing-cli --version 0.5.0
run: cargo install --force trusted-signing-cli
- name: Install NPM Dependencies
run: npm ci
@@ -72,12 +72,16 @@ jobs:
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Run JS build
run: npm run build
# Some things (eg. WASM package) requires building before lint will work
- name: Run bootstrap
run: npm run bootstrap
- name: Run lint
run: npm run lint
- name: Run tests
run: npm test
- name: Set version
run: npm run replace-version
env:
@@ -109,5 +113,5 @@ jobs:
releaseName: 'Release __VERSION__'
releaseBody: '[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)'
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}
prerelease: true
args: '${{ matrix.args }} --config ./src-tauri/tauri.release.conf.json'

44
.github/workflows/sponsors.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: Generate Sponsors README
on:
workflow_dispatch:
schedule:
- cron: 30 15 * * 0-6
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2
- name: Generate Sponsors
uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.SPONSORS_PAT }}
file: 'README.md'
maximum: 1999
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="50px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;'
active-only: false
include-private: true
marker: 'sponsors-base'
- name: Generate Sponsors
uses: JamesIves/github-sponsors-readme-action@v1
with:
token: ${{ secrets.SPONSORS_PAT }}
file: 'README.md'
minimum: 2000
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="80px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;'
active-only: false
include-private: true
marker: 'sponsors-premium'
# ⚠️ Note: You can use any deployment step here to automatically push the README
# changes back to your branch.
- name: Commit Changes
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: main
force: false
folder: '.'

View File

@@ -1,34 +1,70 @@
# Yaak API Client
<p align="center">
<a href="https://github.com/JamesIves/github-sponsors-readme-action">
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/src-tauri/icons/icon.png">
</a>
</p>
Yaak is a desktop API client for interacting with REST, GraphQL, Server Sent Events (SSE), WebSocket, and gRPC
APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
<h1 align="center">
💫 Yaak ➟ Desktop API Client 💫
</h1>
<p align="center">
A fast, privacy-first API client for REST, GraphQL, SSE, WebSocket, and gRPC built with Tauri, Rust, and React.
</p>
<p align="center">
Development is funded by community-purchased <a href="https://yaak.app/pricing">licenses</a>. You can also <a href="https://github.com/sponsors/gschier">become a sponsor</a> to have your logo appear below. 💖
</p>
<br>
<p align="center">
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https:&#x2F;&#x2F;github.com&#x2F;MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a>&nbsp;&nbsp;<a href="https://github.com/dharsanb"><img src="https:&#x2F;&#x2F;github.com&#x2F;dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a>&nbsp;&nbsp;<a href="https://github.com/railwayapp"><img src="https:&#x2F;&#x2F;github.com&#x2F;railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a>&nbsp;&nbsp;<a href="https://github.com/caseyamcl"><img src="https:&#x2F;&#x2F;github.com&#x2F;caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a>&nbsp;&nbsp;<a href="https://github.com/andriyor"><img src="https:&#x2F;&#x2F;github.com&#x2F;andriyor.png" width="80px" alt="User avatar: andriyor" /></a>&nbsp;&nbsp;<a href="https://github.com/"><img src="https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;JamesIves&#x2F;github-sponsors-readme-action&#x2F;dev&#x2F;.github&#x2F;assets&#x2F;placeholder.png" width="80px" alt="User avatar: " /></a>&nbsp;&nbsp;<!-- sponsors-premium -->
</p>
<p align="center">
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanwash.png" width="50px" alt="User avatar: seanwash" /></a>&nbsp;&nbsp;<a href="https://github.com/jerath"><img src="https:&#x2F;&#x2F;github.com&#x2F;jerath.png" width="50px" alt="User avatar: jerath" /></a>&nbsp;&nbsp;<a href="https://github.com/itsa-sh"><img src="https:&#x2F;&#x2F;github.com&#x2F;itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a>&nbsp;&nbsp;<a href="https://github.com/dmmulroy"><img src="https:&#x2F;&#x2F;github.com&#x2F;dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a>&nbsp;&nbsp;<a href="https://github.com/timcole"><img src="https:&#x2F;&#x2F;github.com&#x2F;timcole.png" width="50px" alt="User avatar: timcole" /></a>&nbsp;&nbsp;<a href="https://github.com/VLZH"><img src="https:&#x2F;&#x2F;github.com&#x2F;VLZH.png" width="50px" alt="User avatar: VLZH" /></a>&nbsp;&nbsp;<a href="https://github.com/terasaka2k"><img src="https:&#x2F;&#x2F;github.com&#x2F;terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a>&nbsp;&nbsp;<a href="https://github.com/majudhu"><img src="https:&#x2F;&#x2F;github.com&#x2F;majudhu.png" width="50px" alt="User avatar: majudhu" /></a>&nbsp;&nbsp;<!-- sponsors-base -->
</p>
![Yaak API Client](https://yaak.app/static/screenshot.png)
## Features
Yaak is an offline-first API client designed to stay out of your way while giving you everything you need when you need it.
Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight, and private. No telemetry, no VC funding, and no cloud lock-in.
### 🌐 Work with any API
- Import collections from Postman, Insomnia, OpenAPI, Swagger, or Curl.
- Send requests via REST, GraphQL, gRPC, WebSocket, or Server-Sent Events.
- Filter and inspect responses with JSONPath or XPath.
### 🔐 Stay secure
- Use OAuth 2.0, JWT, Basic Auth, or custom plugins for authentication.
- Secure sensitive values with encrypted secrets.
- Store secrets in your OS keychain.
### ☁️ Organize & collaborate
- Group requests into workspaces and nested folders.
- Use environment variables to switch between dev, staging, and prod.
- Mirror workspaces to your filesystem for versioning in Git or syncing with Dropbox.
### 🧩 Extend & customize
- Insert dynamic values like UUIDs or timestamps with template tags.
- Pick from built-in themes or build your own.
- Create plugins to extend authentication, template tags, or the UI.
![366149288-f18e963f-0b68-4ecb-b8b8-cb71aa9aec02](https://github.com/user-attachments/assets/ca83b7ad-5708-411b-8faf-e36b365841a4)
## 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.
## Feature Overview
- 🪂 Import data from Postman, Insomnia, OpenAPI, Swagger, or Curl.<br/>
- 📤 Send requests via REST, GraphQL, Server Sent Events (SSE), WebSockets, or gRPC.<br/>
- 🔐 Automatically authorize requests with OAuth 2.0, JWT tokens, Basic Auth, and more.<br/>
- 🔎 Filter response bodies using JSONPath or XPath queries.<br/>
- ⛓️ Chain together multiple requests to dynamically reference values.<br/>
- 📂 Organize requests into workspaces and nested folders.<br/>
- 🧮 Use environment variables to easily switch between Prod and Dev.<br/>
- 🛡️ Secure arbitrary text values with end-to-end encryption<br/>
- 🏷️ Send dynamic values like UUIDs or timestamps using template tags.<br/>
- 🎨 Choose from many of the included themes, or make your own.<br/>
- 💽 Mirror workspace data to a directory for integration with Git or Dropbox.<br/>
- 📜 View response history for each request.<br/>
- 🔌 Create your own plugins for authentication, template tags, and more!<br/>
- 🛜 Configure a proxy to access firewall-blocked APIs
## Useful Resources
- [Feedback and Bug Reports](https://feedback.yaak.app)
- [Documentation](https://feedback.yaak.app/help)
- [Yaak vs Postman](https://yaak.app/blog/postman-alternative)
- [Yaak vs Postman](https://yaak.app/alternatives/postman)
- [Yaak vs Bruno](https://yaak.app/alternatives/bruno)
- [Yaak vs Insomnia](https://yaak.app/alternatives/insomnia)

89
eslint.config.cjs Normal file
View File

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

9376
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -7,20 +7,24 @@
"url": "git+https://github.com/mountain-loop/yaak.git"
},
"workspaces": [
"packages/common-lib",
"packages/plugin-runtime",
"packages/plugin-runtime-types",
"packages/common-lib",
"plugins/action-copy-curl",
"plugins/action-copy-grpcurl",
"plugins/auth-apikey",
"plugins/auth-aws",
"plugins/auth-basic",
"plugins/auth-bearer",
"plugins/auth-jwt",
"plugins/auth-oauth2",
"plugins/exporter-curl",
"plugins/filter-jsonpath",
"plugins/filter-xpath",
"plugins/importer-curl",
"plugins/importer-insomnia",
"plugins/importer-openapi",
"plugins/importer-postman",
"plugins/importer-postman-environment",
"plugins/importer-yaak",
"plugins/template-function-cookie",
"plugins/template-function-encode",
@@ -31,11 +35,14 @@
"plugins/template-function-regex",
"plugins/template-function-request",
"plugins/template-function-response",
"plugins/template-function-timestamp",
"plugins/template-function-uuid",
"plugins/template-function-xml",
"plugins/themes-yaak",
"src-tauri",
"src-tauri/yaak-crypto",
"src-tauri/yaak-git",
"src-tauri/yaak-fonts",
"src-tauri/yaak-git",
"src-tauri/yaak-license",
"src-tauri/yaak-mac-window",
"src-tauri/yaak-models",
@@ -49,10 +56,14 @@
"scripts": {
"start": "npm run app-dev",
"app-build": "tauri build",
"app-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
"app-dev": "tauri dev --no-watch --config ./src-tauri/tauri.development.conf.json",
"migration": "node scripts/create-migration.cjs",
"build": "npm run --workspaces --if-present build",
"build-plugins": "npm run --workspaces --if-present build",
"test": "npm run --workspaces --if-present test",
"icons": "run-p icons:*",
"icons:dev": "tauri icon src-tauri/icons/icon-dev.png --output src-tauri/icons/dev",
"icons:release": "tauri icon src-tauri/icons/icon.png --output src-tauri/icons/release",
"bootstrap": "run-p bootstrap:* && npm run --workspaces --if-present bootstrap",
"bootstrap:vendor-node": "node scripts/vendor-node.cjs",
"bootstrap:vendor-plugins": "node scripts/vendor-plugins.cjs",
@@ -67,20 +78,24 @@
"jotai": "^2.12.2"
},
"devDependencies": {
"@tauri-apps/cli": "2.4.1",
"@eslint/compat": "^1.3.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.29.0",
"@tauri-apps/cli": "^2.8.4",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.1.5",
"eslint": "^8",
"eslint-config-prettier": "^8",
"eslint-plugin-import": "^2.31.0",
"@yaakapp/cli": "^0.2.7",
"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.8.3",
"vitest": "^3.2.4",
"workspaces-run": "^1.0.2"
}
}

View File

@@ -24,5 +24,5 @@ the [Quick Start Guide](https://feedback.yaak.app/help/articles/6911763-plugins-
If you prefer starting from scratch, manually install the types package:
```shell
npm install @yaakapp/api
npm install -D @yaakapp/api
```

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.6.4",
"version": "0.7.0",
"keywords": [
"api-client",
"insomnia-alternative",
@@ -31,7 +31,7 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"@types/node": "^22.5.4"
"@types/node": "^24.0.13"
},
"devDependencies": {
"cpy-cli": "^5.0.0"

View File

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

View File

@@ -4,7 +4,9 @@ import type { JsonValue } from "./serde_json/JsonValue.js";
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, };
export type CallGrpcRequestActionArgs = { grpcRequest: GrpcRequest, protoFiles: Array<string>, };
export type CallGrpcRequestActionRequest = { index: number, pluginRefId: string, args: CallGrpcRequestActionArgs, };
export type CallHttpAuthenticationActionArgs = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
@@ -17,7 +19,12 @@ export type CallHttpAuthenticationResponse = {
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders: Array<HttpHeader>, };
setHeaders?: Array<HttpHeader>,
/**
* Query parameters to add to the request. Existing params will be replaced, while
* new params will be added.
*/
setQueryParameters?: Array<HttpHeader>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
@@ -27,7 +34,7 @@ export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
export type CallTemplateFunctionResponse = { value: string | null, };
export type CallTemplateFunctionResponse = { value: string | null, error?: string, };
export type CloseWindowRequest = { label: string, };
@@ -61,7 +68,7 @@ extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FilterResponse = { content: string, error?: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
@@ -336,14 +343,14 @@ export type GetCookieValueRequest = { name: string, };
export type GetCookieValueResponse = { value: string | null, };
export type GetGrpcRequestActionsResponse = { actions: Array<GrpcRequestAction>, pluginRefId: string, };
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
export type GetHttpAuthenticationSummaryResponse = { name: string, label: string, shortLabel: string, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, };
@@ -354,7 +361,17 @@ export type GetKeyValueRequest = { key: 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 GetThemesResponse = { themes: Array<Theme>, };
export type GrpcRequestAction = { label: string, icon?: Icon, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, };
@@ -372,7 +389,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "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_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": "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": "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": "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;
@@ -404,6 +421,12 @@ required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type ReloadResponse = { silent: boolean, };
export type RenderGrpcRequestRequest = { grpcRequest: GrpcRequest, purpose: RenderPurpose, };
export type RenderGrpcRequestResponse = { grpcRequest: GrpcRequest, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
@@ -418,7 +441,7 @@ export type SetKeyValueRequest = { key: string, value: string, };
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,
/**
@@ -436,6 +459,32 @@ export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, }
export type TemplateRenderResponse = { data: JsonValue, };
export type Theme = {
/**
* How the theme is identified. This should never be changed
*/
id: string,
/**
* The friendly name of the theme to be displayed to the user
*/
label: string,
/**
* Whether the theme will be used for dark or light appearance
*/
dark: boolean,
/**
* The default top-level colors for the theme
*/
base: ThemeComponentColors,
/**
* Optionally override theme for individual UI components for more control
*/
components?: ThemeComponents, };
export type ThemeComponentColors = { surface?: string, surfaceHighlight?: string, surfaceActive?: string, text?: string, textSubtle?: string, textSubtlest?: string, border?: string, borderSubtle?: string, borderFocus?: string, shadow?: string, backdrop?: string, selection?: string, primary?: string, secondary?: string, info?: string, success?: string, notice?: string, warning?: string, danger?: 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 WindowNavigateEvent = { url: string, };
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.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: 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, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PluginMetadata = { version: string, name: string, displayName: string, description: string | null, homepageUrl: string | null, repositoryUrl: string | null, };
export type PluginVersion = { id: string, version: string, url: string, description: string | null, name: string, displayName: string, homepageUrl: string | null, repositoryUrl: string | null, checksum: string, readme: string | null, yanked: boolean, };

View File

@@ -9,14 +9,16 @@ import type {
OpenWindowRequest,
PromptTextRequest,
PromptTextResponse,
RenderGrpcRequestRequest,
RenderGrpcRequestResponse,
RenderHttpRequestRequest,
RenderHttpRequestResponse,
SendHttpRequestRequest,
SendHttpRequestResponse,
ShowToastRequest,
TemplateRenderRequest,
TemplateRenderResponse,
} from '../bindings/gen_events.ts';
import { JsonValue } from '../bindings/serde_json/JsonValue';
export interface Context {
clipboard: {
@@ -45,6 +47,9 @@ export interface Context {
listNames(): Promise<ListCookieNamesResponse['names']>;
getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse['value']>;
};
grpcRequest: {
render(args: RenderGrpcRequestRequest): Promise<RenderGrpcRequestResponse['grpcRequest']>;
};
httpRequest: {
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
@@ -54,6 +59,9 @@ export interface Context {
find(args: FindHttpResponsesRequest): Promise<FindHttpResponsesResponse['httpResponses']>;
};
templates: {
render(args: TemplateRenderRequest): Promise<TemplateRenderResponse['data']>;
render<T extends JsonValue>(args: TemplateRenderRequest & { data: T }): Promise<T>;
};
plugin: {
reload(): void;
};
}

View File

@@ -1,12 +1,11 @@
import { FilterResponse } from '../bindings/gen_events';
import type { Context } from './Context';
type FilterPluginResponse = { filtered: string };
export type FilterPlugin = {
name: string;
description?: string;
onFilter(
ctx: Context,
args: { payload: string; filter: string; mimeType: string },
): Promise<FilterPluginResponse> | FilterPluginResponse;
): Promise<FilterResponse> | FilterResponse;
};

View File

@@ -0,0 +1,6 @@
import { CallGrpcRequestActionArgs, GrpcRequestAction } from '../bindings/gen_events';
import type { Context } from './Context';
export type GrpcRequestActionPlugin = GrpcRequestAction & {
onSelect(ctx: Context, args: CallGrpcRequestActionArgs): Promise<void> | void;
};

View File

@@ -1,5 +1,5 @@
import { ImportResources } from '../bindings/gen_events';
import { AtLeast } from '../helpers';
import { AtLeast, MaybePromise } from '../helpers';
import type { Context } from './Context';
type RootFields = 'name' | 'id' | 'model';
@@ -21,5 +21,8 @@ export type ImportPluginResponse = null | {
export type ImporterPlugin = {
name: string;
description?: string;
onImport(ctx: Context, args: { text: string }): Promise<ImportPluginResponse>;
onImport(
ctx: Context,
args: { text: string },
): MaybePromise<ImportPluginResponse | null | undefined>;
};

View File

@@ -1,12 +1,21 @@
import {
CallTemplateFunctionArgs,
FormInput,
GetHttpAuthenticationConfigRequest,
TemplateFunction,
} from "../bindings/gen_events";
import { Context } from "./Context";
TemplateFunctionArg,
} from '../bindings/gen_events';
import { MaybePromise } from '../helpers';
import { Context } from './Context';
export type DynamicTemplateFunctionArg = FormInput & {
dynamic(
ctx: Context,
args: GetHttpAuthenticationConfigRequest,
): MaybePromise<Partial<FormInput> | undefined | null>;
};
export type TemplateFunctionPlugin = TemplateFunction & {
onRender(
ctx: Context,
args: CallTemplateFunctionArgs,
): Promise<string | null>;
args: (TemplateFunctionArg | DynamicTemplateFunctionArg)[];
onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>;
};

View File

@@ -1,8 +1,3 @@
import { Index } from "../themes";
import { Context } from "./Context";
import { Theme } from '../bindings/gen_events';
export type ThemePlugin = {
name: string;
description?: string;
getTheme(ctx: Context, fileContents: string): Promise<Index>;
};
export type ThemePlugin = Theme;

View File

@@ -1,20 +1,26 @@
import { AuthenticationPlugin } from './AuthenticationPlugin';
import type { FilterPlugin } from './FilterPlugin';
import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
import type { ImporterPlugin } from './ImporterPlugin';
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
import type { ThemePlugin } from './ThemePlugin';
export type { Context } from './Context';
import type { Context } from './Context';
export type { Context };
/**
* The global structure of a Yaak plugin
*/
export type PluginDefinition = {
init?: (ctx: Context) => void | Promise<void>;
dispose?: () => void | Promise<void>;
importer?: ImporterPlugin;
theme?: ThemePlugin;
themes?: ThemePlugin[];
filter?: FilterPlugin;
authentication?: AuthenticationPlugin;
httpRequestActions?: HttpRequestActionPlugin[];
grpcRequestActions?: GrpcRequestActionPlugin[];
templateFunctions?: TemplateFunctionPlugin[];
};

View File

@@ -2,12 +2,17 @@
"compilerOptions": {
"module": "node16",
"target": "es6",
"lib": ["es2021"],
"lib": [
"es2021",
"dom"
],
"declaration": true,
"declarationDir": "./lib",
"outDir": "./lib",
"strict": true,
"types": ["node"]
"types": [
"node"
]
},
"files": [
"src/index.ts"

View File

@@ -21,7 +21,7 @@ export class PluginHandle {
this.#instance.postMessage(event);
}
terminate() {
this.#instance.terminate();
async terminate() {
await this.#instance.terminate();
}
}

View File

@@ -7,6 +7,7 @@ import {
GetCookieValueResponse,
GetHttpRequestByIdResponse,
GetKeyValueResponse,
GrpcRequestAction,
HttpAuthenticationAction,
HttpRequestAction,
InternalEvent,
@@ -14,6 +15,7 @@ import {
ListCookieNamesResponse,
PluginWindowContext,
PromptTextResponse,
RenderGrpcRequestResponse,
RenderHttpRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
@@ -52,9 +54,22 @@ export class PluginInstance {
// Reload plugin if the JS or package.json changes
const windowContextNone: PluginWindowContext = { type: 'none' };
this.#mod = {} as any;
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
const fileChangeCallback = async () => {
await this.#mod?.dispose?.();
this.#importModule();
return this.#sendPayload(windowContextNone, { type: 'reload_response' }, null);
await this.#mod?.init?.(this.#newCtx({ type: 'none' }));
return this.#sendPayload(
windowContextNone,
{
type: 'reload_response',
silent: false,
},
null,
);
};
if (this.#workerData.bootRequest.watch) {
@@ -62,12 +77,6 @@ export class PluginInstance {
watchFile(this.#pathPkg(), fileChangeCallback);
}
this.#mod = {};
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
// TODO: Re-implement this now that we're not using workers
// prefixStdout(`[plugin][${this.#pkg.name}] %s`);
this.#importModule();
}
@@ -75,23 +84,20 @@ export class PluginInstance {
this.#appToPluginEvents.emit(event);
}
terminate() {
async terminate() {
await this.#mod?.dispose?.();
this.#unimportModule();
}
async #onMessage(event: InternalEvent) {
const ctx = this.#newCtx(event);
const ctx = this.#newCtx(event.windowContext);
const { windowContext, payload, id: replyId } = event;
try {
if (payload.type === 'boot_request') {
// console.log('Plugin initialized', pkg.name, { capabilities, enableWatch });
const payload: InternalEventPayload = {
type: 'boot_response',
name: this.#pkg.name ?? 'unknown',
version: this.#pkg.version ?? '0.0.1',
};
this.#sendPayload(windowContext, payload, replyId);
await this.#mod?.init?.(ctx);
this.#sendPayload(windowContext, { type: 'boot_response' }, replyId);
return;
}
@@ -99,6 +105,7 @@ export class PluginInstance {
const payload: InternalEventPayload = {
type: 'terminate_response',
};
await this.terminate();
this.#sendPayload(windowContext, payload, replyId);
return;
}
@@ -129,9 +136,23 @@ export class PluginInstance {
payload: payload.content,
mimeType: payload.type,
});
this.#sendPayload(windowContext, { type: 'filter_response', ...reply }, replyId);
return;
}
if (
payload.type === 'get_grpc_request_actions_request' &&
Array.isArray(this.#mod?.grpcRequestActions)
) {
const reply: GrpcRequestAction[] = this.#mod.grpcRequestActions.map((a) => ({
...a,
// Add everything except onSelect
onSelect: undefined,
}));
const replyPayload: InternalEventPayload = {
type: 'filter_response',
content: reply.filtered,
type: 'get_grpc_request_actions_response',
pluginRefId: this.#workerData.pluginRefId,
actions: reply,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
@@ -155,21 +176,65 @@ export class PluginInstance {
return;
}
if (payload.type === 'get_themes_request' && Array.isArray(this.#mod?.themes)) {
const replyPayload: InternalEventPayload = {
type: 'get_themes_response',
themes: this.#mod.themes,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_template_functions_request' &&
payload.type === 'get_template_function_summary_request' &&
Array.isArray(this.#mod?.templateFunctions)
) {
const reply: TemplateFunction[] = this.#mod.templateFunctions.map((templateFunction) => {
return {
...migrateTemplateFunctionSelectOptions(templateFunction),
// Add everything except render
onRender: undefined,
};
});
const functions: TemplateFunction[] = this.#mod.templateFunctions.map(
(templateFunction) => {
return {
...migrateTemplateFunctionSelectOptions(templateFunction),
// Add everything except render
onRender: undefined,
};
},
);
const replyPayload: InternalEventPayload = {
type: 'get_template_functions_response',
type: 'get_template_function_summary_response',
pluginRefId: this.#workerData.pluginRefId,
functions: reply,
functions,
};
this.#sendPayload(windowContext, 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(windowContext, replyId);
return;
}
templateFunction = migrateTemplateFunctionSelectOptions(templateFunction);
// @ts-ignore
delete templateFunction.onRender;
const resolvedArgs: TemplateFunctionArg[] = [];
for (const arg of templateFunction.args) {
if (arg && 'dynamic' in arg) {
const dynamicAttrs = await arg.dynamic(ctx, payload);
const { dynamic, ...other } = arg;
resolvedArgs.push({ ...other, ...dynamicAttrs } as TemplateFunctionArg);
} else if (arg) {
resolvedArgs.push(arg);
}
templateFunction.args = resolvedArgs;
}
const replyPayload: InternalEventPayload = {
type: 'get_template_function_config_response',
pluginRefId: this.#workerData.pluginRefId,
function: templateFunction,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
@@ -191,13 +256,12 @@ export class PluginInstance {
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
const { args, actions } = this.#mod.authentication;
const resolvedArgs: FormInput[] = [];
for (let i = 0; i < args.length; i++) {
let v = args[i];
if ('dynamic' in v) {
for (const v of args) {
if (v && 'dynamic' in v) {
const dynamicAttrs = await v.dynamic(ctx, payload);
const { dynamic, ...other } = v;
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
} else {
} else if (v) {
resolvedArgs.push(v);
}
}
@@ -221,12 +285,11 @@ export class PluginInstance {
const auth = this.#mod.authentication;
if (typeof auth?.onApply === 'function') {
applyFormInputDefaults(auth.args, payload.values);
const result = await auth.onApply(ctx, payload);
this.#sendPayload(
windowContext,
{
type: 'call_http_authentication_response',
setHeaders: result.setHeaders,
...(await auth.onApply(ctx, payload)),
},
replyId,
);
@@ -258,6 +321,18 @@ export class PluginInstance {
}
}
if (
payload.type === 'call_grpc_request_action_request' &&
Array.isArray(this.#mod.grpcRequestActions)
) {
const action = this.#mod.grpcRequestActions[payload.index];
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
this.#sendEmpty(windowContext, replyId);
return;
}
}
if (
payload.type === 'call_template_function_request' &&
Array.isArray(this.#mod?.templateFunctions)
@@ -265,22 +340,30 @@ export class PluginInstance {
const fn = this.#mod.templateFunctions.find((a) => a.name === payload.name);
if (typeof fn?.onRender === 'function') {
applyFormInputDefaults(fn.args, payload.args.values);
const result = await fn.onRender(ctx, payload.args);
this.#sendPayload(
windowContext,
{
type: 'call_template_function_response',
value: result ?? null,
},
replyId,
);
try {
const result = await fn.onRender(ctx, payload.args);
this.#sendPayload(
windowContext,
{
type: 'call_template_function_response',
value: result ?? null,
},
replyId,
);
} catch (err) {
this.#sendPayload(
windowContext,
{
type: 'call_template_function_response',
value: null,
error: `${err}`.replace(/^Error:\s*/g, ''),
},
replyId,
);
}
return;
}
}
if (payload.type === 'reload_request') {
this.#importModule();
}
} catch (err) {
const error = `${err}`.replace(/^Error:\s*/g, '');
console.log('Plugin call threw exception', payload.type, '→', error);
@@ -392,11 +475,11 @@ export class PluginInstance {
this.#sendEvent(eventToSend);
}
#newCtx(event: InternalEvent): Context {
#newCtx(windowContext: PluginWindowContext): Context {
return {
clipboard: {
copyText: async (text) => {
await this.#sendAndWaitForReply(event.windowContext, {
await this.#sendAndWaitForReply(windowContext, {
type: 'copy_text_request',
text,
});
@@ -404,8 +487,10 @@ export class PluginInstance {
},
toast: {
show: async (args) => {
await this.#sendAndWaitForReply(event.windowContext, {
await this.#sendAndWaitForReply(windowContext, {
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,
});
},
@@ -421,21 +506,21 @@ export class PluginInstance {
onClose?.();
}
};
this.#sendAndListenForEvents(event.windowContext, payload, onEvent);
this.#sendAndListenForEvents(windowContext, payload, onEvent);
return {
close: () => {
const closePayload: InternalEventPayload = {
type: 'close_window_request',
label: args.label,
};
this.#sendPayload(event.windowContext, closePayload, null);
this.#sendPayload(windowContext, closePayload, null);
},
};
},
},
prompt: {
text: async (args) => {
const reply: PromptTextResponse = await this.#sendAndWaitForReply(event.windowContext, {
const reply: PromptTextResponse = await this.#sendAndWaitForReply(windowContext, {
type: 'prompt_text_request',
...args,
});
@@ -449,12 +534,25 @@ export class PluginInstance {
...args,
} as const;
const { httpResponses } = await this.#sendAndWaitForReply<FindHttpResponsesResponse>(
event.windowContext,
windowContext,
payload,
);
return httpResponses;
},
},
grpcRequest: {
render: async (args) => {
const payload = {
type: 'render_grpc_request_request',
...args,
} as const;
const { grpcRequest } = await this.#sendAndWaitForReply<RenderGrpcRequestResponse>(
windowContext,
payload,
);
return grpcRequest;
},
},
httpRequest: {
getById: async (args) => {
const payload = {
@@ -462,7 +560,7 @@ export class PluginInstance {
...args,
} as const;
const { httpRequest } = await this.#sendAndWaitForReply<GetHttpRequestByIdResponse>(
event.windowContext,
windowContext,
payload,
);
return httpRequest;
@@ -473,7 +571,7 @@ export class PluginInstance {
...args,
} as const;
const { httpResponse } = await this.#sendAndWaitForReply<SendHttpRequestResponse>(
event.windowContext,
windowContext,
payload,
);
return httpResponse;
@@ -484,7 +582,7 @@ export class PluginInstance {
...args,
} as const;
const { httpRequest } = await this.#sendAndWaitForReply<RenderHttpRequestResponse>(
event.windowContext,
windowContext,
payload,
);
return httpRequest;
@@ -497,7 +595,7 @@ export class PluginInstance {
...args,
} as const;
const { value } = await this.#sendAndWaitForReply<GetCookieValueResponse>(
event.windowContext,
windowContext,
payload,
);
return value;
@@ -505,7 +603,7 @@ export class PluginInstance {
listNames: async () => {
const payload = { type: 'list_cookie_names_request' } as const;
const { names } = await this.#sendAndWaitForReply<ListCookieNamesResponse>(
event.windowContext,
windowContext,
payload,
);
return names;
@@ -519,17 +617,17 @@ export class PluginInstance {
render: async (args) => {
const payload = { type: 'template_render_request', ...args } as const;
const result = await this.#sendAndWaitForReply<TemplateRenderResponse>(
event.windowContext,
windowContext,
payload,
);
return result.data;
return result.data as any;
},
},
store: {
get: async <T>(key: string) => {
const payload = { type: 'get_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<GetKeyValueResponse>(
event.windowContext,
windowContext,
payload,
);
return result.value ? (JSON.parse(result.value) as T) : undefined;
@@ -541,17 +639,22 @@ export class PluginInstance {
key,
value: valueStr,
};
await this.#sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
await this.#sendAndWaitForReply<GetKeyValueResponse>(windowContext, payload);
},
delete: async (key: string) => {
const payload = { type: 'delete_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<DeleteKeyValueResponse>(
event.windowContext,
windowContext,
payload,
);
return result.deleted;
},
},
plugin: {
reload: () => {
this.#sendPayload({ type: 'none' }, { type: 'reload_response', silent: true }, null);
},
},
};
}
}
@@ -579,20 +682,20 @@ function applyFormInputDefaults(
}
}
const watchedFiles: Record<string, Stats> = {};
const watchedFiles: Record<string, Stats | null> = {};
/**
* Watch a file and trigger callback on change.
* Watch a file and trigger a callback on change.
*
* We also track the stat for each file because fs.watch() will
* trigger a "change" event when the access date changes
* trigger a "change" event when the access date changes.
*/
function watchFile(filepath: string, cb: (filepath: string) => void) {
function watchFile(filepath: string, cb: () => void) {
watch(filepath, () => {
const stat = statSync(filepath);
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
cb(filepath);
const stat = statSync(filepath, { throwIfNoEntry: false });
if (stat == null || stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
watchedFiles[filepath] = stat ?? null;
cb();
}
watchedFiles[filepath] = stat;
});
}

View File

@@ -46,10 +46,14 @@ async function handleIncoming(msg: string) {
}
if (pluginEvent.payload.type === 'terminate_request') {
plugin.terminate();
await plugin.terminate();
console.log('Terminated plugin worker', pluginEvent.pluginRefId);
delete plugins[pluginEvent.pluginRefId];
}
plugin.sendToWorker(pluginEvent);
}
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

View File

@@ -1,6 +1,8 @@
import { TemplateFunction } from '@yaakapp/api';
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
export function migrateTemplateFunctionSelectOptions(f: TemplateFunction): TemplateFunction {
export function migrateTemplateFunctionSelectOptions(
f: TemplateFunctionPlugin,
): TemplateFunctionPlugin {
const migratedArgs = f.args.map((a) => {
if (a.type === 'select') {
a.options = a.options.map((o) => ({

View File

@@ -0,0 +1,68 @@
# Copy as cUrl
A request action plugin for Yaak that converts HTTP requests into [curl](https://curl.se)
commands, making it easy to share, debug, and execute requests outside Yaak.
![Screenshot of context menu](screenshot.png)
## Overview
This plugin adds a 'Copy as Curl' action to HTTP requests, converting any request into its
equivalent curl command. This is useful for debugging, sharing requests with team members,
and executing requests in terminal environments where `curl` is available.
## How It Works
The plugin analyzes the given HTTP request and generates a properly formatted curl command
that includes:
- HTTP method (GET, POST, PUT, DELETE, etc.)
- Request URL with query parameters
- Headers (including authentication headers)
- Request body (for POST, PUT, PATCH requests)
- Authentication credentials
## Usage
1. Configure an HTTP request as usual in Yaak
2. Right-click on the request in the sidebar
3. Select 'Copy as Curl'
4. The command is copied to your clipboard
5. Share or execute the command
## Generated Curl Examples
### Simple GET Request
```bash
curl -X GET 'https://api.example.com/users' \
--header 'Accept: application/json'
```
### POST Request with JSON Data
```bash
curl -X POST 'https://api.example.com/users' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{
"name": "John Doe",
"email": "john@example.com"
}'
```
### Request with Multi-part Form Data
```bash
curl -X POST 'yaak.app' \
--header 'Content-Type: multipart/form-data' \
--form 'hello=world' \
--form file=@/path/to/file.json
```
### Request with Authentication
```bash
curl -X GET 'https://api.example.com/protected' \
--user 'username:password'
```

View File

@@ -0,0 +1,18 @@
{
"name": "@yaak/action-copy-curl",
"displayName": "Copy as Curl",
"description": "Copy request as a curl command",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/action-copy-curl"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

View File

@@ -0,0 +1,125 @@
import type { HttpRequest, PluginDefinition } from '@yaakapp/api';
const NEWLINE = '\\\n ';
export const plugin: PluginDefinition = {
httpRequestActions: [
{
label: 'Copy as Curl',
icon: 'copy',
async onSelect(ctx, args) {
const rendered_request = await ctx.httpRequest.render({
httpRequest: args.httpRequest,
purpose: 'preview',
});
const data = await convertToCurl(rendered_request);
await ctx.clipboard.copyText(data);
await ctx.toast.show({
message: 'Command copied to clipboard',
icon: 'copy',
color: 'success',
});
},
},
],
};
export async function convertToCurl(request: Partial<HttpRequest>) {
const xs = ['curl'];
// Add method and URL all on first line
if (request.method) xs.push('-X', request.method);
// Build final URL with parameters (compatible with old curl)
let finalUrl = request.url || '';
const urlParams = (request.urlParameters ?? []).filter(onlyEnabled);
if (urlParams.length > 0) {
// Build url
const [base, hash] = finalUrl.split('#');
const separator = base!.includes('?') ? '&' : '?';
const queryString = urlParams
.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value)}`)
.join('&');
finalUrl = base + separator + queryString + (hash ? `#${hash}` : '');
}
xs.push(quote(finalUrl));
xs.push(NEWLINE);
// Add headers
for (const h of (request.headers ?? []).filter(onlyEnabled)) {
xs.push('--header', quote(`${h.name}: ${h.value}`));
xs.push(NEWLINE);
}
// Add form params
const type = request.bodyType ?? 'none';
if (
(type === 'multipart/form-data' || type === 'application/x-www-form-urlencoded') &&
Array.isArray(request.body?.form)
) {
const flag = request.bodyType === 'multipart/form-data' ? '--form' : '--data';
for (const p of (request.body?.form ?? []).filter(onlyEnabled)) {
if (p.file) {
let v = `${p.name}=@${p.file}`;
v += p.contentType ? `;type=${p.contentType}` : '';
xs.push(flag, v);
} else {
xs.push(flag, quote(`${p.name}=${p.value}`));
}
xs.push(NEWLINE);
}
} else if (type === 'graphql' && typeof request.body?.query === 'string') {
const body = {
query: request.body.query || '',
variables: maybeParseJSON(request.body.variables, undefined),
};
xs.push('--data', quote(JSON.stringify(body)));
xs.push(NEWLINE);
} else if (type !== 'none' && typeof request.body?.text === 'string') {
xs.push('--data', quote(request.body.text));
xs.push(NEWLINE);
}
// Add basic/digest authentication
if (request.authenticationType === 'basic' || request.authenticationType === 'digest') {
if (request.authenticationType === 'digest') xs.push('--digest');
xs.push(
'--user',
quote(`${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`),
);
xs.push(NEWLINE);
}
// Add bearer authentication
if (request.authenticationType === 'bearer') {
const value =
`${request.authentication?.prefix ?? 'Bearer'} ${request.authentication?.token ?? ''}`.trim();
xs.push('--header', quote(`Authorization: ${value}`));
xs.push(NEWLINE);
}
// Remove trailing newline
if (xs[xs.length - 1] === NEWLINE) {
xs.splice(xs.length - 1, 1);
}
return xs.join(' ');
}
function quote(arg: string): string {
const escaped = arg.replace(/'/g, "\\'");
return `'${escaped}'`;
}
function onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {
return v.enabled !== false && !!v.name;
}
function maybeParseJSON<T>(v: string, fallback: T) {
try {
return JSON.parse(v);
} catch {
return fallback;
}
}

View File

@@ -12,9 +12,20 @@ describe('exporter-curl', () => {
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual(
[`curl 'https://yaak.app'`, `--url-query 'a=aaa'`, `--url-query 'b=bbb'`].join(` \\\n `),
);
).toEqual([`curl 'https://yaak.app?a=aaa&b=bbb'`].join(` \\n `));
});
test('Exports GET with params and hash', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app/path#section',
urlParameters: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual([`curl 'https://yaak.app/path?a=aaa&b=bbb#section'`].join(` \\n `));
});
test('Exports POST with url form data', async () => {
expect(
@@ -47,7 +58,10 @@ describe('exporter-curl', () => {
},
}),
).toEqual(
[`curl -X POST 'https://yaak.app'`, `--data-raw '{"query":"{foo,bar}","variables":{"a":"aaa","b":"bbb"}}'`].join(` \\\n `),
[
`curl -X POST 'https://yaak.app'`,
`--data '{"query":"{foo,bar}","variables":{"a":"aaa","b":"bbb"}}'`,
].join(` \\\n `),
);
});
@@ -62,7 +76,7 @@ describe('exporter-curl', () => {
},
}),
).toEqual(
[`curl -X POST 'https://yaak.app'`, `--data-raw '{"query":"{foo,bar}"}'`].join(` \\\n `),
[`curl -X POST 'https://yaak.app'`, `--data '{"query":"{foo,bar}"}'`].join(` \\\n `),
);
});
@@ -106,7 +120,7 @@ describe('exporter-curl', () => {
[
`curl -X POST 'https://yaak.app'`,
`--header 'Content-Type: application/json'`,
`--data-raw '{"foo":"bar\\'s"}'`,
`--data '{"foo":"bar\\'s"}'`,
].join(` \\\n `),
);
});
@@ -126,7 +140,7 @@ describe('exporter-curl', () => {
[
`curl -X POST 'https://yaak.app'`,
`--header 'Content-Type: application/json'`,
`--data-raw '{"foo":"bar",\n"baz":"qux"}'`,
`--data '{"foo":"bar",\n"baz":"qux"}'`,
].join(` \\\n `),
);
});
@@ -140,7 +154,7 @@ describe('exporter-curl', () => {
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual([`curl`, `--header 'a: aaa'`, `--header 'b: bbb'`].join(` \\\n `));
).toEqual([`curl ''`, `--header 'a: aaa'`, `--header 'b: bbb'`].join(` \\\n `));
});
test('Basic auth', async () => {
@@ -191,6 +205,34 @@ describe('exporter-curl', () => {
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer tok'`].join(` \\\n `));
});
test('Bearer auth with custom prefix', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
token: 'abc123',
prefix: 'Token',
},
}),
).toEqual(
[`curl 'https://yaak.app'`, `--header 'Authorization: Token abc123'`].join(` \\\n `),
);
});
test('Bearer auth with empty prefix', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
token: 'xyz789',
prefix: '',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: xyz789'`].join(` \\\n `));
});
test('Broken bearer auth', async () => {
expect(
await convertToCurl({
@@ -201,6 +243,18 @@ describe('exporter-curl', () => {
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer '`].join(` \\\n `));
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer'`].join(` \\\n `));
});
test('Stale body data', async () => {
expect(
await convertToCurl({
url: 'https://yaak.app',
bodyType: 'none',
body: {
text: 'ignore me',
},
}),
).toEqual([`curl 'https://yaak.app'`].join(` \\\n `));
});
});

View File

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

View File

@@ -0,0 +1,76 @@
# Copy as gRPCurl
An HTTP request action plugin that converts gRPC requests
into [gRPCurl](https://github.com/fullstorydev/grpcurl) commands, enabling easy sharing,
debugging, and execution of gRPC calls outside Yaak.
![Screenshot of context menu](screenshot.png)
## Overview
This plugin adds a "Copy as gRPCurl" action to gRPC requests, converting any gRPC request
into its equivalent executable command. This is useful for debugging gRPC services,
sharing requests with team members, or executing gRPC calls in terminal environments where
`grpcurl` is available.
## How It Works
The plugin analyzes your gRPC request configuration and generates a properly formatted
`grpcurl` command that includes:
- gRPC service and method names
- Server address and port
- Request message data (JSON format)
- Metadata (headers)
- Authentication credentials
- Protocol buffer definitions
## Usage
1. Configure a gRPC request as usual in Yaak
2. Right-click on the request sidebar item
3. Select "Copy as gRPCurl" from the available actions
4. The command is copied to your clipboard
5. Share or execute the command
## Generated gRPCurl Examples
### Simple Unary Call
```bash
grpcurl -plaintext \
-d '{"name": "John Doe"}' \
localhost:9090 \
user.UserService/GetUser
```
### Call with Metadata
```bash
grpcurl -plaintext \
-H "authorization: Bearer my-token" \
-H "x-api-version: v1" \
-d '{"user_id": "12345"}' \
api.example.com:443 \
user.UserService/GetUserProfile
```
### Call with TLS
```bash
grpcurl \
-d '{"query": "search term"}' \
secure-api.example.com:443 \
search.SearchService/Search
```
### Call with Proto Files
```bash
grpcurl -import-path /path/to/protos \
-proto /other/path/to/user.proto \
-d '{"email": "user@example.com"}' \
localhost:9090 \
user.UserService/CreateUser
```

View File

@@ -0,0 +1,18 @@
{
"name": "@yaak/action-copy-grpcurl",
"displayName": "Copy as gRPCurl",
"description": "Copy gRPC request as a grpcurl command",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/action-copy-grpcurl"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

View File

@@ -0,0 +1,134 @@
import type { GrpcRequest, PluginDefinition } from '@yaakapp/api';
import path from 'node:path';
const NEWLINE = '\\\n ';
export const plugin: PluginDefinition = {
grpcRequestActions: [
{
label: 'Copy as gRPCurl',
icon: 'copy',
async onSelect(ctx, args) {
const rendered_request = await ctx.grpcRequest.render({
grpcRequest: args.grpcRequest,
purpose: 'preview',
});
const data = await convert(rendered_request, args.protoFiles);
await ctx.clipboard.copyText(data);
await ctx.toast.show({
message: 'Command copied to clipboard',
icon: 'copy',
color: 'success',
});
},
},
],
};
export async function convert(request: Partial<GrpcRequest>, allProtoFiles: string[]) {
const xs = ['grpcurl'];
if (request.url?.startsWith('http://')) {
xs.push('-plaintext');
}
const protoIncludes = allProtoFiles.filter((f) => !f.endsWith('.proto'));
const protoFiles = allProtoFiles.filter((f) => f.endsWith('.proto'));
const inferredIncludes = new Set<string>();
for (const f of protoFiles) {
const protoDir = findParentProtoDir(f);
if (protoDir) {
inferredIncludes.add(protoDir);
} else {
inferredIncludes.add(path.posix.join(f, '..'));
inferredIncludes.add(path.posix.join(f, '..', '..'));
}
}
for (const f of protoIncludes) {
xs.push('-import-path', quote(f));
xs.push(NEWLINE);
}
for (const f of inferredIncludes.values()) {
xs.push('-import-path', quote(f));
xs.push(NEWLINE);
}
for (const f of protoFiles) {
xs.push('-proto', quote(f));
xs.push(NEWLINE);
}
// Add headers
for (const h of (request.metadata ?? []).filter(onlyEnabled)) {
xs.push('-H', quote(`${h.name}: ${h.value}`));
xs.push(NEWLINE);
}
// Add basic authentication
if (request.authenticationType === 'basic') {
const user = request.authentication?.username ?? '';
const pass = request.authentication?.password ?? '';
const encoded = btoa(`${user}:${pass}`);
xs.push('-H', quote(`Authorization: Basic ${encoded}`));
xs.push(NEWLINE);
} else if (request.authenticationType === 'bearer') {
// Add bearer authentication
xs.push('-H', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`));
xs.push(NEWLINE);
}
// Add form params
if (request.message) {
xs.push('-d', `${quote(JSON.stringify(JSON.parse(request.message)))}`);
xs.push(NEWLINE);
}
// Add the server address
if (request.url) {
const server = request.url.replace(/^https?:\/\//, ''); // remove protocol
xs.push(server);
xs.push(NEWLINE);
}
// Add service + method
if (request.service && request.method) {
xs.push(`${request.service}/${request.method}`);
xs.push(NEWLINE);
}
// Remove trailing newline
if (xs[xs.length - 1] === NEWLINE) {
xs.splice(xs.length - 1, 1);
}
return xs.join(' ');
}
function quote(arg: string): string {
const escaped = arg.replace(/'/g, "\\'");
return `'${escaped}'`;
}
function onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {
return v.enabled !== false && !!v.name;
}
function findParentProtoDir(startPath: string): string | null {
let dir = path.resolve(startPath);
while (true) {
if (path.basename(dir) === 'proto') {
return dir;
}
const parent = path.dirname(dir);
if (parent === dir) {
return null; // Reached root
}
dir = parent;
}
}

View File

@@ -0,0 +1,110 @@
import { describe, expect, test } from 'vitest';
import { convert } from '../src';
describe('exporter-curl', () => {
test('Simple example', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
},
[],
),
).toEqual([`grpcurl yaak.app`].join(` \\\n `));
});
test('Basic metadata', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
metadata: [
{ name: 'aaa', value: 'AAA' },
{ enabled: true, name: 'bbb', value: 'BBB' },
{ enabled: false, name: 'disabled', value: 'ddd' },
],
},
[],
),
).toEqual([`grpcurl -H 'aaa: AAA'`, `-H 'bbb: BBB'`, `yaak.app`].join(` \\\n `));
});
test('Single proto file', async () => {
expect(await convert({ url: 'https://yaak.app' }, ['/foo/bar/baz.proto'])).toEqual(
[
`grpcurl -import-path '/foo/bar'`,
`-import-path '/foo'`,
`-proto '/foo/bar/baz.proto'`,
`yaak.app`,
].join(` \\\n `),
);
});
test('Multiple proto files, same dir', async () => {
expect(
await convert({ url: 'https://yaak.app' }, ['/foo/bar/aaa.proto', '/foo/bar/bbb.proto']),
).toEqual(
[
`grpcurl -import-path '/foo/bar'`,
`-import-path '/foo'`,
`-proto '/foo/bar/aaa.proto'`,
`-proto '/foo/bar/bbb.proto'`,
`yaak.app`,
].join(` \\\n `),
);
});
test('Multiple proto files, different dir', async () => {
expect(
await convert({ url: 'https://yaak.app' }, ['/aaa/bbb/ccc.proto', '/xxx/yyy/zzz.proto']),
).toEqual(
[
`grpcurl -import-path '/aaa/bbb'`,
`-import-path '/aaa'`,
`-import-path '/xxx/yyy'`,
`-import-path '/xxx'`,
`-proto '/aaa/bbb/ccc.proto'`,
`-proto '/xxx/yyy/zzz.proto'`,
`yaak.app`,
].join(` \\\n `),
);
});
test('Single include dir', async () => {
expect(await convert({ url: 'https://yaak.app' }, ['/aaa/bbb'])).toEqual(
[`grpcurl -import-path '/aaa/bbb'`, `yaak.app`].join(` \\\n `),
);
});
test('Multiple include dir', async () => {
expect(await convert({ url: 'https://yaak.app' }, ['/aaa/bbb', '/xxx/yyy'])).toEqual(
[`grpcurl -import-path '/aaa/bbb'`, `-import-path '/xxx/yyy'`, `yaak.app`].join(` \\\n `),
);
});
test('Mixed proto and dirs', async () => {
expect(
await convert({ url: 'https://yaak.app' }, ['/aaa/bbb', '/xxx/yyy', '/foo/bar.proto']),
).toEqual(
[
`grpcurl -import-path '/aaa/bbb'`,
`-import-path '/xxx/yyy'`,
`-import-path '/foo'`,
`-import-path '/'`,
`-proto '/foo/bar.proto'`,
`yaak.app`,
].join(` \\\n `),
);
});
test('Sends data', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
message: JSON.stringify({ foo: 'bar', baz: 1.0 }, null, 2),
},
['/foo.proto'],
),
).toEqual(
[
`grpcurl -import-path '/'`,
`-proto '/foo.proto'`,
`-d '{"foo":"bar","baz":1}'`,
`yaak.app`,
].join(` \\\n `),
);
});
});

View File

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

View File

@@ -0,0 +1,17 @@
{
"name": "@yaak/auth-apikey",
"displayName": "API Key Authentication",
"description": "Authenticate requests using an API key",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-apikey"
},
"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,53 @@
import type { PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
authentication: {
name: 'apikey',
label: 'API Key',
shortLabel: 'API Key',
args: [
{
type: 'select',
name: 'location',
label: 'Behavior',
defaultValue: 'header',
options: [
{ label: 'Insert Header', value: 'header' },
{ label: 'Append Query Parameter', value: 'query' },
],
},
{
type: 'text',
name: 'key',
label: 'Key',
dynamic: (_ctx, { values }) => {
return values.location === 'query' ? {
label: 'Parameter Name',
description: 'The name of the query parameter to add to the request',
} : {
label: 'Header Name',
description: 'The name of the header to add to the request',
};
},
},
{
type: 'text',
name: 'value',
label: 'API Key',
optional: true,
password: true,
},
],
async onApply(_ctx, { values }) {
const key = String(values.key ?? '');
const value = String(values.value ?? '');
const location = String(values.location);
if (location === 'query') {
return { setQueryParameters: [{ name: key, value }] };
} else {
return { setHeaders: [{ name: key, value }] };
}
},
},
};

View File

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

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"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

View File

@@ -0,0 +1,97 @@
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: 'auth-aws-sig-v4',
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;
}
}
// TODO: Support body signing here
headers['x-amz-content-sha256'] = 'UNSIGNED-PAYLOAD';
const signature = aws4.sign(
{
host: url.host,
method: args.method,
path: url.pathname + (url.search || '') || undefined,
service: String(values.service || 'sts') || undefined,
region: String(values.region || 'us-east-1') || undefined,
headers,
},
{
accessKeyId,
secretAccessKey,
sessionToken,
},
);
// After signing, aws4 will set:
// - opts.headers["Authorization"]
// - opts.headers["X-Amz-Date"]
// - optionally content sha256 header etc
console.log('ADDING STUFF', signature);
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

@@ -0,0 +1,44 @@
# Basic Authentication
A simple Basic Authentication plugin that implements HTTP Basic Auth according
to [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617), enabling secure
authentication with username and password credentials.
![Screenshot of basic auth UI](screenshot.png)
## Overview
This plugin provides HTTP Basic Authentication support for API requests in Yaak. Basic
Auth is one of the most widely supported authentication methods, making it ideal for APIs
that require simple username/password authentication without the complexity of OAuth
flows.
## How Basic Authentication Works
Basic Authentication encodes your username and password credentials using Base64 encoding
and sends them in the `Authorization` header with each request. The format is:
```
Authorization: Basic <base64-encoded-credentials>
```
Where `<base64-encoded-credentials>` is the Base64 encoding of `username:password`.
## Configuration
The plugin presents two fields:
- **Username**: Username or user identifier
- **Password**: Password or authentication token
## Usage
1. Configure the request, folder, or workspace to use Basic Authentication
2. Enter your username and password in the authentication configuration
3. The plugin will automatically add the proper `Authorization` header to your requests
## Troubleshooting
- **401 Unauthorized**: Verify your username and password are correct
- **403 Forbidden**: Check if your account has the necessary permissions
- **Connection Issues**: Ensure you're using HTTPS for secure transmission

View File

@@ -1,9 +1,17 @@
{
"name": "@yaakapp/auth-basic",
"name": "@yaak/auth-basic",
"displayName": "Basic Authentication",
"description": "Authenticate requests using Basic Auth",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-basic"
},
"private": true,
"version": "0.0.1",
"version": "0.1.0",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

View File

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

View File

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

View File

@@ -0,0 +1,47 @@
# Bearer Token Authentication Plugin
A Bearer Token authentication plugin for Yaak that
implements [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750), enabling secure API
access using tokens, API keys, and other bearer credentials.
![Screenshot of bearer auth UI](screenshot.png)
## Overview
This plugin provides Bearer Token authentication support for your API requests in Yaak.
Bearer Token authentication is widely used in modern APIs, especially those following REST
principles and OAuth 2.0 standards. It's the preferred method for APIs that issue access
tokens, API keys, or other bearer credentials.
## How Bearer Token Authentication Works
Bearer Token authentication sends your token in the `Authorization` header with each
request using the Bearer scheme:
```
Authorization: Bearer <your-token>
```
The token is transmitted as-is without any additional encoding, making it simple and
efficient for API authentication.
## Configuration
The plugin requires only one field:
- **Token**: Your bearer token, access token, API key, or other credential
- **Prefix**: The prefix to use for the Authorization header, which will be of the
format "<PREFIX> <TOKEN>"
## Usage
1. Configure the request, folder, or workspace to use Bearer Authentication
2. Enter the token and optional prefix in the authentication configuration
3. The plugin will automatically add the proper `Authorization` header to your requests
## Troubleshooting
- **401 Unauthorized**: Verify your token is valid and not expired
- **403 Forbidden**: Check if your token has the necessary permissions/scopes
- **Invalid Token Format**: Ensure you're using the complete token without truncation
- **Token Expiration**: Refresh or regenerate expired tokens

View File

@@ -1,9 +1,18 @@
{
"name": "@yaakapp/auth-bearer",
"name": "@yaak/auth-bearer",
"displayName": "Bearer Authentication",
"description": "Authenticate requests using bearer authentication",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-bearer"
},
"private": true,
"version": "0.0.1",
"version": "0.1.0",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

View File

@@ -1,21 +1,39 @@
import { PluginDefinition } from '@yaakapp/api';
import type { CallHttpAuthenticationRequest } from '@yaakapp-internal/plugins';
import type { PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
authentication: {
name: 'bearer',
label: 'Bearer Token',
shortLabel: 'Bearer',
args: [{
type: 'text',
name: 'token',
label: 'Token',
optional: true,
password: true,
}],
args: [
{
type: 'text',
name: 'token',
label: 'Token',
optional: true,
password: true,
},
{
type: 'text',
name: 'prefix',
label: 'Prefix',
optional: true,
placeholder: '',
defaultValue: 'Bearer',
description:
'The prefix to use for the Authorization header, which will be of the format "<PREFIX> <TOKEN>".',
},
],
async onApply(_ctx, { values }) {
const { token } = values;
const value = `Bearer ${token}`.trim();
return { setHeaders: [{ name: 'Authorization', value }] };
return { setHeaders: [generateAuthorizationHeader(values)] };
},
},
};
function generateAuthorizationHeader(values: CallHttpAuthenticationRequest['values']) {
const token = String(values.token || '').trim();
const prefix = String(values.prefix || '').trim();
const value = `${prefix} ${token}`.trim();
return { name: 'Authorization', value };
}

View File

@@ -0,0 +1,67 @@
import type { Context } from '@yaakapp/api';
import { describe, expect, test } from 'vitest';
import { plugin } from '../src';
const ctx = {} as Context;
describe('auth-bearer', () => {
test('No values', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
values: {},
headers: [],
url: 'https://yaak.app',
method: 'POST',
contextId: '111',
}),
).toEqual({ setHeaders: [{ name: 'Authorization', value: '' }] });
});
test('Only token', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
values: { token: 'my-token' },
headers: [],
url: 'https://yaak.app',
method: 'POST',
contextId: '111',
}),
).toEqual({ setHeaders: [{ name: 'Authorization', value: 'my-token' }] });
});
test('Only prefix', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
values: { prefix: 'Hello' },
headers: [],
url: 'https://yaak.app',
method: 'POST',
contextId: '111',
}),
).toEqual({ setHeaders: [{ name: 'Authorization', value: 'Hello' }] });
});
test('Prefix and token', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
values: { prefix: 'Hello', token: 'my-token' },
headers: [],
url: 'https://yaak.app',
method: 'POST',
contextId: '111',
}),
).toEqual({ setHeaders: [{ name: 'Authorization', value: 'Hello my-token' }] });
});
test('Extra spaces', async () => {
expect(
await plugin.authentication!.onApply(ctx, {
values: { prefix: '\t Hello ', token: ' \nmy-token ' },
headers: [],
url: 'https://yaak.app',
method: 'POST',
contextId: '111',
}),
).toEqual({ setHeaders: [{ name: 'Authorization', value: 'Hello my-token' }] });
});
});

View File

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

View File

@@ -0,0 +1,53 @@
# JSON Web Token (JWT) Authentication
A [JSON Web Token](https://datatracker.ietf.org/doc/html/rfc7519) (JWT) authentication
plugin that supports token generation, signing, and automatic header management.
![Screenshot of JWT auth UI](screenshot.png)
## Overview
This plugin provides JWT authentication support for API requests. JWT is a compact,
URL-safe means of representing claims between two parties, commonly used for
authentication and information exchange in modern web applications and APIs.
## How JWT Authentication Works
JWT authentication involves creating a signed token containing claims about the user or
application. The token is sent in the `Authorization` header:
```
Authorization: Bearer <jwt-token>
```
A JWT consists of three parts separated by dots:
- **Header**: Contains the token type and signing algorithm
- **Payload**: Contains the claims (user data, permissions, expiration, etc.)
- **Signature**: Ensures the token hasn't been tampered with
## Usage
1. Configure the request, folder, or workspace to use JWT Authentication
2. Set up your signing algorithm and secret/key
3. Configure the required claims for your JWT
4. The plugin will generate, sign, and include the JWT in your requests
## Common Use Cases
JWT authentication is commonly used for:
- **Microservices Authentication**: Service-to-service communication
- **API Gateway Integration**: Authenticating with API gateways
- **Single Sign-On (SSO)**: Sharing authentication across applications
- **Stateless Authentication**: No server-side session storage required
- **Mobile App APIs**: Secure authentication for mobile applications
- **Third-party Integrations**: Authenticating with external services
## Troubleshooting
- **Invalid Signature**: Check your secret/key and algorithm configuration
- **Token Expired**: Verify expiration time settings
- **Invalid Claims**: Ensure required claims are properly configured
- **Algorithm Mismatch**: Verify the algorithm matches what the API expects
- **Key Format Issues**: Ensure RSA keys are in the correct PEM format

View File

@@ -1,10 +1,18 @@
{
"name": "@yaakapp/auth-jwt",
"name": "@yaak/auth-jwt",
"displayName": "JSON Web Tokens",
"description": "Authenticate requests using JSON web tokens (JWT)",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-jwt"
},
"private": true,
"version": "0.0.1",
"version": "0.1.0",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
},
"dependencies": {
"jsonwebtoken": "^9.0.2"

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

View File

@@ -1,4 +1,4 @@
import { PluginDefinition } from '@yaakapp/api';
import type { PluginDefinition } from '@yaakapp/api';
import jwt from 'jsonwebtoken';
const algorithms = [
@@ -20,49 +20,49 @@ const algorithms = [
const defaultAlgorithm = algorithms[0];
export const plugin: PluginDefinition = {
authentication: {
name: 'jwt',
label: 'JWT Bearer',
shortLabel: 'JWT',
args: [
{
type: 'select',
name: 'algorithm',
label: 'Algorithm',
hideLabel: true,
defaultValue: defaultAlgorithm,
options: algorithms.map(value => ({ label: value === 'none' ? 'None' : value, value })),
},
{
type: 'text',
name: 'secret',
label: 'Secret or Private Key',
password: true,
optional: true,
multiLine: true,
},
{
type: 'checkbox',
name: 'secretBase64',
label: 'Secret is base64 encoded',
},
{
type: 'editor',
name: 'payload',
label: 'Payload',
language: 'json',
defaultValue: '{\n "foo": "bar"\n}',
placeholder: '{ }',
},
],
async onApply(_ctx, { values }) {
const { algorithm, secret: _secret, secretBase64, payload } = values;
const secret = secretBase64 ? Buffer.from(`${_secret}`, 'base64') : `${_secret}`;
const token = jwt.sign(`${payload}`, secret, { algorithm: algorithm as any });
const value = `Bearer ${token}`;
return { setHeaders: [{ name: 'Authorization', value }] };
}
,
authentication: {
name: 'jwt',
label: 'JWT Bearer',
shortLabel: 'JWT',
args: [
{
type: 'select',
name: 'algorithm',
label: 'Algorithm',
hideLabel: true,
defaultValue: defaultAlgorithm,
options: algorithms.map((value) => ({ label: value === 'none' ? 'None' : value, value })),
},
{
type: 'text',
name: 'secret',
label: 'Secret or Private Key',
password: true,
optional: true,
multiLine: true,
},
{
type: 'checkbox',
name: 'secretBase64',
label: 'Secret is base64 encoded',
},
{
type: 'editor',
name: 'payload',
label: 'Payload',
language: 'json',
defaultValue: '{\n "foo": "bar"\n}',
placeholder: '{ }',
},
],
async onApply(_ctx, { values }) {
const { algorithm, secret: _secret, secretBase64, payload } = values;
const secret = secretBase64 ? Buffer.from(`${_secret}`, 'base64') : `${_secret}`;
const token = jwt.sign(`${payload}`, secret, {
algorithm: algorithm as (typeof algorithms)[number],
});
const value = `Bearer ${token}`;
return { setHeaders: [{ name: 'Authorization', value }] };
},
}
;
},
};

View File

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

View File

@@ -0,0 +1,72 @@
# OAuth 2.0 Authentication
An [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749) authentication plugin that
supports multiple grant types and flows, enabling secure API authentication with OAuth 2.0
providers.
![Screenshot of OAuth 2.0 auth UI](screenshot.png)
## Overview
This plugin implements OAuth 2.0 authentication for requests, supporting the most common
OAuth 2.0 grant types used in modern API integrations. It handles token management,
automatic refresh, and [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) (Proof Key
for Code Exchange) for enhanced security.
## Supported Grant Types
### Authorization Code Flow
The most secure and commonly used OAuth 2.0 flow for web applications.
- Standard Authorization Code flow
- Optional PKCE (Proof Key for Code Exchange) for enhanced security
- Supports automatic token refresh
### Client Credentials Flow
Ideal for server-to-server authentication where no user interaction is required.
### Implicit Flow
Legacy flow for single-page applications (deprecated but still supported):
- Direct access token retrieval
- No refresh token support
- Suitable for legacy integrations
### Resource Owner Password Credentials Flow
Direct username/password authentication.
- User credentials are exchanged directly for tokens
- Should only be used with trusted applications
- Supports automatic token refresh
## Features
- **Automatic Token Management**: Handles token storage, expiration, and refresh
automatically
- **PKCE Support**: Enhanced security for Authorization Code flow
- **Token Persistence**: Stores tokens between sessions
- **Flexible Configuration**: Supports custom authorization and token endpoints
- **Scope Management**: Configure required OAuth scopes for your API
- **Error Handling**: Comprehensive error handling and user feedback
## Usage
1. Configure the request, folder, or workspace to use OAuth 2.0 Authentication
2. Select the appropriate grant type for your use case
3. Fill in the required OAuth 2.0 parameters from your API provider
4. The plugin will handle the authentication flow and token management automatically
## Compatibility
This plugin is compatible with OAuth 2.0 providers including:
- Google APIs
- Microsoft Graph
- GitHub API
- Auth0
- Okta
- And many other OAuth 2.0 compliant services

View File

@@ -1,9 +1,18 @@
{
"name": "@yaakapp/auth-oauth2",
"name": "@yaak/auth-oauth2",
"displayName": "OAuth 2.0",
"description": "Authenticate requests using OAuth 2.0",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-oauth2"
},
"private": true,
"version": "0.0.1",
"version": "0.1.0",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

View File

@@ -1,8 +1,8 @@
import { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api';
import type { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api';
import { readFileSync } from 'node:fs';
import { AccessTokenRawResponse } from './store';
import type { AccessTokenRawResponse } from './store';
export async function getAccessToken(
export async function fetchAccessToken(
ctx: Context,
{
accessTokenUrl,

View File

@@ -1,29 +1,34 @@
import { Context, HttpRequest } from '@yaakapp/api';
import type { Context, HttpRequest } from '@yaakapp/api';
import { readFileSync } from 'node:fs';
import { AccessToken, AccessTokenRawResponse, deleteToken, getToken, storeToken } from './store';
import type { AccessToken, AccessTokenRawResponse, TokenStoreArgs } from './store';
import { deleteToken, getToken, storeToken } from './store';
import { isTokenExpired } from './util';
export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
scope,
accessTokenUrl,
credentialsInBody,
clientId,
clientSecret,
forceRefresh,
}: {
scope: string | null;
accessTokenUrl: string;
credentialsInBody: boolean;
clientId: string;
clientSecret: string;
forceRefresh?: boolean;
}): Promise<AccessToken | null> {
const token = await getToken(ctx, contextId);
export async function getOrRefreshAccessToken(
ctx: Context,
tokenArgs: TokenStoreArgs,
{
scope,
accessTokenUrl,
credentialsInBody,
clientId,
clientSecret,
forceRefresh,
}: {
scope: string | null;
accessTokenUrl: string;
credentialsInBody: boolean;
clientId: string;
clientSecret: string;
forceRefresh?: boolean;
},
): Promise<AccessToken | null> {
const token = await getToken(ctx, tokenArgs);
if (token == null) {
return null;
}
const now = Date.now();
const isExpired = token.expiresAt && now > token.expiresAt;
const isExpired = isTokenExpired(token);
// Return the current access token if it's still valid
if (!isExpired && !forceRefresh) {
@@ -70,7 +75,7 @@ export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
// Bad refresh token, so we'll force it to fetch a fresh access token by deleting
// and returning null;
console.log('[oauth2] Unauthorized refresh_token request');
await deleteToken(ctx, contextId);
await deleteToken(ctx, tokenArgs);
return null;
}
@@ -79,7 +84,9 @@ export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
console.log('[oauth2] Got refresh token response', resp.status);
if (resp.status < 200 || resp.status >= 300) {
throw new Error('Failed to refresh access token with status=' + resp.status + ' and body=' + body);
throw new Error(
'Failed to refresh access token with status=' + resp.status + ' and body=' + body,
);
}
let response;
@@ -90,7 +97,9 @@ export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
}
if (response.error) {
throw new Error(`Failed to fetch access token with ${response.error} -> ${response.error_description}`);
throw new Error(
`Failed to fetch access token with ${response.error} -> ${response.error_description}`,
);
}
const newResponse: AccessTokenRawResponse = {
@@ -99,5 +108,5 @@ export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
refresh_token: response.refresh_token ?? token.response.refresh_token,
};
return storeToken(ctx, contextId, newResponse);
return storeToken(ctx, tokenArgs, newResponse);
}

View File

@@ -1,8 +1,10 @@
import { Context } from '@yaakapp/api';
import type { Context } from '@yaakapp/api';
import { createHash, randomBytes } from 'node:crypto';
import { getAccessToken } from '../getAccessToken';
import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import { AccessToken, getDataDirKey, storeToken } from '../store';
import type { AccessToken, TokenStoreArgs } from '../store';
import { getDataDirKey, storeToken } from '../store';
import { extractCode } from '../util';
export const PKCE_SHA256 = 'S256';
export const PKCE_PLAIN = 'plain';
@@ -34,13 +36,20 @@ export async function getAuthorizationCode(
audience: string | null;
credentialsInBody: boolean;
pkce: {
challengeMethod: string | null;
codeVerifier: string | null;
challengeMethod: string;
codeVerifier: string;
} | null;
tokenName: 'access_token' | 'id_token';
},
): Promise<AccessToken> {
const token = await getOrRefreshAccessToken(ctx, contextId, {
const tokenArgs: TokenStoreArgs = {
contextId,
clientId,
accessTokenUrl,
authorizationUrl: authorizationUrlRaw,
};
const token = await getOrRefreshAccessToken(ctx, tokenArgs, {
accessTokenUrl,
scope,
clientId,
@@ -51,7 +60,12 @@ export async function getAuthorizationCode(
return token;
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
let authorizationUrl: URL;
try {
authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
} catch {
throw new Error(`Invalid authorization URL "${authorizationUrlRaw}"`);
}
authorizationUrl.searchParams.set('response_type', 'code');
authorizationUrl.searchParams.set('client_id', clientId);
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
@@ -59,79 +73,75 @@ export async function getAuthorizationCode(
if (state) authorizationUrl.searchParams.set('state', state);
if (audience) authorizationUrl.searchParams.set('audience', audience);
if (pkce) {
const verifier = pkce.codeVerifier || createPkceCodeVerifier();
const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD;
authorizationUrl.searchParams.set(
'code_challenge',
createPkceCodeChallenge(verifier, challengeMethod),
pkceCodeChallenge(pkce.codeVerifier, pkce.challengeMethod),
);
authorizationUrl.searchParams.set('code_challenge_method', challengeMethod);
authorizationUrl.searchParams.set('code_challenge_method', pkce.challengeMethod);
}
return new Promise(async (resolve, reject) => {
const authorizationUrlStr = authorizationUrl.toString();
const logsEnabled = (await ctx.store.get('enable_logs')) ?? false;
console.log('[oauth2] Authorizing', authorizationUrlStr);
const dataDirKey = await getDataDirKey(ctx, contextId);
const authorizationUrlStr = authorizationUrl.toString();
console.log('[oauth2] Authorizing', authorizationUrlStr);
// eslint-disable-next-line no-async-promise-executor
const code = await new Promise<string>(async (resolve, reject) => {
let foundCode = false;
let { close } = await ctx.window.openUrl({
const { close } = await ctx.window.openUrl({
dataDirKey,
url: authorizationUrlStr,
label: 'oauth-authorization-url',
dataDirKey: await getDataDirKey(ctx, contextId),
async onClose() {
if (!foundCode) {
reject(new Error('Authorization window closed'));
}
},
async onNavigate({ url: urlStr }) {
const url = new URL(urlStr);
if (logsEnabled) console.log('[oauth2] Navigated to', urlStr);
if (url.searchParams.has('error')) {
return reject(new Error(`Failed to authorize: ${url.searchParams.get('error')}`));
let code;
try {
code = extractCode(urlStr, redirectUri);
} catch (err) {
reject(err);
close();
return;
}
const code = url.searchParams.get('code');
if (!code) {
console.log('[oauth2] Code not found');
return; // Could be one of many redirects in a chain, so skip it
return;
}
// Close the window here, because we don't need it anymore!
foundCode = true;
close();
console.log('[oauth2] Code found');
const response = await getAccessToken(ctx, {
grantType: 'authorization_code',
accessTokenUrl,
clientId,
clientSecret,
scope,
audience,
credentialsInBody,
params: [
{ name: 'code', value: code },
...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []),
],
});
try {
resolve(await storeToken(ctx, contextId, response, tokenName));
} catch (err) {
reject(err);
}
resolve(code);
},
});
});
console.log('[oauth2] Code found');
const response = await fetchAccessToken(ctx, {
grantType: 'authorization_code',
accessTokenUrl,
clientId,
clientSecret,
scope,
audience,
credentialsInBody,
params: [
{ name: 'code', value: code },
...(pkce ? [{ name: 'code_verifier', value: pkce.codeVerifier }] : []),
...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []),
],
});
return storeToken(ctx, tokenArgs, response, tokenName);
}
function createPkceCodeVerifier() {
export function genPkceCodeVerifier() {
return encodeForPkce(randomBytes(32));
}
function createPkceCodeChallenge(verifier: string, method: string) {
function pkceCodeChallenge(verifier: string, method: string) {
if (method === 'plain') {
return verifier;
}

View File

@@ -1,6 +1,8 @@
import { Context } from '@yaakapp/api';
import { getAccessToken } from '../getAccessToken';
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import type { TokenStoreArgs } from '../store';
import { getToken, storeToken } from '../store';
import { isTokenExpired } from '../util';
export async function getClientCredentials(
ctx: Context,
@@ -21,14 +23,18 @@ export async function getClientCredentials(
credentialsInBody: boolean;
},
) {
const token = await getToken(ctx, contextId);
if (token) {
// resolve(token.response.access_token);
// TODO: Refresh token if expired
// return;
const tokenArgs: TokenStoreArgs = {
contextId,
clientId,
accessTokenUrl,
authorizationUrl: null,
};
const token = await getToken(ctx, tokenArgs);
if (token && !isTokenExpired(token)) {
return token;
}
const response = await getAccessToken(ctx, {
const response = await fetchAccessToken(ctx, {
grantType: 'client_credentials',
accessTokenUrl,
audience,
@@ -39,5 +45,5 @@ export async function getClientCredentials(
params: [],
});
return storeToken(ctx, contextId, response);
return storeToken(ctx, tokenArgs, response);
}

View File

@@ -1,7 +1,9 @@
import { Context } from '@yaakapp/api';
import { AccessToken, AccessTokenRawResponse, getToken, storeToken } from '../store';
import type { Context } from '@yaakapp/api';
import type { AccessToken, AccessTokenRawResponse} from '../store';
import { getDataDirKey , getToken, storeToken } from '../store';
import { isTokenExpired } from '../util';
export function getImplicit(
export async function getImplicit(
ctx: Context,
contextId: string,
{
@@ -24,31 +26,43 @@ export function getImplicit(
tokenName: 'access_token' | 'id_token';
},
): Promise<AccessToken> {
return new Promise(async (resolve, reject) => {
const token = await getToken(ctx, contextId);
if (token) {
// resolve(token.response.access_token);
// TODO: Refresh token if expired
// return;
}
const tokenArgs = {
contextId,
clientId,
accessTokenUrl: null,
authorizationUrl: authorizationUrlRaw,
};
const token = await getToken(ctx, tokenArgs);
if (token != null && !isTokenExpired(token)) {
return token;
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
authorizationUrl.searchParams.set('response_type', 'token');
authorizationUrl.searchParams.set('client_id', clientId);
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
if (scope) authorizationUrl.searchParams.set('scope', scope);
if (state) authorizationUrl.searchParams.set('state', state);
if (audience) authorizationUrl.searchParams.set('audience', audience);
if (responseType.includes('id_token')) {
authorizationUrl.searchParams.set(
'nonce',
String(Math.floor(Math.random() * 9999999999999) + 1),
);
}
let authorizationUrl: URL;
try {
authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`);
} catch {
throw new Error(`Invalid authorization URL "${authorizationUrlRaw}"`);
}
authorizationUrl.searchParams.set('response_type', 'token');
authorizationUrl.searchParams.set('client_id', clientId);
if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri);
if (scope) authorizationUrl.searchParams.set('scope', scope);
if (state) authorizationUrl.searchParams.set('state', state);
if (audience) authorizationUrl.searchParams.set('audience', audience);
if (responseType.includes('id_token')) {
authorizationUrl.searchParams.set(
'nonce',
String(Math.floor(Math.random() * 9999999999999) + 1),
);
}
const authorizationUrlStr = authorizationUrl.toString();
// eslint-disable-next-line no-async-promise-executor
const newToken = await new Promise<AccessToken>(async (resolve, reject) => {
let foundAccessToken = false;
let { close } = await ctx.window.openUrl({
const authorizationUrlStr = authorizationUrl.toString();
const dataDirKey = await getDataDirKey(ctx, contextId);
const { close } = await ctx.window.openUrl({
dataDirKey,
url: authorizationUrlStr,
label: 'oauth-authorization-url',
async onClose() {
@@ -76,11 +90,13 @@ export function getImplicit(
const response = Object.fromEntries(params) as unknown as AccessTokenRawResponse;
try {
resolve(await storeToken(ctx, contextId, response));
resolve(storeToken(ctx, tokenArgs, response));
} catch (err) {
reject(err);
}
},
});
});
return newToken;
}

View File

@@ -1,7 +1,8 @@
import { Context } from '@yaakapp/api';
import { getAccessToken } from '../getAccessToken';
import type { Context } from '@yaakapp/api';
import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import { AccessToken, storeToken } from '../store';
import type { AccessToken, TokenStoreArgs } from '../store';
import { storeToken } from '../store';
export async function getPassword(
ctx: Context,
@@ -26,7 +27,13 @@ export async function getPassword(
credentialsInBody: boolean;
},
): Promise<AccessToken> {
const token = await getOrRefreshAccessToken(ctx, contextId, {
const tokenArgs: TokenStoreArgs = {
contextId,
clientId,
accessTokenUrl,
authorizationUrl: null,
};
const token = await getOrRefreshAccessToken(ctx, tokenArgs, {
accessTokenUrl,
scope,
clientId,
@@ -37,7 +44,7 @@ export async function getPassword(
return token;
}
const response = await getAccessToken(ctx, {
const response = await fetchAccessToken(ctx, {
accessTokenUrl,
clientId,
clientSecret,
@@ -51,5 +58,5 @@ export async function getPassword(
],
});
return storeToken(ctx, contextId, response);
return storeToken(ctx, tokenArgs, response);
}

View File

@@ -1,4 +1,4 @@
import {
import type {
Context,
FormInputSelectOption,
GetHttpAuthenticationConfigRequest,
@@ -7,6 +7,7 @@ import {
} from '@yaakapp/api';
import {
DEFAULT_PKCE_METHOD,
genPkceCodeVerifier,
getAuthorizationCode,
PKCE_PLAIN,
PKCE_SHA256,
@@ -14,7 +15,8 @@ import {
import { getClientCredentials } from './grants/clientCredentials';
import { getImplicit } from './grants/implicit';
import { getPassword } from './grants/password';
import { AccessToken, deleteToken, getToken, resetDataDirKey } from './store';
import type { AccessToken, TokenStoreArgs } from './store';
import { deleteToken, getToken, resetDataDirKey } from './store';
type GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials';
@@ -81,8 +83,14 @@ export const plugin: PluginDefinition = {
actions: [
{
label: 'Copy Current Token',
async onSelect(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
async onSelect(ctx, { contextId, values }) {
const tokenArgs: TokenStoreArgs = {
contextId,
authorizationUrl: stringArg(values, 'authorizationUrl'),
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
clientId: stringArg(values, 'clientId'),
};
const token = await getToken(ctx, tokenArgs);
if (token == null) {
await ctx.toast.show({ message: 'No token to copy', color: 'warning' });
} else {
@@ -97,8 +105,14 @@ export const plugin: PluginDefinition = {
},
{
label: 'Delete Token',
async onSelect(ctx, { contextId }) {
if (await deleteToken(ctx, contextId)) {
async onSelect(ctx, { contextId, values }) {
const tokenArgs: TokenStoreArgs = {
contextId,
authorizationUrl: stringArg(values, 'authorizationUrl'),
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
clientId: stringArg(values, 'clientId'),
};
if (await deleteToken(ctx, tokenArgs)) {
await ctx.toast.show({ message: 'Token deleted', color: 'success' });
} else {
await ctx.toast.show({ message: 'No token to delete', color: 'warning' });
@@ -111,17 +125,6 @@ export const plugin: PluginDefinition = {
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: [
{
@@ -219,9 +222,9 @@ export const plugin: PluginDefinition = {
},
{
type: 'text',
name: 'pkceCodeVerifier',
name: 'pkceCodeChallenge',
label: 'Code Verifier',
placeholder: 'Automatically generated if not provided',
placeholder: 'Automatically generated when not set',
optional: true,
dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce),
},
@@ -257,6 +260,12 @@ export const plugin: PluginDefinition = {
label: 'Advanced',
inputs: [
{ type: 'text', name: 'scope', label: 'Scope', optional: true },
{
type: 'text',
name: 'headerName',
label: 'Header Name',
defaultValue: 'Authorization',
},
{
type: 'text',
name: 'headerPrefix',
@@ -279,8 +288,14 @@ export const plugin: PluginDefinition = {
{
type: 'accordion',
label: 'Access Token Response',
async dynamic(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
async dynamic(ctx, { contextId, values }) {
const tokenArgs: TokenStoreArgs = {
contextId,
authorizationUrl: stringArg(values, 'authorizationUrl'),
accessTokenUrl: stringArg(values, 'accessTokenUrl'),
clientId: stringArg(values, 'clientId'),
};
const token = await getToken(ctx, tokenArgs);
if (token == null) {
return { hidden: true };
}
@@ -310,12 +325,14 @@ export const plugin: PluginDefinition = {
const authorizationUrl = stringArg(values, 'authorizationUrl');
const accessTokenUrl = stringArg(values, 'accessTokenUrl');
token = await getAuthorizationCode(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//)
? accessTokenUrl
: `https://${accessTokenUrl}`,
authorizationUrl: authorizationUrl.match(/^https?:\/\//)
? authorizationUrl
: `https://${authorizationUrl}`,
accessTokenUrl:
accessTokenUrl === '' || accessTokenUrl.match(/^https?:\/\//)
? accessTokenUrl
: `https://${accessTokenUrl}`,
authorizationUrl:
authorizationUrl === '' || authorizationUrl.match(/^https?:\/\//)
? authorizationUrl
: `https://${authorizationUrl}`,
clientId: stringArg(values, 'clientId'),
clientSecret: stringArg(values, 'clientSecret'),
redirectUri: stringArgOrNull(values, 'redirectUri'),
@@ -325,8 +342,8 @@ export const plugin: PluginDefinition = {
credentialsInBody,
pkce: values.usePkce
? {
challengeMethod: stringArg(values, 'pkceChallengeMethod'),
codeVerifier: stringArgOrNull(values, 'pkceCodeVerifier'),
challengeMethod: stringArg(values, 'pkceChallengeMethod') || DEFAULT_PKCE_METHOD,
codeVerifier: stringArg(values, 'pkceCodeVerifier') || genPkceCodeVerifier(),
}
: null,
tokenName: tokenName,
@@ -375,15 +392,9 @@ export const plugin: PluginDefinition = {
throw new Error('Invalid grant type ' + grantType);
}
const headerName = stringArg(values, 'headerName') || 'Authorization';
const headerValue = `${headerPrefix} ${token.response[tokenName]}`.trim();
return {
setHeaders: [
{
name: 'Authorization',
value: headerValue,
},
],
};
return { setHeaders: [{ name: headerName, value: headerValue }] };
},
},
};

View File

@@ -1,8 +1,9 @@
import { Context } from '@yaakapp/api';
import type { Context } from '@yaakapp/api';
import { createHash } from 'node:crypto';
export async function storeToken(
ctx: Context,
contextId: string,
args: TokenStoreArgs,
response: AccessTokenRawResponse,
tokenName: 'access_token' | 'id_token' = 'access_token',
) {
@@ -15,16 +16,16 @@ export async function storeToken(
response,
expiresAt,
};
await ctx.store.set<AccessToken>(tokenStoreKey(contextId), token);
await ctx.store.set<AccessToken>(tokenStoreKey(args), token);
return token;
}
export async function getToken(ctx: Context, contextId: string) {
return ctx.store.get<AccessToken>(tokenStoreKey(contextId));
export async function getToken(ctx: Context, args: TokenStoreArgs) {
return ctx.store.get<AccessToken>(tokenStoreKey(args));
}
export async function deleteToken(ctx: Context, contextId: string) {
return ctx.store.delete(tokenStoreKey(contextId));
export async function deleteToken(ctx: Context, args: TokenStoreArgs) {
return ctx.store.delete(tokenStoreKey(args));
}
export async function resetDataDirKey(ctx: Context, contextId: string) {
@@ -37,8 +38,25 @@ export async function getDataDirKey(ctx: Context, contextId: string) {
return `${contextId}::${key}`;
}
function tokenStoreKey(contextId: string) {
return ['token', contextId].join('::');
export interface TokenStoreArgs {
contextId: string;
clientId: string;
accessTokenUrl: string | null;
authorizationUrl: string | null;
}
/**
* Generate a store key to use based on some arguments. The arguments will be normalized a bit to
* account for slight variations (like domains with and without a protocol scheme).
*/
function tokenStoreKey(args: TokenStoreArgs) {
const hash = createHash('md5');
if (args.contextId) hash.update(args.contextId.trim());
if (args.clientId) hash.update(args.clientId.trim());
if (args.accessTokenUrl) hash.update(args.accessTokenUrl.trim().replace(/^https?:\/\//, ''));
if (args.authorizationUrl) hash.update(args.authorizationUrl.trim().replace(/^https?:\/\//, ''));
const key = hash.digest('hex');
return ['token', key].join('::');
}
function dataDirStoreKey(contextId: string) {

View File

@@ -0,0 +1,85 @@
import type { AccessToken } from './store';
export function isTokenExpired(token: AccessToken) {
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

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

View File

@@ -1,9 +0,0 @@
{
"name": "@yaakapp/exporter-curl",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
}
}

View File

@@ -1,101 +0,0 @@
import { HttpRequest, PluginDefinition } from '@yaakapp/api';
const NEWLINE = '\\\n ';
export const plugin: PluginDefinition = {
httpRequestActions: [{
label: 'Copy as Curl',
icon: 'copy',
async onSelect(ctx, args) {
const rendered_request = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: 'preview' });
const data = await convertToCurl(rendered_request);
await ctx.clipboard.copyText(data);
await ctx.toast.show({ message: 'Curl copied to clipboard', icon: 'copy', color: 'success' });
},
}],
};
export async function convertToCurl(request: Partial<HttpRequest>) {
const xs = ['curl'];
// Add method and URL all on first line
if (request.method) xs.push('-X', request.method);
if (request.url) xs.push(quote(request.url));
xs.push(NEWLINE);
// Add URL params
for (const p of (request.urlParameters ?? []).filter(onlyEnabled)) {
xs.push('--url-query', quote(`${p.name}=${p.value}`));
xs.push(NEWLINE);
}
// Add headers
for (const h of (request.headers ?? []).filter(onlyEnabled)) {
xs.push('--header', quote(`${h.name}: ${h.value}`));
xs.push(NEWLINE);
}
// Add form params
if (Array.isArray(request.body?.form)) {
const flag = request.bodyType === 'multipart/form-data' ? '--form' : '--data';
for (const p of (request.body?.form ?? []).filter(onlyEnabled)) {
if (p.file) {
let v = `${p.name}=@${p.file}`;
v += p.contentType ? `;type=${p.contentType}` : '';
xs.push(flag, v);
} else {
xs.push(flag, quote(`${p.name}=${p.value}`));
}
xs.push(NEWLINE);
}
} else if (typeof request.body?.query === 'string') {
const body = { query: request.body.query || '', variables: maybeParseJSON(request.body.variables, undefined) };
xs.push('--data-raw', `${quote(JSON.stringify(body))}`);
xs.push(NEWLINE);
} else if (typeof request.body?.text === 'string') {
xs.push('--data-raw', `${quote(request.body.text)}`);
xs.push(NEWLINE);
}
// Add basic/digest authentication
if (request.authenticationType === 'basic' || request.authenticationType === 'digest') {
if (request.authenticationType === 'digest') xs.push('--digest');
xs.push(
'--user',
quote(`${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`),
);
xs.push(NEWLINE);
}
// Add bearer authentication
if (request.authenticationType === 'bearer') {
xs.push('--header', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`));
xs.push(NEWLINE);
}
// Remove trailing newline
if (xs[xs.length - 1] === NEWLINE) {
xs.splice(xs.length - 1, 1);
}
return xs.join(' ');
}
function quote(arg: string): string {
const escaped = arg.replace(/'/g, '\\\'');
return `'${escaped}'`;
}
function onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {
return v.enabled !== false && !!v.name;
}
function maybeParseJSON(v: any, fallback: any): string {
try {
return JSON.parse(v);
} catch (err) {
return fallback;
}
}

View File

@@ -0,0 +1,59 @@
# JSONPath
A filter plugin that enables [JSONPath](https://en.wikipedia.org/wiki/JSONPath)
extraction and filtering for JSON responses, making it easy to extract specific values
from complex JSON structures.
![Screenshot of JSONPath filtering](screenshot.png)
## Overview
This plugin provides JSONPath filtering for responses in Yaak. JSONPath is a query
language for JSON, similar to XPath for XML, that provides the ability to extract data
from JSON documents using a simple, expressive syntax. This is useful for working with
complex API responses where you need to only view a small subset of response data.
## How JSONPath Works
JSONPath uses a dot-notation syntax to navigate JSON structures:
- `$` - Root element
- `.` - Child element
- `..` - Recursive descent
- `*` - Wildcard
- `[]` - Array index or filter
## JSONPath Syntax Examples
### Basic Navigation
```
$.store.book[0].title # First book title
$.store.book[*].author # All book authors
$.store.book[-1] # Last book
$.store.book[0,1] # First two books
$.store.book[0:2] # First two books (slice)
```
### Filtering
```
$.store.book[?(@.price < 10)] # Books under $10
$.store.book[?(@.author == 'Tolkien')] # Books by Tolkien
$.store.book[?(@.category == 'fiction')] # Fiction books
```
### Recursive Search
```
$..author # All authors anywhere in the document
$..book[2] # Third book anywhere
$..price # All prices in the document
```
## Usage
1. Make an API request that returns JSON data
2. Below the response body, click the filter icon
3. Enter a JSONPath expression
4. View the extracted data in the results panel

View File

@@ -1,10 +1,18 @@
{
"name": "@yaakapp/filter-jsonpath",
"name": "@yaak/filter-jsonpath",
"displayName": "JSONPath Filter",
"description": "Filter JSON response data using JSONPath expressions",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/filter-jsonpath"
},
"private": true,
"version": "0.0.1",
"version": "0.1.0",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
},
"dependencies": {
"jsonpath-plus": "^10.3.0"

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

View File

@@ -1,4 +1,4 @@
import { PluginDefinition } from '@yaakapp/api';
import type { PluginDefinition } from '@yaakapp/api';
import { JSONPath } from 'jsonpath-plus';
export const plugin: PluginDefinition = {
@@ -7,8 +7,12 @@ export const plugin: PluginDefinition = {
description: 'Filter JSONPath',
onFilter(_ctx, args) {
const parsed = JSON.parse(args.payload);
const filtered = JSONPath({ path: args.filter, json: parsed });
return { filtered: JSON.stringify(filtered, null, 2) };
try {
const filtered = JSONPath({ path: args.filter, json: parsed });
return { content: JSON.stringify(filtered, null, 2) };
} catch (err) {
return { content: '', error: `Invalid filter: ${err}` };
}
},
},
};

View File

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

View File

@@ -1,13 +1,16 @@
{
"name": "@yaakapp/filter-xpath",
"name": "@yaak/filter-xpath",
"displayName": "XPath Filter",
"description": "Filter response XML data using XPath expressions",
"private": true,
"version": "0.0.1",
"version": "0.1.0",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
},
"dependencies": {
"@xmldom/xmldom": "^0.8.10",
"@xmldom/xmldom": "^0.9.8",
"xpath": "^0.0.34"
}
}

View File

@@ -1,5 +1,5 @@
import { DOMParser } from '@xmldom/xmldom';
import { PluginDefinition } from '@yaakapp/api';
import type { PluginDefinition } from '@yaakapp/api';
import xpath from 'xpath';
export const plugin: PluginDefinition = {
@@ -7,14 +7,18 @@ export const plugin: PluginDefinition = {
name: 'XPath',
description: 'Filter XPath',
onFilter(_ctx, args) {
const doc = new DOMParser().parseFromString(args.payload, 'text/xml');
const result = xpath.select(args.filter, doc, false);
if (Array.isArray(result)) {
return { filtered: result.map(r => String(r)).join('\n') };
} else {
// Not sure what cases this happens in (?)
return { filtered: String(result) };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const doc: any = new DOMParser().parseFromString(args.payload, 'text/xml');
try {
const result = xpath.select(args.filter, doc, false);
if (Array.isArray(result)) {
return { content: result.map((r) => String(r)).join('\n') };
} else {
// Not sure what cases this happens in (?)
return { content: String(result) };
}
} catch (err) {
return { content: '', error: `Invalid filter: ${err}` };
}
},
},

View File

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

View File

@@ -1,10 +1,14 @@
{
"name": "@yaakapp/importer-curl",
"name": "@yaak/importer-curl",
"displayName": "cURL Importer",
"description": "Import requests from cURL commands",
"private": true,
"version": "0.0.1",
"version": "0.1.0",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
},
"dependencies": {
"shell-quote": "^1.8.1"

View File

@@ -1,5 +1,6 @@
import { Context, Environment, Folder, HttpRequest, HttpUrlParameter, PluginDefinition, Workspace } from '@yaakapp/api';
import { ControlOperator, parse, ParseEntry } from 'shell-quote';
import type { Context, Environment, Folder, HttpRequest, HttpUrlParameter, PluginDefinition, Workspace } from '@yaakapp/api';
import type { ControlOperator, ParseEntry } from 'shell-quote';
import { parse } from 'shell-quote';
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
@@ -40,6 +41,7 @@ export const plugin: PluginDefinition = {
name: 'cURL',
description: 'Import cURL commands',
onImport(_ctx: Context, args: { text: string }) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return convertCurl(args.text) as any;
},
},
@@ -177,19 +179,15 @@ function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
// Build the request //
// ~~~~~~~~~~~~~~~~~ //
// Url and Parameters
let urlParameters: HttpUrlParameter[];
let url: string;
const urlArg = getPairValue(flagsByName, (singletons[0] as string) || '', ['url']);
const [baseUrl, search] = splitOnce(urlArg, '?');
urlParameters =
const urlParameters: HttpUrlParameter[] =
search?.split('&').map((p) => {
const v = splitOnce(p, '=');
return { name: decodeURIComponent(v[0] ?? ''), value: decodeURIComponent(v[1] ?? ''), enabled: true };
}) ?? [];
url = baseUrl ?? urlArg;
const url = baseUrl ?? urlArg;
// Query params
for (const p of flagsByName['url-query'] ?? []) {
@@ -375,7 +373,7 @@ interface DataParameter {
}
function pairsToDataParameters(keyedPairs: FlagsByName): DataParameter[] {
let dataParameters: DataParameter[] = [];
const dataParameters: DataParameter[] = [];
for (const flagName of DATA_FLAGS) {
const pairs = keyedPairs[flagName];
@@ -386,9 +384,9 @@ function pairsToDataParameters(keyedPairs: FlagsByName): DataParameter[] {
for (const p of pairs) {
if (typeof p !== 'string') continue;
let params = p.split("&");
const params = p.split("&");
for (const param of params) {
const [name, value] = param.split('=');
const [name, value] = splitOnce(param, '=');
if (param.startsWith('@')) {
// Yaak doesn't support files in url-encoded data, so
dataParameters.push({

View File

@@ -1,4 +1,4 @@
import { HttpRequest, Workspace } from '@yaakapp/api';
import type { HttpRequest, Workspace } from '@yaakapp/api';
import { describe, expect, test } from 'vitest';
import { convertCurl } from '../src';
@@ -221,20 +221,20 @@ describe('importer-curl', () => {
});
test('Imports post data into URL', () => {
expect(
convertCurl('curl -G https://api.stripe.com/v1/payment_links -d limit=3'),
).toEqual({
expect(convertCurl('curl -G https://api.stripe.com/v1/payment_links -d limit=3')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
method: 'GET',
url: 'https://api.stripe.com/v1/payment_links',
urlParameters: [{
enabled: true,
name: 'limit',
value: '3',
}],
urlParameters: [
{
enabled: true,
name: 'limit',
value: '3',
},
],
}),
],
},
@@ -243,7 +243,9 @@ describe('importer-curl', () => {
test('Imports multi-line JSON', () => {
expect(
convertCurl(`curl -H Content-Type:application/json -d $'{\n "foo":"bar"\n}' https://yaak.app`),
convertCurl(
`curl -H Content-Type:application/json -d $'{\n "foo":"bar"\n}' https://yaak.app`,
),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
@@ -364,6 +366,31 @@ describe('importer-curl', () => {
},
});
});
test('Imports weird body', () => {
expect(convertCurl(`curl 'https://yaak.app' -X POST --data-raw 'foo=bar=baz'`)).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: 'https://yaak.app',
method: "POST",
bodyType: 'application/x-www-form-urlencoded',
body: {
form: [{ name: 'foo', value: 'bar=baz', enabled: true }],
},
headers: [
{
enabled: true,
name: 'Content-Type',
value: 'application/x-www-form-urlencoded',
},
],
}),
],
},
});
});
});
const idCount: Partial<Record<string, number>> = {};

View File

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

View File

@@ -1,10 +1,14 @@
{
"name": "@yaakapp/importer-insomnia",
"name": "@yaak/importer-insomnia",
"displayName": "Insomnia Importer",
"description": "Import data from Insomnia",
"private": true,
"version": "0.0.1",
"version": "0.1.0",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
},
"dependencies": {
"yaml": "^2.4.2"

View File

@@ -4,11 +4,11 @@ export function convertSyntax(variable: string): string {
return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}');
}
export function isJSObject(obj: any) {
export function isJSObject(obj: unknown) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
export function isJSString(obj: any) {
export function isJSString(obj: unknown) {
return Object.prototype.toString.call(obj) === '[object String]';
}

View File

@@ -1,4 +1,4 @@
import { Context, PluginDefinition } from '@yaakapp/api';
import type { Context, PluginDefinition } from '@yaakapp/api';
import YAML from 'yaml';
import { deleteUndefinedAttrs, isJSObject } from './common';
import { convertInsomniaV4 } from './v4';
@@ -15,16 +15,18 @@ export const plugin: PluginDefinition = {
};
export function convertInsomnia(contents: string) {
let parsed: any;
let parsed: unknown;
try {
parsed = JSON.parse(contents);
} catch (e) {
} catch {
// Fall through
}
try {
parsed = parsed ?? YAML.parse(contents);
} catch (e) {
} catch {
// Fall through
}
if (!isJSObject(parsed)) return null;

View File

@@ -1,7 +1,8 @@
import { PartialImportResources } from '@yaakapp/api';
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { PartialImportResources } from '@yaakapp/api';
import { convertId, convertSyntax, isJSObject } from './common';
export function convertInsomniaV4(parsed: Record<string, any>) {
export function convertInsomniaV4(parsed: any) {
if (!Array.isArray(parsed.resources)) return null;
const resources: PartialImportResources = {
@@ -14,7 +15,9 @@ export function convertInsomniaV4(parsed: Record<string, any>) {
};
// Import workspaces
const workspacesToImport = parsed.resources.filter(r => isJSObject(r) && r._type === 'workspace');
const workspacesToImport = parsed.resources.filter(
(r: any) => isJSObject(r) && r._type === 'workspace',
);
for (const w of workspacesToImport) {
resources.workspaces.push({
id: convertId(w._id),
@@ -40,13 +43,9 @@ export function convertInsomniaV4(parsed: Record<string, any>) {
resources.folders.push(importFolder(child, w._id));
nextFolder(child._id);
} else if (child._type === 'request') {
resources.httpRequests.push(
importHttpRequest(child, w._id),
);
resources.httpRequests.push(importHttpRequest(child, w._id));
} else if (child._type === 'grpc_request') {
resources.grpcRequests.push(
importGrpcRequest(child, w._id),
);
resources.grpcRequests.push(importGrpcRequest(child, w._id));
}
}
};
@@ -64,10 +63,7 @@ export function convertInsomniaV4(parsed: Record<string, any>) {
return { resources };
}
function importHttpRequest(
r: any,
workspaceId: string,
): PartialImportResources['httpRequests'][0] {
function importHttpRequest(r: any, workspaceId: string): PartialImportResources['httpRequests'][0] {
let bodyType: string | null = null;
let body = {};
if (r.body.mimeType === 'application/octet-stream') {
@@ -141,10 +137,7 @@ function importHttpRequest(
};
}
function importGrpcRequest(
r: any,
workspaceId: string,
): PartialImportResources['grpcRequests'][0] {
function importGrpcRequest(r: any, workspaceId: string): PartialImportResources['grpcRequests'][0] {
const parts = r.protoMethodName.split('/').filter((p: any) => p !== '');
const service = parts[0] ?? null;
const method = parts[1] ?? null;
@@ -186,13 +179,18 @@ function importFolder(f: any, workspaceId: string): PartialImportResources['fold
};
}
function importEnvironment(e: any, workspaceId: string, isParent?: boolean): PartialImportResources['environments'][0] {
function importEnvironment(
e: any,
workspaceId: string,
isParent?: boolean,
): PartialImportResources['environments'][0] {
return {
id: convertId(e._id),
createdAt: e.created ? new Date(e.created).toISOString().replace('Z', '') : undefined,
updatedAt: e.modified ? new Date(e.modified).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId),
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
sortPriority: e.metaSortKey, // Will be added to Yaak later
base: isParent ?? e.parentId === workspaceId,
model: 'environment',

View File

@@ -1,8 +1,16 @@
import { PartialImportResources } from '@yaakapp/api';
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { PartialImportResources } from '@yaakapp/api';
import { convertId, convertSyntax, isJSObject } from './common';
export function convertInsomniaV5(parsed: Record<string, any>) {
if (!Array.isArray(parsed.collection)) return null;
export function convertInsomniaV5(parsed: any) {
// Assert parsed is object
if (parsed == null || typeof parsed !== 'object') {
return null;
}
if (!('collection' in parsed) || !Array.isArray(parsed.collection)) {
return null;
}
const resources: PartialImportResources = {
environments: [],
@@ -14,7 +22,7 @@ export function convertInsomniaV5(parsed: Record<string, any>) {
};
// Import workspaces
const meta: Record<string, any> = parsed.meta ?? {};
const meta = ('meta' in parsed ? parsed.meta : {}) as Record<string, any>;
resources.workspaces.push({
id: convertId(meta.id ?? 'collection'),
createdAt: meta.created ? new Date(meta.created).toISOString().replace('Z', '') : undefined,
@@ -22,31 +30,32 @@ export function convertInsomniaV5(parsed: Record<string, any>) {
model: 'workspace',
name: parsed.name,
description: meta.description || undefined,
...importHeaders(parsed),
...importAuthentication(parsed),
});
// Import environments
resources.environments.push(
importEnvironment(parsed.environments, meta.id, true),
...(parsed.environments.subEnvironments ?? []).map((r: any) => importEnvironment(r, meta.id)),
);
// Import folders
const nextFolder = (children: any[], parentId: string) => {
for (const child of children ?? []) {
if (!isJSObject(child)) continue;
if (Array.isArray(child.children)) {
resources.folders.push(importFolder(child, meta.id, parentId));
const { folder, environment } = importFolder(child, meta.id, parentId);
resources.folders.push(folder);
if (environment) resources.environments.push(environment);
nextFolder(child.children, child.meta.id);
} else if (child.method) {
resources.httpRequests.push(
importHttpRequest(child, meta.id, parentId),
);
resources.httpRequests.push(importHttpRequest(child, meta.id, parentId));
} else if (child.protoFileId) {
resources.grpcRequests.push(
importGrpcRequest(child, meta.id, parentId),
);
resources.grpcRequests.push(importGrpcRequest(child, meta.id, parentId));
} else if (child.url) {
resources.websocketRequests.push(
importWebsocketRequest(child, meta.id, parentId),
);
resources.websocketRequests.push(importWebsocketRequest(child, meta.id, parentId));
}
}
};
@@ -189,8 +198,8 @@ function importWebsocketRequest(
};
}
function importHeaders(r: any) {
const headers = (r.headers ?? [])
function importHeaders(obj: any) {
const headers = (obj.headers ?? [])
.map((h: any) => ({
enabled: !h.disabled,
name: h.name ?? '',
@@ -200,46 +209,81 @@ function importHeaders(r: any) {
return { headers } as const;
}
function importAuthentication(r: any) {
function importAuthentication(obj: any) {
let authenticationType: string | null = null;
let authentication = {};
if (r.authentication?.type === 'bearer') {
if (obj.authentication?.type === 'bearer') {
authenticationType = 'bearer';
authentication = {
token: convertSyntax(r.authentication.token),
token: convertSyntax(obj.authentication.token),
};
} else if (r.authentication?.type === 'basic') {
} else if (obj.authentication?.type === 'basic') {
authenticationType = 'basic';
authentication = {
username: convertSyntax(r.authentication.username),
password: convertSyntax(r.authentication.password),
username: convertSyntax(obj.authentication.username),
password: convertSyntax(obj.authentication.password),
};
}
return { authenticationType, authentication } as const;
}
function importFolder(f: any, workspaceId: string, parentId: string): PartialImportResources['folders'][0] {
function importFolder(
f: any,
workspaceId: string,
parentId: string,
): {
folder: PartialImportResources['folders'][0];
environment: PartialImportResources['environments'][0] | null;
} {
const id = f.meta?.id ?? f._id;
const created = f.meta?.created ?? f.created;
const updated = f.meta?.modified ?? f.updated;
const sortKey = f.meta?.sortKey ?? f.sortKey;
let environment: PartialImportResources['environments'][0] | null = null;
if (Object.keys(f.environment ?? {}).length > 0) {
environment = {
id: convertId(id + 'folder'),
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId),
public: true,
parentModel: 'folder',
parentId: convertId(id),
model: 'environment',
name: 'Folder Environment',
variables: Object.entries(f.environment ?? {}).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
})),
};
}
return {
model: 'folder',
id: convertId(id),
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
folderId: parentId === workspaceId ? null : convertId(parentId),
sortPriority: sortKey,
workspaceId: convertId(workspaceId),
description: f.description || undefined,
name: f.name,
folder: {
model: 'folder',
id: convertId(id),
createdAt: created ? new Date(created).toISOString().replace('Z', '') : undefined,
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
folderId: parentId === workspaceId ? null : convertId(parentId),
sortPriority: sortKey,
workspaceId: convertId(workspaceId),
description: f.description || undefined,
name: f.name,
...importAuthentication(f),
...importHeaders(f),
},
environment,
};
}
function importEnvironment(e: any, workspaceId: string, isParent?: boolean): PartialImportResources['environments'][0] {
function importEnvironment(
e: any,
workspaceId: string,
isParent?: boolean,
): PartialImportResources['environments'][0] {
const id = e.meta?.id ?? e._id;
const created = e.meta?.created ?? e.created;
const updated = e.meta?.modified ?? e.updated;
@@ -251,9 +295,11 @@ function importEnvironment(e: any, workspaceId: string, isParent?: boolean): Par
updatedAt: updated ? new Date(updated).toISOString().replace('Z', '') : undefined,
workspaceId: convertId(workspaceId),
public: !e.isPrivate,
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
sortPriority: sortKey, // Will be added to Yaak later
base: isParent ?? e.parentId === workspaceId,
parentModel: isParent ? 'workspace' : 'environment',
parentId: null,
model: 'environment',
name: e.name,
variables: Object.entries(e.data ?? {}).map(([name, value]) => ({

View File

@@ -38,6 +38,8 @@ collection:
name: foo
value: bar
disabled: false
environment:
folder_env_var: testing
- name: New Request
meta:
id: req_e3f8cdbd58784a539dd4c1e127d73451

View File

@@ -2,7 +2,6 @@
"resources": {
"environments": [
{
"base": true,
"createdAt": "2025-05-14T04:45:24.903",
"id": "GENERATE_ID::env_e46dc73e8ccda30ca132153e8f11183bd08119ce",
"model": "environment",
@@ -10,6 +9,26 @@
"public": true,
"updatedAt": "2025-05-14T04:45:24.903",
"variables": [],
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c",
"parentId": null,
"parentModel": "workspace"
},
{
"createdAt": "2025-05-16T16:48:12.298",
"id": "GENERATE_ID::fld_296933ea4ea84783a775d199997e9be7folder",
"model": "environment",
"name": "Folder Environment",
"parentId": "GENERATE_ID::fld_296933ea4ea84783a775d199997e9be7",
"parentModel": "folder",
"public": true,
"updatedAt": "2025-05-16T16:49:02.427",
"variables": [
{
"enabled": true,
"name": "folder_env_var",
"value": "testing"
}
],
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
}
],
@@ -22,7 +41,16 @@
"name": "My Folder",
"sortPriority": -1747414092298,
"updatedAt": "2025-05-16T16:49:02.427",
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c"
"workspaceId": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c",
"authentication": {},
"authenticationType": null,
"headers": [
{
"enabled": true,
"name": "foo",
"value": "bar"
}
]
}
],
"grpcRequests": [],
@@ -80,7 +108,10 @@
"id": "GENERATE_ID::wrk_9717dd1c9e0c4b2e9ed6d2abcf3bd45c",
"model": "workspace",
"name": "Debugging",
"updatedAt": "2025-05-14T04:45:24.902"
"updatedAt": "2025-05-14T04:45:24.902",
"authentication": {},
"authenticationType": null,
"headers": []
}
]
}

View File

@@ -4,11 +4,12 @@
{
"createdAt": "2025-01-13T15:15:43.767",
"updatedAt": "2025-01-13T15:15:55.209",
"base": true,
"public": true,
"id": "GENERATE_ID::env_20945044d3c8497ca8b717bef750987e",
"model": "environment",
"name": "Base Environment",
"parentId": null,
"parentModel": "workspace",
"variables": [
{
"enabled": true,
@@ -21,11 +22,12 @@
{
"createdAt": "2025-01-13T15:15:58.515",
"updatedAt": "2025-01-13T15:16:34.705",
"base": false,
"public": true,
"id": "GENERATE_ID::env_6f7728bb7fc04d558d668e954d756ea2",
"model": "environment",
"name": "Production",
"parentId": null,
"parentModel": "environment",
"sortPriority": 1736781358515,
"variables": [
{
@@ -39,8 +41,9 @@
{
"createdAt": "2025-01-13T15:16:14.707",
"updatedAt": "2025-01-13T15:16:31.078",
"base": false,
"public": true,
"parentId": null,
"parentModel": "environment",
"id": "GENERATE_ID::env_976a8b6eb5d44fb6a20150f65c32d243",
"model": "environment",
"name": "Staging",
@@ -64,7 +67,10 @@
"model": "folder",
"name": "Top Level",
"sortPriority": -1736781404718,
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53"
"workspaceId": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53",
"authentication": {},
"authenticationType": null,
"headers": []
}
],
"grpcRequests": [
@@ -165,7 +171,10 @@
"description": "This is the description",
"id": "GENERATE_ID::wrk_c1eacfa750a04f3ea9985ef28043fa53",
"model": "workspace",
"name": "Dummy"
"name": "Dummy",
"authentication": {},
"authenticationType": null,
"headers": []
}
]
}

View File

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

View File

@@ -1,16 +1,20 @@
{
"name": "@yaakapp/importer-openapi",
"name": "@yaak/importer-openapi",
"displayName": "OpenAPI Importer",
"description": "Import API specifications from OpenAPI/Swagger format",
"private": true,
"version": "0.0.1",
"version": "0.1.0",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
},
"dependencies": {
"openapi-to-postmanv2": "^5.0.0",
"yaml": "^2.4.2"
},
"devDependencies": {
"@types/openapi-to-postmanv2": "^3.2.4"
"@types/openapi-to-postmanv2": "^5.0.0"
}
}

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