Compare commits

..

195 Commits

Author SHA1 Message Date
Gregory Schier
2e144f064d Fix syntax highlighting 2025-05-21 08:26:15 -07:00
Gregory Schier
d8b1cadae6 Fix model deletion 2025-05-21 08:25:12 -07:00
Gregory Schier
c2f9760d08 Fix template parsing 2025-05-21 08:18:09 -07:00
Gregory Schier
a4c600cb48 Lint errors 2025-05-20 08:15:19 -07:00
Gregory Schier
bc3a5e3e58 Include license status in notification endpoint 2025-05-20 08:13:57 -07:00
Gregory Schier
4c3a02ac53 Show decrypt error in secure input 2025-05-20 07:41:32 -07:00
Gregory Schier
1974d61aa4 Fix syntax highlighting 2025-05-19 15:41:19 -07:00
Gregory Schier
0bcb092854 Update README.md 2025-05-19 15:10:56 -07:00
Gregory Schier
409620f533 More advanced template grammar
Fixes https://feedback.yaak.app/p/cannot-escape-call-to-variable-in-json-body
2025-05-19 13:37:12 -07:00
Gregory Schier
3e9037f70a No longer mark environments as external in Git 2025-05-17 06:06:36 -07:00
Desperate Necromancer
be82b67ed3 Allow disabling window decorations/controls (#176)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-05-16 13:33:59 -07:00
Gregory Schier
432b366105 Fix grpc/ws events error 2025-05-16 13:00:50 -07:00
Hao Xiang
42e70b941d fix proto to json-schema (#194) 2025-05-16 12:53:53 -07:00
Gregory Schier
3808215210 Better unicode un-escaping 2025-05-16 12:42:08 -07:00
Walyson G Oliveira
763a60982a Adjusting the JSON viewing response to accept accentuation (#203) 2025-05-16 12:37:00 -07:00
dependabot[bot]
a05679fd93 Bump vite from 6.2.6 to 6.2.7 in /src-web (#205)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-16 12:31:46 -07:00
Gregory Schier
73c366dc27 Hopefully fix weird env routing issue 2025-05-16 09:12:36 -07:00
Gregory Schier
0be7d0283b Add ref=<app_id> to external links pointing to yaak.app 2025-05-16 07:53:22 -07:00
Gregory Schier
749df338c5 Disable wasm-opt 2025-05-15 14:29:29 -07:00
Gregory Schier
3184c1b79e Remove dynamic imports 2025-05-15 12:29:45 -07:00
Gregory Schier
b52bf7cd56 Fix HttpResponse AnyModel deserialization 2025-05-15 09:37:05 -07:00
Gregory Schier
d962d7f94b remove codemirror dep and restructure a bit 2025-05-15 09:28:14 -07:00
Gregory Schier
21e2a67c1e Fix sidebar scroll drag
https://feedback.yaak.app/p/endpoinst-scrollbar-not-clickable
2025-05-15 07:10:08 -07:00
Gregory Schier
c188435524 Fix obscured text overflow
https://feedback.yaak.app/p/pasting-token-auth-results-in-invisible-text
2025-05-15 07:00:25 -07:00
Gregory Schier
8a7a7ba49d Try fixing trusted-signing-cli 2025-05-14 20:43:59 -07:00
Gregory Schier
cbc40230bb Fix cursor position after variable on Linux
Closes https://feedback.yaak.app/p/editing-the-url-sometimes-freezes-the-app
2025-05-14 20:05:04 -07:00
Gregory Schier
bc4c3178c9 Add Content-Length: 0 default for post/put/patch
https://feedback.yaak.app/p/missing-content-length
2025-05-13 21:58:00 -07:00
Gregory Schier
121fe5b3ea Fix help text 2025-05-13 11:53:46 -07:00
Gregory Schier
861609ddc0 Update encryption help 2025-05-13 11:38:02 -07:00
Gregory Schier
e5070513ac Regenerate types 2025-05-13 10:45:41 -07:00
Gregory Schier
f5c3798df9 Ability to disable proxy config
Closes https://feedback.yaak.app/p/proxy-save-last-data
2025-05-13 10:35:02 -07:00
Gregory Schier
469d12fede Don't query KeyValue.id == NULL 2025-05-13 10:11:24 -07:00
Gregory Schier
417a02744b Don't select <Input/> text when focus is due to window focus
Closes https://feedback.yaak.app/p/url-input-auto-selects-all-text-when-regaining-focus-after
2025-05-12 22:19:16 -07:00
Gregory Schier
81e78ef24c Fix auth padding 2025-05-12 16:57:43 -07:00
Gregory Schier
dad9cebb9e Don't send empty ? for ws query params 2025-05-12 16:57:13 -07:00
Gregory Schier
b3ede3d6d6 Add error boundaries 2025-05-12 15:53:21 -07:00
Gregory Schier
035fe54df0 Send grpc metadata/auth with reflection requests
Closes https://feedback.yaak.app/p/send-metadata-during-grpc-reflection
2025-05-11 07:20:57 -07:00
Gregory Schier
5f8d99ba64 Build plugins 2025-05-11 06:46:51 -07:00
Gregory Schier
84b8d130dc Some small tweaks for plugins 2025-05-11 06:36:50 -07:00
Gregory Schier
94d4227bc1 Ability to sync environments to folder (#207) 2025-05-08 14:10:07 -07:00
Gregory Schier
77cdea2f9f Merge remote-tracking branch 'origin/master' 2025-05-08 08:02:32 -07:00
Gregory Schier
8b1ca4cb47 Fix copy response body reference
Closes https://feedback.yaak.app/p/copy-body-only-works-on-first-click
2025-05-08 08:02:27 -07:00
Gregory Schier
d3b8a42180 Update README.md 2025-05-07 12:00:34 -07:00
Gregory Schier
95f39c514a Update README.md 2025-05-07 11:59:01 -07:00
hexchain
7cba082eb0 Allow building and running on aarch64 Linux (#206)
Co-authored-by: Haochen Tong <haochentong@bytedance.com>
2025-05-01 08:06:13 -07:00
Billzabob
3b9b320be2 Send cookies for introspection (#204) 2025-04-30 10:13:06 -07:00
Gregory Schier
18664975a9 Padding on encrypted input 2025-04-26 07:33:32 -07:00
Gregory Schier
bb014b7c43 Remove folder/environment foreign keys to make sync/import easier, and simplify batch upsert code. 2025-04-24 19:57:02 -07:00
Gregory Schier
9fa0650647 Add scrollbar to sidebar
Fixes: https://feedback.yaak.app/p/missing-scrollbar-on-request-list
2025-04-22 07:48:34 -07:00
Gregory Schier
b8c42677ca Fix cmd+p filtering reference
https://feedback.yaak.app/p/search-doesnt-actually-search-through-all-the-apis
2025-04-22 07:46:10 -07:00
Gregory Schier
2eb3c2241c Fix duration tag
Closes: https://feedback.yaak.app/p/elapsed-time-not-stopping-on-failed-request
2025-04-22 07:29:17 -07:00
Gregory Schier
8fb7bbfe2e Don't prompt user for keychain password more than once 2025-04-22 07:23:05 -07:00
Gregory Schier
52eba74151 Handle no text 2025-04-22 07:01:48 -07:00
Gregory Schier
e651760713 Merge remote-tracking branch 'origin/master' 2025-04-22 06:59:11 -07:00
Gregory Schier
82451a26f6 Use mimeType for response viewer 2025-04-22 06:58:53 -07:00
jzhangdev
cc15f60fb6 Fix header layout (#182) 2025-04-22 06:51:39 -07:00
Gregory Schier
2f8b2a81c7 Fix jotai/index imports 2025-04-21 07:08:13 -07:00
Gregory Schier
6d4fdc91fe Fix text decoding when no content-type
Closes https://feedback.yaak.app/p/not-rendering-response
2025-04-21 06:54:03 -07:00
Gregory Schier
faca29c789 Fix key/value re-render issue 2025-04-20 07:08:46 -07:00
Gregory Schier
1ab937aae4 Fix infinite GraphQL render loop 2025-04-17 14:45:33 -07:00
Gregory Schier
45fcea1954 Real-time response time
Closes https://feedback.yaak.app/p/real-time-display-of-request-execution-timer
2025-04-17 14:16:10 -07:00
Gregory Schier
73554078d1 Add elapsed after headers 2025-04-17 07:01:31 -07:00
Gregory Schier
a42a88de7b Don't parse URI for HTTP requests anymore.
Fixes https://feedback.yaak.app/p/using-chinese-characters-in-request-parameters-can-result-in-errors
2025-04-17 06:48:39 -07:00
Gregory Schier
14a6079176 Fix URL grammar for path parameters 2025-04-17 06:30:48 -07:00
Gregory Schier
6c513616c0 Don't vendor keyring (libdbus) 2025-04-16 10:46:12 -07:00
Gregory Schier
cdf5f1b7a5 Fix vite and top-level-await build error 2025-04-15 07:57:32 -07:00
Gregory Schier
6566857d54 Adjust keychain config for dev 2025-04-15 07:28:01 -07:00
Gregory Schier
2e55a1bd6d [WIP] Encryption for secure values (#183) 2025-04-15 07:18:26 -07:00
Gregory Schier
e114a85c39 Render gRPC request for reflection.
Closes https://feedback.yaak.app/p/grpc-address-reflection-and-address-bar-issues
2025-03-31 12:26:07 -07:00
Gregory Schier
92be088e6c useClickOutside account for right click 2025-03-31 11:57:50 -07:00
Gregory Schier
f1757ae427 Generalized frontend model store (#193) 2025-03-31 11:56:17 -07:00
Gregory Schier
ce885c3551 port window ext from encryption PR 2025-03-26 11:46:55 -07:00
Gregory Schier
17657a4d04 plugin:yaak-models|upsert PoC 2025-03-26 09:54:42 -07:00
Gregory Schier
b7f62b78b1 Clean up DB refactor access (#192) 2025-03-26 07:54:58 -07:00
Gregory Schier
006284b99c Fix scrollbars 2025-03-25 09:38:15 -07:00
Gregory Schier
bac3968aac Revert scrollbar fix 2025-03-25 09:23:45 -07:00
Gregory Schier
e5fa044eda Merge remote-tracking branch 'origin/master' 2025-03-25 09:23:33 -07:00
Gregory Schier
5969120140 Fix folder upsert 2025-03-25 09:23:26 -07:00
dependabot[bot]
8801936ad2 Bump vite from 6.0.6 to 6.0.12 (#191)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 09:13:56 -07:00
Gregory Schier
1d37d46130 Database access refactor (#190) 2025-03-25 08:35:10 -07:00
Gregory Schier
445c30f3a9 Fix iframe scrollbar 2025-03-23 06:57:36 -07:00
Gregory Schier
5fedea38c2 Fix lint error 2025-03-21 07:42:05 -07:00
Gregory Schier
d86549f492 Always show GQL schema dropdown.
Fixes https://feedback.yaak.app/p/unable-to-disable-graphql-automatic-introspection
2025-03-21 07:28:08 -07:00
Gregory Schier
4c4eaba7d2 Queries now use AppHandle instead of Window (#189) 2025-03-20 09:43:14 -07:00
Gregory Schier
cf8f8743bb Remove non-existing import 2025-03-20 06:23:36 -07:00
Andy Bao
aa75636026 Fix labels in GRPC service/method selector dropdown (#188) 2025-03-20 06:07:31 -07:00
Gregory Schier
2c41b243b6 Some cleanup around window creation 2025-03-20 06:05:17 -07:00
Gregory Schier
6aea343d4f Shorten data directory description 2025-03-19 11:41:14 -07:00
Gregory Schier
c300e8cbd5 Fix dropdown refresh after Git init 2025-03-19 11:35:20 -07:00
dependabot[bot]
6e25c26e9f Bump zip from 2.1.6 to 2.4.1 in /src-tauri (#185)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-19 09:09:02 -07:00
Gregory Schier
dce1455be7 Inline to_fixed_hash fn 2025-03-19 08:52:02 -07:00
Gregory Schier
736025b12f Move editor search to top 2025-03-19 08:22:57 -07:00
Gregory Schier
cb9e9a67a3 Try registering URL scheme 2025-03-19 07:58:12 -07:00
Gregory Schier
93c323458f Tweak Git history table 2025-03-19 06:59:54 -07:00
Gregory Schier
6f8c03d8c1 Fix git confid for commit 2025-03-19 06:59:43 -07:00
Gregory Schier
afd4228fcf Don't style scrollbars on mac 2025-03-19 06:49:14 -07:00
Gregory Schier
d478e5a12e Hotkey scrolling 2025-03-19 06:48:29 -07:00
Gregory Schier
0db9ebe67d Better Codemirror search match styles 2025-03-19 06:48:07 -07:00
Gregory Schier
80ea5e6b91 Fix autoscroller header scrolling 2025-03-19 06:37:02 -07:00
Gregory Schier
cb773babe1 Nested template functions (#186) 2025-03-18 12:49:19 -07:00
Gregory Schier
b9ed554aca Remove useTemplating prop (#184) 2025-03-18 05:34:38 -07:00
Gregory Schier
f42f3d0e27 Support multi-line params and env vars 2025-03-17 09:29:37 -07:00
Gregory Schier
93ba5b6e5c Fix close bracket bug 2025-03-13 13:09:13 -07:00
Gregory Schier
be11d5968e Fix notification not showing all 2025-03-12 06:41:53 -07:00
Gregory Schier
0828599e4f Don't switch to XML for HTML responses.
Fixes https://feedback.yaak.app/p/issue-with-rendering-html-responses-after-update
2025-03-08 08:34:41 -08:00
dependabot[bot]
f47d22c395 Bump ring from 0.17.8 to 0.17.13 in /src-tauri (#181)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-08 08:11:38 -08:00
Gregory Schier
12233cb6f6 Build plugins 2025-03-08 08:06:18 -08:00
Hao Xiang
cdce2ac53a fix ws connection state (#175)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-03-08 08:03:16 -08:00
Gregory Schier
f4d0371060 Merge remote-tracking branch 'origin/master' 2025-03-06 07:15:07 -08:00
Gregory Schier
787a0433cb Support _PREFIXED variable names and fail when variable missing 2025-03-06 07:15:02 -08:00
Gregory Schier
60ea408e51 Add sponsor button
Some people want to contribute additional funds, so this makes it easier.
2025-03-06 06:46:02 -08:00
Gregory Schier
0db0cdfd6c Only font rendering fix for Linux 2025-03-06 06:29:03 -08:00
Gregory Schier
26371e5f6b Ignore whitespace during content type detection 2025-03-06 06:22:21 -08:00
Hermes Junior
6b7c144a11 Fix font aliasing on webkit. (#178) 2025-03-06 06:19:16 -08:00
Andy Bao
62f43ca24c Fix wrong protoc includes path (#179) 2025-03-06 06:18:06 -08:00
Gregory Schier
fbf4d3c11e Make rendering return Result, and handle infinite recursion 2025-03-05 13:49:45 -08:00
Gregory Schier
7a1a0689b0 Add ability to deactivate license 2025-03-05 07:13:19 -08:00
Hao Xiang
9ead45d67a fix plugin manager listen addr (#177) 2025-03-02 05:51:19 -08:00
Gregory Schier
eb8153f409 Better trial activation flows 2025-02-25 22:16:55 -08:00
Gregory Schier
80de232bec Fix dropdown button icon 2025-02-25 19:52:57 -08:00
Gregory Schier
7af8c95fea Allow opening workspace if sync dir not empty 2025-02-25 06:54:30 -08:00
Gregory Schier
2db72fe6ef Fix WS duplication from context menu 2025-02-25 06:10:35 -08:00
Gregory Schier
d297e92a5a Fix content type parsing exception 2025-02-24 22:44:58 -08:00
Gregory Schier
7e1da4395d Build OAuth 2 plugin 2025-02-24 22:34:29 -08:00
Gregory Schier
7f8b0479e1 Plugin window data directory key 2025-02-24 22:32:40 -08:00
Gregory Schier
c8d6183456 Reduce plugin runtime memory 2025-02-24 12:20:47 -08:00
Gregory Schier
9d5f7784c4 Fix code splitting from tanstack/router migration 2025-02-24 07:12:45 -08:00
Gregory Schier
05ac836265 Remove analytics and add more update headers 2025-02-24 06:31:49 -08:00
Gregory Schier
af7782c93b Better license flows 2025-02-24 05:59:15 -08:00
Gregory Schier
2b1431d041 Merge remote-tracking branch 'origin/master' 2025-02-23 06:25:59 -08:00
Gregory Schier
9d8b7a5265 Tweak getting content type 2025-02-23 06:25:53 -08:00
dependabot[bot]
95c12ad291 Bump openssl from 0.10.66 to 0.10.70 in /src-tauri (#161)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-23 06:09:26 -08:00
Gregory Schier
dac2cec52f Merge remote-tracking branch 'origin/master' 2025-02-23 06:06:43 -08:00
Gregory Schier
efe4eef1b7 Fix deleting selected environment 2025-02-23 06:05:01 -08:00
Hao Xiang
a0e196a9e7 adding alternate key combinations for special shift (#173) 2025-02-22 07:00:04 -08:00
Gregory Schier
c6427dc724 Update README.md 2025-02-21 14:02:29 -08:00
Gregory Schier
8ce1e22b4e Update README.md 2025-02-21 13:57:51 -08:00
Gregory Schier
022d725e03 Update issue templates 2025-02-21 13:53:54 -08:00
Gregory Schier
ed7fdb1b4c Only close brackets for json-like langs
Fixes #162
2025-02-21 13:50:21 -08:00
Gregory Schier
e510204b8c More monospace fallbacks
Closes #167
2025-02-21 13:36:54 -08:00
Gregory Schier
d31b4448df Pass templating vars to recursive children (closes #171) 2025-02-21 13:34:15 -08:00
Gregory Schier
e420a0a45e Don't expand the directory setting when creating workspace 2025-02-21 13:18:47 -08:00
Gregory Schier
84ecbe0cd6 Better querystring import (https://feedback.yaak.app/p/url-pasted-params-parsed-incorrectly) 2025-02-21 13:16:09 -08:00
Gregory Schier
6a63cc26b9 Fix commit-and-push loading state 2025-02-19 10:35:41 -08:00
Gregory Schier
8ed0fd55c3 Remove environments from synced folder, and stop syncing 2025-02-19 10:35:31 -08:00
Gregory Schier
74f14a8392 Tweak some things for launch 2025-02-18 21:28:03 -08:00
Gregory Schier
ccbc8d4e18 Update 2025-02-15 12:04:35 -08:00
Gregory Schier
e4cc11aec5 Update 2025-02-15 07:29:50 -08:00
Gregory Schier
7fdf6f2798 Update 2025-02-15 07:12:12 -08:00
Gregory Schier
2aa27f7003 Create FUNDING.yml 2025-02-15 05:22:39 -08:00
Gregory Schier
3aaa0355e1 Show folders in sync confirm dialog 2025-02-09 08:35:29 -08:00
Gregory Schier
325c88f251 Show push errors in commit dialog 2025-02-07 22:50:12 -08:00
Gregory Schier
83ab93cebf Show push errors in commit dialog 2025-02-07 22:20:39 -08:00
Gregory Schier
c6289f13c1 Handle external files 2025-02-07 22:14:40 -08:00
Gregory Schier
266892dc8d Error for http remotes 2025-02-07 13:31:27 -08:00
Gregory Schier
a42bee098b Handle remote branches 2025-02-07 13:21:30 -08:00
Gregory Schier
2da898d2d4 Cargo lock 2025-02-07 12:38:59 -08:00
Gregory Schier
246e0d3f79 Vendor openssl for lib git 2025-02-07 12:38:39 -08:00
Gregory Schier
1a7c27663a Git support (#143) 2025-02-07 07:59:48 -08:00
Gregory Schier
cffc7714c1 Update README.md 2025-02-04 06:58:34 -08:00
Gregory Schier
25c1b04043 New loading icon 2025-02-04 06:52:25 -08:00
Gregory Schier
4d80c8d993 Actually handle "enabled" checkbox on auth form 2025-02-03 12:53:11 -08:00
Gregory Schier
1682d1ef0c Fix banner height 2025-02-03 12:46:15 -08:00
Gregory Schier
903bae2a18 Fix large response banner height 2025-02-03 12:41:11 -08:00
Gregory Schier
a15176841b Add features to README 2025-02-03 12:19:13 -08:00
Gregory Schier
11ef1ff2c6 Add features to README 2025-02-03 12:17:36 -08:00
Gregory Schier
615ad81ab5 Fix row height debug thing 2025-02-03 12:00:51 -08:00
Gregory Schier
fcf2577430 Url parameters for websocket URLs 2025-02-03 11:40:19 -08:00
Gregory Schier
dd0516cc55 Support list of notifications 2025-02-03 07:12:32 -08:00
Gregory Schier
17dc1991f1 Auto-scroll component for websocket/grpc/sse 2025-02-03 07:05:14 -08:00
Gregory Schier
be0ef7afce Fix sync 2025-01-31 09:27:38 -08:00
Gregory Schier
6ab9c1c3a0 Pre-publish stuff 2025-01-31 09:05:44 -08:00
Gregory Schier
c8be8082c5 Websocket Support (#159) 2025-01-31 09:00:11 -08:00
Gregory Schier
d411713502 Fix dynamic form defaults 2025-01-27 08:38:53 -08:00
Gregory Schier
93bd437e71 Fix editor formatting 2025-01-27 08:17:31 -08:00
Gregory Schier
229d9c1bd6 Better HTTP methods 2025-01-27 07:59:00 -08:00
Gregory Schier
662c38d7a0 Multi-line multi-part values 2025-01-27 07:30:06 -08:00
Gregory Schier
1d37a15cfe Fix types 2025-01-27 06:06:02 -08:00
Gregory Schier
22db739413 Swap curl and license badge 2025-01-26 13:19:26 -08:00
Gregory Schier
6393bbbc0e Slight padding 2025-01-26 13:18:40 -08:00
Gregory Schier
f678593903 OAuth 2 (#158) 2025-01-26 13:13:45 -08:00
Gregory Schier
82b1ad35ff Fix UrlBar wrapping on focus 2025-01-22 06:43:38 -08:00
Gregory Schier
4ae045cf18 Fix Faker issue 2025-01-21 13:23:13 -08:00
Gregory Schier
5d505d1366 Fix plugin runtime port 2025-01-21 06:09:36 -08:00
Gregory Schier
c58bfeb109 Use less sessionStorage for editor state 2025-01-20 14:56:25 -08:00
Gregory Schier
2a2fe700b4 Fix loading state 2025-01-20 14:23:24 -08:00
Gregory Schier
19403983b7 Better reading of response body 2025-01-20 14:12:49 -08:00
Gregory Schier
8ad7ac0bef Clean up model fetching and loading states 2025-01-20 13:44:11 -08:00
Gregory Schier
0453e84d38 Remove response text state key 2025-01-20 10:58:32 -08:00
Gregory Schier
b698a56549 Websockets for plugin runtime communication (#156) 2025-01-20 10:55:53 -08:00
Gregory Schier
095aaa5e92 Update release.yml 2025-01-19 06:34:50 -08:00
Gregory Schier
b1fe763591 Update release.yml 2025-01-19 05:53:09 -08:00
Gregory Schier
2257e88c51 Fix window creation due to manual state restore 2025-01-18 23:04:52 -08:00
Gregory Schier
9415a3a8d7 Remember size of settings window 2025-01-18 21:14:20 -08:00
Gregory Schier
590ef7839c NPM install 2025-01-18 17:57:47 -08:00
563 changed files with 26471 additions and 40439 deletions

15
.github/FUNDING.yml vendored Normal file
View File

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

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -65,7 +65,7 @@ jobs:
- name: install dependencies (windows only)
if: matrix.platform == 'windows-latest'
run: cargo install --force trusted-signing-cli
run: cargo install --force trusted-signing-cli --version 0.5.0
- name: Install NPM Dependencies
run: |
@@ -119,11 +119,7 @@ jobs:
with:
tagName: 'v__VERSION__'
releaseName: 'Release __VERSION__'
releaseBody: |
> [!IMPORTANT]
> The Yaak project is open source. However, to fund development, these pre-built binaries require a license for commercial use. Please see the [Pricing Page](https://yaak.app/pricing) for more details.
You can view the full release notes here → https://yaak.app/blog/2024.13.0
releaseBody: '[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)'
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}

View File

@@ -1,8 +0,0 @@
module.exports = {
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 100,
"bracketSpacing": true
}

8
.prettierrc.js Normal file
View File

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

View File

@@ -60,3 +60,10 @@ Run the app to apply the migrations.
If nothing happens, try `cargo clean` and run the app again.
_Note: Development builds use a separate database location from production builds._
## Lezer Grammer Generation
```sh
# Example
lezer-generator components/core/Editor/<LANG>/<LANG>.grammar > components/core/Editor/<LANG>/<LANG>.ts
```

View File

@@ -1,26 +1,34 @@
# Yaak API Client
Yaak is a desktop API client for organizing and executing REST, GraphQL, and gRPC
requests. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
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.
![screenshot](https://github.com/user-attachments/assets/f18e963f-0b68-4ecb-b8b8-cb71aa9aec02)
## Feedback and Bug Reports
All feedback, bug reports, questions, and feature requests should be reported to
[feedback.yaak.app](https://feedback.yaak.app). Issues will be duplicated
in this repository if applicable.
## Community Projects
- [`yaak2postman`](https://github.com/BiteCraft/yaak2postman) CLI for converting Yaak data
exports to Postman-compatible collections
![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. See the
[`good first issue`](https://github.com/yaakapp/app/labels/good%20first%20issue) label for
issues that are more approachable for contribution.
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.
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)

4789
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,16 @@
"packages/plugin-runtime",
"packages/plugin-runtime-types",
"packages/common-lib",
"src-tauri/yaak-crypto",
"src-tauri/yaak-git",
"src-tauri/yaak-license",
"src-tauri/yaak-mac-window",
"src-tauri/yaak-models",
"src-tauri/yaak-plugins",
"src-tauri/yaak-sse",
"src-tauri/yaak-sync",
"src-tauri/yaak-templates",
"src-tauri/yaak-ws",
"src-web"
],
"scripts": {
@@ -33,10 +37,13 @@
"tauri-before-build": "npm run bootstrap && npm run --workspaces --if-present build",
"tauri-before-dev": "npm run --workspaces --if-present dev"
},
"dependencies": {
"jotai": "^2.12.2"
},
"devDependencies": {
"@tauri-apps/cli": "^2.2.4",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"@tauri-apps/cli": "^2.4.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"eslint": "^8",
"eslint-config-prettier": "^8",
"eslint-plugin-import": "^2.31.0",
@@ -46,6 +53,6 @@
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.7.2"
"typescript": "^5.8.2"
}
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.3.2",
"version": "0.5.3",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [
@@ -20,8 +20,6 @@
"@types/node": "^22.5.4"
},
"devDependencies": {
"cpy-cli": "^5.0.0",
"npm-run-all": "^4.1.5",
"typescript": "^5.6.2"
"cpy-cli": "^5.0.0"
}
}

View File

@@ -1,288 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./models";
import type { Folder } from "./models";
import type { GrpcRequest } from "./models";
import type { HttpRequest } from "./models";
import type { HttpResponse } from "./models";
import type { JsonValue } from "./serde_json/JsonValue";
import type { Workspace } from "./models";
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, capabilities: Array<string>, };
export type CallHttpAuthenticationRequest = { config: { [key in string]?: JsonValue }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = {
/**
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders: Array<HttpHeader>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { key: string, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
export type CallTemplateFunctionResponse = { value: string | null, };
export type Color = "custom" | "default" | "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger";
export type CopyTextRequest = { text: string, };
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown";
export type EmptyPayload = {};
export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type ExportHttpRequestResponse = { content: string, };
export type FileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest;
export type FormInputBase = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputCheckbox = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputEditor = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Don't show the editor gutter (line numbers, folds, etc.)
*/
hideGutter?: boolean,
/**
* Language for syntax highlighting
*/
language?: EditorLanguage, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputFile = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputHttpRequest = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputSelect = {
/**
* The options that will be available in the select input
*/
options: Array<FormInputSelectOption>, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputSelectOption = { name: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Placeholder for the text input
*/
password?: boolean, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type GetHttpAuthenticationResponse = { name: string, label: string, shortLabel: string, config: Array<FormInput>, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, };
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpHeader = { name: string, value: string, };
export type HttpRequestAction = { key: string, label: string, icon?: Icon, };
export type Icon = "copy" | "info" | "check_circle" | "alert_triangle" | "_unknown";
export type ImportRequest = { content: string, };
export type ImportResources = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, };
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, replyId: string | null, payload: InternalEventPayload, windowContext: WindowContext, };
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": "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_request" } & EmptyPayload | { "type": "get_http_authentication_response" } & GetHttpAuthenticationResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "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;
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
/**
* Text to add to the cancel button
*/
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: HttpRequest, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<FormInput>, };
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };

View File

@@ -0,0 +1,411 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { HttpResponse } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, };
export type CallHttpAuthenticationActionArgs = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId: string, args: CallHttpAuthenticationActionArgs, };
export type CallHttpAuthenticationRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = {
/**
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders: Array<HttpHeader>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
export type CallTemplateFunctionResponse = { value: string | null, };
export type CloseWindowRequest = { label: string, };
export type Color = "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger";
export type CompletionOptionType = "constant" | "variable";
export type Content = { "type": "text", content: string, } | { "type": "markdown", content: string, };
export type CopyTextRequest = { text: string, };
export type DeleteKeyValueRequest = { key: string, };
export type DeleteKeyValueResponse = { deleted: boolean, };
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown";
export type EmptyPayload = {};
export type ErrorResponse = { error: string, };
export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type ExportHttpRequestResponse = { content: string, };
export type FileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
export type FormInputBanner = { inputs?: Array<FormInput>, hidden?: boolean, color?: Color, };
export type FormInputBase = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputCheckbox = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputEditor = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Don't show the editor gutter (line numbers, folds, etc.)
*/
hideGutter?: boolean,
/**
* Language for syntax highlighting
*/
language?: EditorLanguage, readOnly?: boolean, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputFile = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputHttpRequest = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputMarkdown = { content: string, hidden?: boolean, };
export type FormInputSelect = {
/**
* The options that will be available in the select input
*/
options: Array<FormInputSelectOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputSelectOption = { label: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Placeholder for the text input
*/
password?: boolean,
/**
* Whether to allow newlines in the input, like a <textarea/>
*/
multiLine?: boolean, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type GenericCompletionOption = { label: string, detail?: string, info?: string, type?: CompletionOptionType, boost?: number, };
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, };
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetKeyValueRequest = { key: string, };
export type GetKeyValueResponse = { value?: string, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, };
export type HttpHeader = { name: string, value: string, };
export type HttpRequestAction = { label: string, icon?: Icon, };
export type Icon = "alert_triangle" | "check" | "check_circle" | "chevron_down" | "copy" | "info" | "pin" | "search" | "trash" | "_unknown";
export type ImportRequest = { content: string, };
export type ImportResources = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };
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": "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 JsonPrimitive = string | number | boolean | null;
export type OpenWindowRequest = { url: string,
/**
* Label for the window. If not provided, a random one will be generated.
*/
label: string, title?: string, size?: WindowSize, dataDirKey?: string, };
export type PluginWindowContext = { "type": "none" } | { "type": "label", label: string, workspace_id: string | null, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
/**
* Text to add to the cancel button
*/
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: Partial<HttpRequest>, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type SetKeyValueRequest = { key: string, value: string, };
export type SetKeyValueResponse = {};
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<TemplateFunctionArg>, };
/**
* Similar to FormInput, but contains
*/
export type TemplateFunctionArg = FormInput;
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
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, environmentId: string | null, createdAt: string, updatedAt: string, name: string, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
@@ -22,4 +22,6 @@ export type HttpResponseState = "initialized" | "connected" | "closed";
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type JsonValue = number | string | Array<JsonValue> | { [key in string]?: JsonValue };
export type JsonValue = number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null;

View File

@@ -1 +1,2 @@
export type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
export type MaybePromise<T> = Promise<T> | T;

View File

@@ -1,5 +1,9 @@
export type * from './plugins';
export type * from './themes';
export * from './bindings/models';
export * from './bindings/events';
export * from './bindings/gen_models';
export * from './bindings/gen_events';
// Some extras for utility
export type { PartialImportResources } from './plugins/ImporterPlugin';

View File

@@ -1,13 +1,29 @@
import {
CallHttpAuthenticationActionArgs,
CallHttpAuthenticationRequest,
CallHttpAuthenticationResponse,
GetHttpAuthenticationResponse,
} from '..';
import type { Context } from './Context';
FormInput,
GetHttpAuthenticationConfigRequest,
GetHttpAuthenticationSummaryResponse,
HttpAuthenticationAction,
} from '../bindings/gen_events';
import { MaybePromise } from '../helpers';
import { Context } from './Context';
export type AuthenticationPlugin = Omit<GetHttpAuthenticationResponse, 'pluginName'> & {
type DynamicFormInput = FormInput & {
dynamic(
ctx: Context,
args: GetHttpAuthenticationConfigRequest,
): MaybePromise<Partial<FormInput> | undefined | null>;
};
export type AuthenticationPlugin = GetHttpAuthenticationSummaryResponse & {
args: (FormInput | DynamicFormInput)[];
onApply(
ctx: Context,
args: CallHttpAuthenticationRequest,
): Promise<CallHttpAuthenticationResponse> | CallHttpAuthenticationResponse;
): MaybePromise<CallHttpAuthenticationResponse>;
actions?: (HttpAuthenticationAction & {
onSelect(ctx: Context, args: CallHttpAuthenticationActionArgs): Promise<void> | void;
})[];
};

View File

@@ -3,6 +3,7 @@ import type {
FindHttpResponsesResponse,
GetHttpRequestByIdRequest,
GetHttpRequestByIdResponse,
OpenWindowRequest,
PromptTextRequest,
PromptTextResponse,
RenderHttpRequestRequest,
@@ -12,18 +13,31 @@ import type {
ShowToastRequest,
TemplateRenderRequest,
TemplateRenderResponse,
} from '..';
} from '../bindings/gen_events.ts';
export type Context = {
export interface Context {
clipboard: {
copyText(text: string): void;
copyText(text: string): Promise<void>;
};
toast: {
show(args: ShowToastRequest): void;
show(args: ShowToastRequest): Promise<void>;
};
prompt: {
text(args: PromptTextRequest): Promise<PromptTextResponse['value']>;
};
store: {
set<T>(key: string, value: T): Promise<void>;
get<T>(key: string): Promise<T | undefined>;
delete(key: string): Promise<boolean>;
};
window: {
openUrl(
args: OpenWindowRequest & {
onNavigate?: (args: { url: string }) => void;
onClose?: () => void;
},
): Promise<{ close: () => void }>;
};
httpRequest: {
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
@@ -35,4 +49,4 @@ export type Context = {
templates: {
render(args: TemplateRenderRequest): Promise<TemplateRenderResponse['data']>;
};
};
}

View File

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

View File

@@ -1,4 +1,4 @@
import type { CallHttpRequestActionArgs, HttpRequestAction } from '..';
import type { CallHttpRequestActionArgs, HttpRequestAction } from '../bindings/gen_events';
import type { Context } from './Context';
export type HttpRequestActionPlugin = HttpRequestAction & {

View File

@@ -1,13 +1,21 @@
import type { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '..';
import type { AtLeast } from '../helpers';
import { ImportResources } from '../bindings/gen_events';
import { AtLeast } from '../helpers';
import type { Context } from './Context';
type RootFields = 'name' | 'id' | 'model';
type CommonFields = RootFields | 'workspaceId';
export type PartialImportResources = {
workspaces: Array<AtLeast<ImportResources['workspaces'][0], RootFields>>;
environments: Array<AtLeast<ImportResources['environments'][0], CommonFields>>;
folders: Array<AtLeast<ImportResources['folders'][0], CommonFields>>;
httpRequests: Array<AtLeast<ImportResources['httpRequests'][0], CommonFields>>;
grpcRequests: Array<AtLeast<ImportResources['grpcRequests'][0], CommonFields>>;
websocketRequests: Array<AtLeast<ImportResources['websocketRequests'][0], CommonFields>>;
};
export type ImportPluginResponse = null | {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
grpcRequests: AtLeast<GrpcRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
resources: PartialImportResources;
};
export type ImporterPlugin = {

View File

@@ -1,6 +1,12 @@
import type { CallTemplateFunctionArgs, TemplateFunction } from '..';
import type { Context } from './Context';
import {
CallTemplateFunctionArgs,
TemplateFunction,
} from "../bindings/gen_events";
import { Context } from "./Context";
export type TemplateFunctionPlugin = TemplateFunction & {
onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>;
onRender(
ctx: Context,
args: CallTemplateFunctionArgs,
): Promise<string | null>;
};

View File

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

View File

@@ -24,7 +24,7 @@ export type Colors = {
danger?: string;
};
export type Theme = Colors & {
export type Index = Colors & {
id: string;
name: string;
components?: Partial<{

View File

@@ -1,3 +0,0 @@
build
node_modules
*.blob

View File

@@ -0,0 +1,10 @@
{
"name": "@yaakapp-internal/plugin-runtime",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@yaakapp-internal/plugin-runtime"
}
}
}

View File

@@ -3,19 +3,12 @@
"scripts": {
"bootstrap": "npm run build",
"build": "run-p build:*",
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/vendored/plugin-runtime/index.cjs",
"build:worker": "esbuild src/index.worker.ts --bundle --platform=node --outfile=../../src-tauri/vendored/plugin-runtime/index.worker.cjs",
"build:proto": "grpc_tools_node_protoc --ts_proto_out=src/gen --ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false --proto_path=../../proto ../../proto/plugins/*.proto"
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/vendored/plugin-runtime/index.cjs"
},
"dependencies": {
"intercept-stdout": "^0.1.2",
"long": "^5.2.3",
"nice-grpc": "^2.1.9",
"protobufjs": "^7.4.0"
"ws": "^8.18.0"
},
"devDependencies": {
"@types/intercept-stdout": "^0.1.3",
"grpc-tools": "^1.12.4",
"ts-proto": "^2.2.0"
"@types/ws": "^8.5.13"
}
}

View File

@@ -1,21 +1,19 @@
import type { InternalEvent } from '@yaakapp/api';
import EventEmitter from 'node:events';
import type { EventStreamEvent } from './gen/plugins/runtime';
export class EventChannel {
emitter: EventEmitter = new EventEmitter();
#listeners = new Set<(event: InternalEvent) => void>();
emit(e: InternalEvent) {
this.emitter.emit('__plugin_event__', { event: JSON.stringify(e) });
}
async *listen(): AsyncGenerator<EventStreamEvent> {
while (true) {
yield new Promise<EventStreamEvent>((resolve) => {
this.emitter.once('__plugin_event__', (event: EventStreamEvent) => {
resolve(event);
});
});
for (const l of this.#listeners) {
l(e);
}
}
listen(cb: (e: InternalEvent) => void) {
this.#listeners.add(cb);
}
unlisten(cb: (e: InternalEvent) => void) {
this.#listeners.delete(cb);
}
}

View File

@@ -1,56 +1,27 @@
import type { BootRequest, InternalEvent } from '@yaakapp-internal/plugins';
import path from 'node:path';
import { Worker } from 'node:worker_threads';
import type { BootRequest, InternalEvent } from '@yaakapp/api';
import type { EventChannel } from './EventChannel';
import type { PluginWorkerData } from './index.worker';
import { PluginInstance, PluginWorkerData } from './PluginInstance';
export class PluginHandle {
#worker: Worker;
#instance: PluginInstance;
constructor(
readonly pluginRefId: string,
readonly bootRequest: BootRequest,
readonly events: EventChannel,
readonly pluginToAppEvents: EventChannel,
) {
this.#worker = this.#createWorker();
}
sendToWorker(event: InternalEvent) {
this.#worker.postMessage(event);
}
async terminate() {
await this.#worker.terminate();
}
#createWorker(): Worker {
const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs');
const workerData: PluginWorkerData = {
pluginRefId: this.pluginRefId,
bootRequest: this.bootRequest,
};
const worker = new Worker(workerPath, {
workerData,
});
worker.on('message', (e) => this.events.emit(e));
worker.on('error', this.#handleError.bind(this));
worker.on('exit', this.#handleExit.bind(this));
console.log('Created plugin worker for ', this.bootRequest.dir);
return worker;
this.#instance = new PluginInstance(workerData, pluginToAppEvents);
}
async #handleError(err: Error) {
console.error('Plugin errored', this.bootRequest.dir, err);
sendToWorker(event: InternalEvent) {
this.#instance.postMessage(event);
}
async #handleExit(code: number) {
if (code === 0) {
console.log('Plugin exited successfully', this.bootRequest.dir);
} else {
console.log('Plugin exited with status', code, this.bootRequest.dir);
}
terminate() {
this.#instance.terminate();
}
}

View File

@@ -0,0 +1,582 @@
import { PluginWindowContext, TemplateFunctionArg } from '@yaakapp-internal/plugins';
import type {
BootRequest,
Context,
DeleteKeyValueResponse,
FindHttpResponsesResponse,
FormInput,
GetHttpRequestByIdResponse,
GetKeyValueResponse,
HttpAuthenticationAction,
HttpRequestAction,
InternalEvent,
InternalEventPayload,
JsonPrimitive,
PluginDefinition,
PromptTextResponse,
RenderHttpRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
TemplateRenderResponse,
} from '@yaakapp/api';
import console from 'node:console';
import { readFileSync, type Stats, statSync, watch } from 'node:fs';
import path from 'node:path';
// import util from 'node:util';
import { EventChannel } from './EventChannel';
// import { interceptStdout } from './interceptStdout';
import { migrateTemplateFunctionSelectOptions } from './migrations';
export interface PluginWorkerData {
bootRequest: BootRequest;
pluginRefId: string;
}
export class PluginInstance {
#workerData: PluginWorkerData;
#mod: PluginDefinition;
#pkg: { name?: string; version?: string };
#pluginToAppEvents: EventChannel;
#appToPluginEvents: EventChannel;
constructor(workerData: PluginWorkerData, pluginEvents: EventChannel) {
this.#workerData = workerData;
this.#pluginToAppEvents = pluginEvents;
this.#appToPluginEvents = new EventChannel();
// Forward incoming events to onMessage()
this.#appToPluginEvents.listen(async (event) => {
await this.#onMessage(event);
});
// Reload plugin if the JS or package.json changes
const windowContextNone: PluginWindowContext = { type: 'none' };
const fileChangeCallback = async () => {
this.#importModule();
return this.#sendPayload(windowContextNone, { type: 'reload_response' }, null);
};
if (this.#workerData.bootRequest.watch) {
watchFile(this.#pathMod(), fileChangeCallback);
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();
}
postMessage(event: InternalEvent) {
this.#appToPluginEvents.emit(event);
}
terminate() {
this.#unimportModule();
}
async #onMessage(event: InternalEvent) {
const ctx = this.#newCtx(event);
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);
return;
}
if (payload.type === 'terminate_request') {
const payload: InternalEventPayload = {
type: 'terminate_response',
};
this.#sendPayload(windowContext, payload, replyId);
return;
}
if (
payload.type === 'import_request' &&
typeof this.#mod?.importer?.onImport === 'function'
) {
const reply = await this.#mod.importer.onImport(ctx, {
text: payload.content,
});
if (reply != null) {
const replyPayload: InternalEventPayload = {
type: 'import_response',
// deno-lint-ignore no-explicit-any
resources: reply.resources as any,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
} else {
// Continue, to send back an empty reply
}
}
if (payload.type === 'filter_request' && typeof this.#mod?.filter?.onFilter === 'function') {
const reply = await this.#mod.filter.onFilter(ctx, {
filter: payload.filter,
payload: payload.content,
mimeType: payload.type,
});
const replyPayload: InternalEventPayload = {
type: 'filter_response',
content: reply.filtered,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_http_request_actions_request' &&
Array.isArray(this.#mod?.httpRequestActions)
) {
const reply: HttpRequestAction[] = this.#mod.httpRequestActions.map((a) => ({
...a,
// Add everything except onSelect
onSelect: undefined,
}));
const replyPayload: InternalEventPayload = {
type: 'get_http_request_actions_response',
pluginRefId: this.#workerData.pluginRefId,
actions: reply,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_template_functions_request' &&
Array.isArray(this.#mod?.templateFunctions)
) {
const reply: TemplateFunction[] = this.#mod.templateFunctions.map((templateFunction) => {
return {
...migrateTemplateFunctionSelectOptions(templateFunction),
// Add everything except render
onRender: undefined,
};
});
const replyPayload: InternalEventPayload = {
type: 'get_template_functions_response',
pluginRefId: this.#workerData.pluginRefId,
functions: reply,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'get_http_authentication_summary_request' && this.#mod?.authentication) {
const { name, shortLabel, label } = this.#mod.authentication;
const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_summary_response',
name,
label,
shortLabel,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
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) {
const dynamicAttrs = await v.dynamic(ctx, payload);
const { dynamic, ...other } = v;
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
} else {
resolvedArgs.push(v);
}
}
const resolvedActions: HttpAuthenticationAction[] = [];
for (const { onSelect, ...action } of actions ?? []) {
resolvedActions.push(action);
}
const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_config_response',
args: resolvedArgs,
actions: resolvedActions,
pluginRefId: this.#workerData.pluginRefId,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) {
const auth = this.#mod.authentication;
if (typeof auth?.onApply === 'function') {
applyFormInputDefaults(auth.args, payload.values);
const result = await auth.onApply(ctx, payload);
this.#sendPayload(
windowContext,
{
type: 'call_http_authentication_response',
setHeaders: result.setHeaders,
},
replyId,
);
return;
}
}
if (
payload.type === 'call_http_authentication_action_request' &&
this.#mod.authentication != null
) {
const action = this.#mod.authentication.actions?.[payload.index];
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
this.#sendEmpty(windowContext, replyId);
return;
}
}
if (
payload.type === 'call_http_request_action_request' &&
Array.isArray(this.#mod.httpRequestActions)
) {
const action = this.#mod.httpRequestActions[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)
) {
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,
);
return;
}
}
if (payload.type === 'reload_request') {
this.#importModule();
}
} catch (err) {
console.log('Plugin call threw exception', payload.type, err);
this.#sendPayload(
windowContext,
{
type: 'error_response',
error: `${err}`,
},
replyId,
);
return;
}
// No matches, so send back an empty response so the caller doesn't block forever
this.#sendEmpty(windowContext, replyId);
}
#pathMod() {
return path.posix.join(this.#workerData.bootRequest.dir, 'build', 'index.js');
}
#pathPkg() {
return path.join(this.#workerData.bootRequest.dir, 'package.json');
}
#unimportModule() {
const id = require.resolve(this.#pathMod());
delete require.cache[id];
}
#importModule() {
const id = require.resolve(this.#pathMod());
delete require.cache[id];
this.#mod = require(id).plugin;
}
#buildEventToSend(
windowContext: PluginWindowContext,
payload: InternalEventPayload,
replyId: string | null = null,
): InternalEvent {
return {
pluginRefId: this.#workerData.pluginRefId,
pluginName: path.basename(this.#workerData.bootRequest.dir),
id: genId(),
replyId,
payload,
windowContext,
};
}
#sendPayload(
windowContext: PluginWindowContext,
payload: InternalEventPayload,
replyId: string | null,
): string {
const event = this.#buildEventToSend(windowContext, payload, replyId);
this.#sendEvent(event);
return event.id;
}
#sendEvent(event: InternalEvent) {
// if (event.payload.type !== 'empty_response') {
// console.log('Sending event to app', this.#pkg.name, event.id, event.payload.type);
// }
this.#pluginToAppEvents.emit(event);
}
#sendEmpty(windowContext: PluginWindowContext, replyId: string | null = null): string {
return this.#sendPayload(windowContext, { type: 'empty_response' }, replyId);
}
#sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
windowContext: PluginWindowContext,
payload: InternalEventPayload,
): Promise<T> {
// 1. Build event to send
const eventToSend = this.#buildEventToSend(windowContext, payload, null);
// 2. Spawn listener in background
const promise = new Promise<T>((resolve) => {
const cb = (event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
this.#appToPluginEvents.unlisten(cb); // Unlisten, now that we're done
const { type: _, ...payload } = event.payload;
resolve(payload as T);
}
};
this.#appToPluginEvents.listen(cb);
});
// 3. Send the event after we start listening (to prevent race)
this.#sendEvent(eventToSend);
// 4. Return the listener promise
return promise as unknown as Promise<T>;
}
#sendAndListenForEvents(
windowContext: PluginWindowContext,
payload: InternalEventPayload,
onEvent: (event: InternalEventPayload) => void,
): void {
// 1. Build event to send
const eventToSend = this.#buildEventToSend(windowContext, payload, null);
// 2. Listen for replies in the background
this.#appToPluginEvents.listen((event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
onEvent(event.payload);
}
});
// 3. Send the event after we start listening (to prevent race)
this.#sendEvent(eventToSend);
}
#newCtx(event: InternalEvent): Context {
return {
clipboard: {
copyText: async (text) => {
await this.#sendAndWaitForReply(event.windowContext, {
type: 'copy_text_request',
text,
});
},
},
toast: {
show: async (args) => {
await this.#sendAndWaitForReply(event.windowContext, {
type: 'show_toast_request',
...args,
});
},
},
window: {
openUrl: async ({ onNavigate, onClose, ...args }) => {
args.label = args.label || `${Math.random()}`;
const payload: InternalEventPayload = { type: 'open_window_request', ...args };
const onEvent = (event: InternalEventPayload) => {
if (event.type === 'window_navigate_event') {
onNavigate?.(event);
} else if (event.type === 'window_close_event') {
onClose?.();
}
};
this.#sendAndListenForEvents(event.windowContext, payload, onEvent);
return {
close: () => {
const closePayload: InternalEventPayload = {
type: 'close_window_request',
label: args.label,
};
this.#sendPayload(event.windowContext, closePayload, null);
},
};
},
},
prompt: {
text: async (args) => {
const reply: PromptTextResponse = await this.#sendAndWaitForReply(event.windowContext, {
type: 'prompt_text_request',
...args,
});
return reply.value;
},
},
httpResponse: {
find: async (args) => {
const payload = {
type: 'find_http_responses_request',
...args,
} as const;
const { httpResponses } = await this.#sendAndWaitForReply<FindHttpResponsesResponse>(
event.windowContext,
payload,
);
return httpResponses;
},
},
httpRequest: {
getById: async (args) => {
const payload = {
type: 'get_http_request_by_id_request',
...args,
} as const;
const { httpRequest } = await this.#sendAndWaitForReply<GetHttpRequestByIdResponse>(
event.windowContext,
payload,
);
return httpRequest;
},
send: async (args) => {
const payload = {
type: 'send_http_request_request',
...args,
} as const;
const { httpResponse } = await this.#sendAndWaitForReply<SendHttpRequestResponse>(
event.windowContext,
payload,
);
return httpResponse;
},
render: async (args) => {
const payload = {
type: 'render_http_request_request',
...args,
} as const;
const { httpRequest } = await this.#sendAndWaitForReply<RenderHttpRequestResponse>(
event.windowContext,
payload,
);
return httpRequest;
},
},
templates: {
/**
* Invoke Yaak's template engine to render a value. If the value is a nested type
* (eg. object), it will be recursively rendered.
*/
render: async (args) => {
const payload = { type: 'template_render_request', ...args } as const;
const result = await this.#sendAndWaitForReply<TemplateRenderResponse>(
event.windowContext,
payload,
);
return result.data;
},
},
store: {
get: async <T>(key: string) => {
const payload = { type: 'get_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<GetKeyValueResponse>(
event.windowContext,
payload,
);
return result.value ? (JSON.parse(result.value) as T) : undefined;
},
set: async <T>(key: string, value: T) => {
const valueStr = JSON.stringify(value);
const payload: InternalEventPayload = {
type: 'set_key_value_request',
key,
value: valueStr,
};
await this.#sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
},
delete: async (key: string) => {
const payload = { type: 'delete_key_value_request', key } as const;
const result = await this.#sendAndWaitForReply<DeleteKeyValueResponse>(
event.windowContext,
payload,
);
return result.deleted;
},
},
};
}
}
function genId(len = 5): string {
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let id = '';
for (let i = 0; i < len; i++) {
id += alphabet[Math.floor(Math.random() * alphabet.length)];
}
return id;
}
/** Recursively apply form input defaults to a set of values */
function applyFormInputDefaults(
inputs: TemplateFunctionArg[],
values: { [p: string]: JsonPrimitive | undefined },
) {
for (const input of inputs) {
if ('inputs' in input) {
applyFormInputDefaults(input.inputs ?? [], values);
} else if ('defaultValue' in input && values[input.name] === undefined) {
values[input.name] = input.defaultValue;
}
}
}
const watchedFiles: Record<string, Stats> = {};
/**
* Watch a file and trigger callback on change.
*
* We also track the stat for each file because fs.watch() will
* trigger a "change" event when the access date changes
*/
function watchFile(filepath: string, cb: (filepath: string) => void) {
watch(filepath, () => {
const stat = statSync(filepath);
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
cb(filepath);
}
watchedFiles[filepath] = stat;
});
}

View File

@@ -1,126 +0,0 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.2.3
// protoc v3.19.1
// source: plugins/runtime.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
import { type CallContext, type CallOptions } from "nice-grpc-common";
export const protobufPackage = "yaak.plugins.runtime";
export interface EventStreamEvent {
event: string;
}
function createBaseEventStreamEvent(): EventStreamEvent {
return { event: "" };
}
export const EventStreamEvent: MessageFns<EventStreamEvent> = {
encode(message: EventStreamEvent, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.event !== "") {
writer.uint32(10).string(message.event);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): EventStreamEvent {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseEventStreamEvent();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.event = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): EventStreamEvent {
return { event: isSet(object.event) ? globalThis.String(object.event) : "" };
},
toJSON(message: EventStreamEvent): unknown {
const obj: any = {};
if (message.event !== "") {
obj.event = message.event;
}
return obj;
},
create(base?: DeepPartial<EventStreamEvent>): EventStreamEvent {
return EventStreamEvent.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<EventStreamEvent>): EventStreamEvent {
const message = createBaseEventStreamEvent();
message.event = object.event ?? "";
return message;
},
};
export type PluginRuntimeDefinition = typeof PluginRuntimeDefinition;
export const PluginRuntimeDefinition = {
name: "PluginRuntime",
fullName: "yaak.plugins.runtime.PluginRuntime",
methods: {
eventStream: {
name: "EventStream",
requestType: EventStreamEvent,
requestStream: true,
responseType: EventStreamEvent,
responseStream: true,
options: {},
},
},
} as const;
export interface PluginRuntimeServiceImplementation<CallContextExt = {}> {
eventStream(
request: AsyncIterable<EventStreamEvent>,
context: CallContext & CallContextExt,
): ServerStreamingMethodResult<DeepPartial<EventStreamEvent>>;
}
export interface PluginRuntimeClient<CallOptionsExt = {}> {
eventStream(
request: AsyncIterable<DeepPartial<EventStreamEvent>>,
options?: CallOptions & CallOptionsExt,
): AsyncIterable<EventStreamEvent>;
}
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
export type DeepPartial<T> = T extends Builtin ? T
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
export type ServerStreamingMethodResult<Response> = { [Symbol.asyncIterator](): AsyncIterator<Response, void> };
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter;
decode(input: BinaryReader | Uint8Array, length?: number): T;
fromJSON(object: any): T;
toJSON(message: T): unknown;
create(base?: DeepPartial<T>): T;
fromPartial(object: DeepPartial<T>): T;
}

View File

@@ -1,52 +1,55 @@
import type { InternalEvent } from '@yaakapp/api';
import { createChannel, createClient, Status } from 'nice-grpc';
import { EventChannel } from './EventChannel';
import type { PluginRuntimeClient} from './gen/plugins/runtime';
import { PluginRuntimeDefinition } from './gen/plugins/runtime';
import { PluginHandle } from './PluginHandle';
import WebSocket from 'ws';
const port = process.env.PORT || '50051';
const port = process.env.PORT;
if (!port) {
throw new Error('Plugin runtime missing PORT')
}
const channel = createChannel(`localhost:${port}`, undefined, {
'grpc.max_receive_message_length': Number.MAX_SAFE_INTEGER,
'grpc.max_send_message_length': Number.MAX_SAFE_INTEGER,
});
const client: PluginRuntimeClient = createClient(PluginRuntimeDefinition, channel);
const events = new EventChannel();
const pluginToAppEvents = new EventChannel();
const plugins: Record<string, PluginHandle> = {};
(async () => {
const ws = new WebSocket(`ws://localhost:${port}`);
ws.on('message', async (e: Buffer) => {
try {
for await (const e of client.eventStream(events.listen())) {
const pluginEvent: InternalEvent = JSON.parse(e.event);
// Handle special event to bootstrap plugin
if (pluginEvent.payload.type === 'boot_request') {
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.payload, events);
plugins[pluginEvent.pluginRefId] = plugin;
}
// Once booted, forward all events to the plugin worker
const plugin = plugins[pluginEvent.pluginRefId];
if (!plugin) {
console.warn('Failed to get plugin for event by', pluginEvent.pluginRefId);
continue;
}
if (pluginEvent.payload.type === 'terminate_request') {
await plugin.terminate();
console.log('Terminated plugin worker', pluginEvent.pluginRefId);
delete plugins[pluginEvent.pluginRefId];
}
plugin.sendToWorker(pluginEvent);
}
console.log('Stream ended');
} catch (err: any) {
if (err.code === Status.CANCELLED) {
console.log('Stream was cancelled by server');
} else {
console.log('Client stream errored', err);
}
await handleIncoming(e.toString());
} catch (err) {
console.log('Failed to handle incoming plugin event', err);
}
})();
});
ws.on('open', () => console.log('Plugin runtime connected to websocket'));
ws.on('error', (err: any) => console.error('Plugin runtime websocket error', err));
ws.on('close', (code: number) => console.log('Plugin runtime websocket closed', code));
// Listen for incoming events from plugins
pluginToAppEvents.listen((e) => {
const eventStr = JSON.stringify(e);
ws.send(eventStr);
});
async function handleIncoming(msg: string) {
const pluginEvent: InternalEvent = JSON.parse(msg);
// Handle special event to bootstrap plugin
if (pluginEvent.payload.type === 'boot_request') {
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.payload, pluginToAppEvents);
plugins[pluginEvent.pluginRefId] = plugin;
}
// Once booted, forward all events to the plugin worker
const plugin = plugins[pluginEvent.pluginRefId];
if (!plugin) {
console.warn('Failed to get plugin for event by', pluginEvent.pluginRefId);
return;
}
if (pluginEvent.payload.type === 'terminate_request') {
plugin.terminate();
console.log('Terminated plugin worker', pluginEvent.pluginRefId);
delete plugins[pluginEvent.pluginRefId];
}
plugin.sendToWorker(pluginEvent);
}

View File

@@ -1,427 +0,0 @@
import type {
BootRequest,
FindHttpResponsesResponse,
GetHttpRequestByIdResponse,
HttpRequestAction,
ImportResponse,
InternalEvent,
InternalEventPayload,
PromptTextResponse,
RenderHttpRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
TemplateRenderResponse,
WindowContext,
} from '@yaakapp-internal/plugins';
import type { Context } from '@yaakapp/api';
import type { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/HttpRequestActionPlugin';
import type { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
import interceptStdout from 'intercept-stdout';
import * as console from 'node:console';
import type { Stats } from 'node:fs';
import { readFileSync, statSync, watch } from 'node:fs';
import path from 'node:path';
import * as util from 'node:util';
import { parentPort, workerData } from 'node:worker_threads';
export interface PluginWorkerData {
bootRequest: BootRequest;
pluginRefId: string;
}
async function initialize() {
const {
bootRequest: { dir: pluginDir, watch: enableWatch },
pluginRefId,
}: PluginWorkerData = workerData;
const pathPkg = path.join(pluginDir, 'package.json');
const pathMod = path.posix.join(pluginDir, 'build', 'index.js');
async function importModule() {
const id = require.resolve(pathMod);
delete require.cache[id];
return require(id);
}
const pkg = JSON.parse(readFileSync(pathPkg, 'utf8'));
prefixStdout(`[plugin][${pkg.name}] %s`);
let mod = await importModule();
const capabilities: string[] = [];
if (typeof mod.pluginHookExport === 'function') capabilities.push('export');
if (typeof mod.pluginHookImport === 'function') capabilities.push('import');
if (typeof mod.pluginHookResponseFilter === 'function') capabilities.push('filter');
console.log('Plugin initialized', pkg.name, { capabilities, enableWatch });
function buildEventToSend(
windowContext: WindowContext,
payload: InternalEventPayload,
replyId: string | null = null,
): InternalEvent {
return { pluginRefId, id: genId(), replyId, payload, windowContext };
}
function sendEmpty(windowContext: WindowContext, replyId: string | null = null): string {
return sendPayload(windowContext, { type: 'empty_response' }, replyId);
}
function sendPayload(
windowContext: WindowContext,
payload: InternalEventPayload,
replyId: string | null,
): string {
const event = buildEventToSend(windowContext, payload, replyId);
sendEvent(event);
return event.id;
}
function sendEvent(event: InternalEvent) {
if (event.payload.type !== 'empty_response') {
console.log('Sending event to app', event.id, event.payload.type);
}
parentPort!.postMessage(event);
}
async function sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
windowContext: WindowContext,
payload: InternalEventPayload,
): Promise<T> {
// 1. Build event to send
const eventToSend = buildEventToSend(windowContext, payload, null);
// 2. Spawn listener in background
const promise = new Promise<InternalEventPayload>(async (resolve) => {
const cb = (event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
parentPort!.off('message', cb); // Unlisten, now that we're done
resolve(event.payload); // Not type-safe but oh well
}
};
parentPort!.on('message', cb);
});
// 3. Send the event after we start listening (to prevent race)
sendEvent(eventToSend);
// 4. Return the listener promise
return promise as unknown as Promise<T>;
}
async function reloadModule() {
mod = await importModule();
}
// Reload plugin if JS or package.json changes
const windowContextNone: WindowContext = { type: 'none' };
const cb = async () => {
await reloadModule();
return sendPayload(windowContextNone, { type: 'reload_response' }, null);
};
if (enableWatch) {
watchFile(pathMod, cb);
watchFile(pathPkg, cb);
}
const newCtx = (event: InternalEvent): Context => ({
clipboard: {
async copyText(text) {
await sendAndWaitForReply(event.windowContext, { type: 'copy_text_request', text });
},
},
toast: {
async show(args) {
await sendAndWaitForReply(event.windowContext, { type: 'show_toast_request', ...args });
},
},
prompt: {
async text(args) {
const reply: PromptTextResponse = await sendAndWaitForReply(event.windowContext, {
type: 'prompt_text_request',
...args,
});
return reply.value;
},
},
httpResponse: {
async find(args) {
const payload = { type: 'find_http_responses_request', ...args } as const;
const { httpResponses } = await sendAndWaitForReply<FindHttpResponsesResponse>(
event.windowContext,
payload,
);
return httpResponses;
},
},
httpRequest: {
async getById(args) {
const payload = { type: 'get_http_request_by_id_request', ...args } as const;
const { httpRequest } = await sendAndWaitForReply<GetHttpRequestByIdResponse>(
event.windowContext,
payload,
);
return httpRequest;
},
async send(args) {
const payload = { type: 'send_http_request_request', ...args } as const;
const { httpResponse } = await sendAndWaitForReply<SendHttpRequestResponse>(
event.windowContext,
payload,
);
return httpResponse;
},
async render(args) {
const payload = { type: 'render_http_request_request', ...args } as const;
const { httpRequest } = await sendAndWaitForReply<RenderHttpRequestResponse>(
event.windowContext,
payload,
);
return httpRequest;
},
},
templates: {
/**
* Invoke Yaak's template engine to render a value. If the value is a nested type
* (eg. object), it will be recursively rendered.
* */
async render(args) {
const payload = { type: 'template_render_request', ...args } as const;
const result = await sendAndWaitForReply<TemplateRenderResponse>(
event.windowContext,
payload,
);
return result.data;
},
},
});
// Message comes into the plugin to be processed
parentPort!.on('message', async (event: InternalEvent) => {
const { windowContext, payload, id: replyId } = event;
const ctx = newCtx(event);
try {
if (payload.type === 'boot_request') {
const payload: InternalEventPayload = {
type: 'boot_response',
name: pkg.name,
version: pkg.version,
capabilities,
};
sendPayload(windowContext, payload, replyId);
return;
}
if (payload.type === 'terminate_request') {
const payload: InternalEventPayload = {
type: 'terminate_response',
};
sendPayload(windowContext, payload, replyId);
return;
}
if (payload.type === 'import_request' && typeof mod.pluginHookImport === 'function') {
const reply: ImportResponse | null = await mod.pluginHookImport(ctx, payload.content);
if (reply != null) {
const replyPayload: InternalEventPayload = {
type: 'import_response',
resources: reply?.resources,
};
sendPayload(windowContext, replyPayload, replyId);
return;
} else {
// Continue, to send back an empty reply
}
}
if (
payload.type === 'export_http_request_request' &&
typeof mod.pluginHookExport === 'function'
) {
const reply: string = await mod.pluginHookExport(ctx, payload.httpRequest);
const replyPayload: InternalEventPayload = {
type: 'export_http_request_response',
content: reply,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'filter_request' && typeof mod.pluginHookResponseFilter === 'function') {
const reply: string = await mod.pluginHookResponseFilter(ctx, {
filter: payload.filter,
body: payload.content,
});
const replyPayload: InternalEventPayload = {
type: 'filter_response',
content: reply,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_http_request_actions_request' &&
Array.isArray(mod.plugin?.httpRequestActions)
) {
const reply: HttpRequestAction[] = mod.plugin.httpRequestActions.map(
(a: HttpRequestActionPlugin) => ({
...a,
// Add everything except onSelect
onSelect: undefined,
}),
);
const replyPayload: InternalEventPayload = {
type: 'get_http_request_actions_response',
pluginRefId,
actions: reply,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_template_functions_request' &&
Array.isArray(mod.plugin?.templateFunctions)
) {
const reply: TemplateFunction[] = mod.plugin.templateFunctions.map(
(a: TemplateFunctionPlugin) => ({
...a,
// Add everything except render
onRender: undefined,
}),
);
const replyPayload: InternalEventPayload = {
type: 'get_template_functions_response',
pluginRefId,
functions: reply,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'get_http_authentication_request' && mod.plugin?.authentication) {
const auth = mod.plugin.authentication;
const replyPayload: InternalEventPayload = {
...auth,
type: 'get_http_authentication_response',
// Remove unneeded attrs
onApply: undefined,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'call_http_authentication_request' && mod.plugin?.authentication) {
const auth = mod.plugin.authentication;
if (typeof auth?.onApply === 'function') {
const result = await auth.onApply(ctx, payload);
sendPayload(
windowContext,
{
...result,
type: 'call_http_authentication_response',
},
replyId,
);
return;
}
}
if (
payload.type === 'call_http_request_action_request' &&
Array.isArray(mod.plugin?.httpRequestActions)
) {
const action = mod.plugin.httpRequestActions.find(
(a: HttpRequestActionPlugin) => a.key === payload.key,
);
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
sendEmpty(windowContext, replyId);
return;
}
}
if (
payload.type === 'call_template_function_request' &&
Array.isArray(mod.plugin?.templateFunctions)
) {
const action = mod.plugin.templateFunctions.find(
(a: TemplateFunctionPlugin) => a.name === payload.name,
);
if (typeof action?.onRender === 'function') {
const result = await action.onRender(ctx, payload.args);
sendPayload(
windowContext,
{
type: 'call_template_function_response',
value: result ?? null,
},
replyId,
);
return;
}
}
if (payload.type === 'reload_request') {
await reloadModule();
}
} catch (err) {
console.log('Plugin call threw exception', payload.type, err);
// TODO: Return errors to server
}
// No matches, so send back an empty response so the caller doesn't block forever
sendEmpty(windowContext, replyId);
});
}
initialize().catch((err) => {
console.log('failed to boot plugin', err);
});
function genId(len = 5): string {
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let id = '';
for (let i = 0; i < len; i++) {
id += alphabet[Math.floor(Math.random() * alphabet.length)];
}
return id;
}
function prefixStdout(s: string) {
if (!s.includes('%s')) {
throw new Error('Console prefix must contain a "%s" replacer');
}
interceptStdout((text) => {
const lines = text.split(/\n/);
let newText = '';
for (let i = 0; i < lines.length; i++) {
if (lines[i] == '') continue;
newText += util.format(s, lines[i]) + '\n';
}
return newText.trimEnd();
});
}
const watchedFiles: Record<string, Stats> = {};
/**
* Watch a file and trigger callback on change.
*
* We also track the stat for each file because fs.watch will
* trigger a "change" event when the access date changes
*/
function watchFile(filepath: string, cb: (filepath: string) => void) {
watch(filepath, (_event, _name) => {
const stat = statSync(filepath);
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
cb(filepath);
}
watchedFiles[filepath] = stat;
});
}

View File

@@ -0,0 +1,37 @@
import process from "node:process";
export function interceptStdout(
intercept: (text: string) => string,
) {
const old_stdout_write = process.stdout.write;
const old_stderr_write = process.stderr.write;
process.stdout.write = (function (write) {
return function (text: string) {
arguments[0] = interceptor(text, intercept);
// deno-lint-ignore no-explicit-any
write.apply(process.stdout, arguments as any);
return true;
};
})(process.stdout.write);
process.stderr.write = (function (write) {
return function (text: string) {
arguments[0] = interceptor(text, intercept);
// deno-lint-ignore no-explicit-any
write.apply(process.stderr, arguments as any);
return true;
};
})(process.stderr.write);
// puts back to original
return function unhook() {
process.stdout.write = old_stdout_write;
process.stderr.write = old_stderr_write;
};
}
function interceptor(text: string, fn: (text: string) => string) {
return fn(text).replace(/\n$/, "") +
(fn(text) && /\n$/.test(text) ? "\n" : "");
}

View File

@@ -0,0 +1,18 @@
import { TemplateFunction } from '@yaakapp/api';
export function migrateTemplateFunctionSelectOptions(f: TemplateFunction): TemplateFunction {
const migratedArgs = f.args.map((a) => {
if (a.type === 'select') {
a.options = a.options.map((o) => ({
...o,
label: o.label || (o as any).name,
}));
}
return a;
});
return {
...f,
args: migratedArgs,
};
}

View File

@@ -8,6 +8,7 @@
"lib": ["es2021"],
"noImplicitAny": false,
"moduleResolution": "node16",
"resolveJsonModule": true,
"sourceMap": true,
"outDir": "build",
"baseUrl": ".",

View File

@@ -1,4 +1,4 @@
edition = "2018"
edition = "2024"
# Widths
chain_width = 100

View File

@@ -9,12 +9,14 @@ const NODE_VERSION = 'v22.9.0';
// `${process.platform}_${process.arch}`
const MAC_ARM = 'darwin_arm64';
const MAC_X64 = 'darwin_x64';
const LNX_ARM = 'linux_arm64';
const LNX_X64 = 'linux_x64';
const WIN_X64 = 'win32_x64';
const URL_MAP = {
[MAC_ARM]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-darwin-arm64.tar.gz`,
[MAC_X64]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-darwin-x64.tar.gz`,
[LNX_ARM]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz`,
[LNX_X64]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz`,
[WIN_X64]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-win-x64.zip`,
};
@@ -22,15 +24,17 @@ const URL_MAP = {
const SRC_BIN_MAP = {
[MAC_ARM]: `node-${NODE_VERSION}-darwin-arm64/bin/node`,
[MAC_X64]: `node-${NODE_VERSION}-darwin-x64/bin/node`,
[LNX_ARM]: `node-${NODE_VERSION}-linux-arm64/bin/node`,
[LNX_X64]: `node-${NODE_VERSION}-linux-x64/bin/node`,
[WIN_X64]: `node-${NODE_VERSION}-win-x64/node.exe`,
};
const DST_BIN_MAP = {
darwin_arm64: 'yaaknode-aarch64-apple-darwin',
darwin_x64: 'yaaknode-x86_64-apple-darwin',
linux_x64: 'yaaknode-x86_64-unknown-linux-gnu',
win32_x64: 'yaaknode-x86_64-pc-windows-msvc.exe',
[MAC_ARM]: 'yaaknode-aarch64-apple-darwin',
[MAC_X64]: 'yaaknode-x86_64-apple-darwin',
[LNX_ARM]: 'yaaknode-aarch64-unknown-linux-gnu',
[LNX_X64]: 'yaaknode-x86_64-unknown-linux-gnu',
[WIN_X64]: 'yaaknode-x86_64-pc-windows-msvc.exe',
};
const key = `${process.platform}_${process.env.YAAK_TARGET_ARCH ?? process.arch}`;

View File

@@ -9,12 +9,14 @@ const VERSION = '28.3';
// `${process.platform}_${process.arch}`
const MAC_ARM = 'darwin_arm64';
const MAC_X64 = 'darwin_x64';
const LNX_ARM = 'linux_arm64';
const LNX_X64 = 'linux_x64';
const WIN_X64 = 'win32_x64';
const URL_MAP = {
[MAC_ARM]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-osx-aarch_64.zip`,
[MAC_X64]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-osx-x86_64.zip`,
[LNX_ARM]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-linux-aarch_64.zip`,
[LNX_X64]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-linux-x86_64.zip`,
[WIN_X64]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-win64.zip`,
};
@@ -22,6 +24,7 @@ const URL_MAP = {
const SRC_BIN_MAP = {
[MAC_ARM]: 'bin/protoc',
[MAC_X64]: 'bin/protoc',
[LNX_ARM]: 'bin/protoc',
[LNX_X64]: 'bin/protoc',
[WIN_X64]: 'bin/protoc.exe',
};
@@ -29,6 +32,7 @@ const SRC_BIN_MAP = {
const DST_BIN_MAP = {
[MAC_ARM]: 'yaakprotoc-aarch64-apple-darwin',
[MAC_X64]: 'yaakprotoc-x86_64-apple-darwin',
[LNX_ARM]: 'yaakprotoc-aarch64-unknown-linux-gnu',
[LNX_X64]: 'yaakprotoc-x86_64-unknown-linux-gnu',
[WIN_X64]: 'yaakprotoc-x86_64-pc-windows-msvc.exe',
};

View File

@@ -4,3 +4,8 @@ target/
vendored/*
!vendored/plugins
gen/*
**/permissions/autogenerated
**/permissions/schemas

1903
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,23 @@
[workspace]
members = [
"yaak-crypto",
"yaak-git",
"yaak-grpc",
"yaak-http",
"yaak-license",
"yaak-mac-window",
"yaak-models",
"yaak-plugins",
"yaak-sse",
"yaak-sync",
"yaak-templates",
"yaak-ws",
]
[package]
name = "yaak-app"
version = "0.0.0"
edition = "2021"
edition = "2024"
authors = ["Gregory Schier"]
publish = false
@@ -28,63 +33,70 @@ strip = true # Automatically strip symbols from the binary.
cargo-clippy = []
[build-dependencies]
tauri-build = { version = "2.0.5", features = [] }
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
cocoa = "0.26.0"
tauri-build = { version = "2.1.1", features = [] }
[target.'cfg(target_os = "linux")'.dependencies]
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
[dependencies]
chrono = { version = "0.4.31", features = ["serde"] }
datetime = "0.5.2"
encoding_rs = "0.8.35"
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
hex_color = "3.0.0"
http = { version = "1.2.0", default-features = false }
log = "0.4.21"
rand = "0.8.5"
regex = "1.10.2"
log = "0.4.27"
md5 = "0.7.0"
mime_guess = "2.0.5"
rand = "0.9.0"
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider"] }
reqwest_cookie_store = "0.8.0"
rustls = { version = "0.23.21", default-features = false }
rustls-platform-verifier = "0.5.0"
rustls = { version = "0.23.25", default-features = false, features = ["custom-provider", "ring"] }
rustls-platform-verifier = "0.5.1"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
tauri-plugin-clipboard-manager = "2.2.0"
tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.2.0"
tauri-plugin-log = { version = "2.2.0", features = ["colored"] }
tauri-plugin-opener = "2.2.4"
tauri-plugin-os = "2.2.0"
tauri-plugin-log = { version = "2.3.1", features = ["colored"] }
tauri-plugin-opener = "2.2.6"
tauri-plugin-os = "2.2.1"
tauri-plugin-shell = { workspace = true }
tauri-plugin-single-instance = "2.2.1"
tauri-plugin-updater = "2.3.1"
tauri-plugin-window-state = "2.2.0"
tokio = { version = "1.36.0", features = ["sync"] }
tokio-stream = "0.1.15"
ts-rs = { workspace = true }
mime_guess = "2.0.5"
urlencoding = "2.1.3"
uuid = "1.7.0"
tauri-plugin-single-instance = "2.2.2"
tauri-plugin-updater = "2.6.1"
tauri-plugin-window-state = "2.2.1"
thiserror = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
tokio-stream = "0.1.17"
uuid = "1.12.1"
yaak-common = { workspace = true }
yaak-crypto = { workspace = true }
yaak-git = { path = "yaak-git" }
yaak-grpc = { path = "yaak-grpc" }
yaak-http = { workspace = true }
yaak-license = { path = "yaak-license" }
yaak-mac-window = { path = "yaak-mac-window" }
yaak-models = { workspace = true }
yaak-plugins = { workspace = true }
yaak-sse = { workspace = true }
yaak-sync = { path = "yaak-sync" }
yaak-templates = { path = "yaak-templates" }
yaak-sync = { workspace = true }
yaak-templates = { workspace = true }
yaak-ws = { path = "yaak-ws" }
[workspace.dependencies]
reqwest = "0.12.15"
serde = "1.0.219"
serde_json = "1.0.140"
tauri = "2.4.1"
tauri-plugin = "2.1.1"
tauri-plugin-shell = "2.2.1"
tokio = "1.44.2"
thiserror = "2.0.12"
ts-rs = "10.1.0"
yaak-common = { path = "yaak-common" }
yaak-http = { path = "yaak-http" }
yaak-models = { path = "yaak-models" }
yaak-sse = { path = "yaak-sse" }
yaak-plugins = { path = "yaak-plugins" }
serde = "1.0.215"
serde_json = "1.0.132"
tauri-plugin-shell = "2.2.0"
tauri = "2.2.0"
thiserror = "2.0.3"
ts-rs = "10.0.0"
reqwest = "0.12.12"
yaak-sse = { path = "yaak-sse" }
yaak-sync = { path = "yaak-sync" }
yaak-templates = { path = "yaak-templates" }
yaak-crypto = { path = "yaak-crypto" }

View File

@@ -1,5 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AnalyticsAction = "cancel" | "click" | "commit" | "create" | "delete" | "delete_many" | "duplicate" | "error" | "export" | "hide" | "import" | "launch" | "launch_first" | "launch_update" | "send" | "show" | "toggle" | "update" | "upsert";
export type AnalyticsResource = "app" | "appearance" | "button" | "checkbox" | "cookie_jar" | "dialog" | "environment" | "folder" | "grpc_connection" | "grpc_event" | "grpc_request" | "http_request" | "http_response" | "key_value" | "link" | "mutation" | "plugin" | "select" | "setting" | "sidebar" | "tab" | "theme" | "workspace";

View File

@@ -7,6 +7,7 @@
"*"
],
"permissions": [
"core:app:allow-identifier",
"core:event:allow-emit",
"core:event:allow-listen",
"core:event:allow-unlisten",
@@ -30,10 +31,8 @@
}
]
},
"opener:allow-open-url",
"opener:allow-open-path",
"opener:allow-default-urls",
"opener:allow-reveal-item-in-dir",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"core:webview:allow-set-webview-zoom",
"core:window:allow-close",
"core:window:allow-internal-toggle-maximize",
@@ -47,9 +46,17 @@
"core:window:allow-start-dragging",
"core:window:allow-theme",
"core:window:allow-unmaximize",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"opener:allow-default-urls",
"opener:allow-open-path",
"opener:allow-open-url",
"opener:allow-reveal-item-in-dir",
"shell:allow-open",
"yaak-crypto:default",
"yaak-git:default",
"yaak-license:default",
"yaak-sync:default"
"yaak-mac-window:default",
"yaak-models:default",
"yaak-sync:default",
"yaak-ws:default"
]
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-dir","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"opener:allow-open-url","opener:allow-open-path","opener:allow-default-urls","opener:allow-reveal-item-in-dir","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-is-maximized","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","yaak-license:default","yaak-sync:default"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
-- This setting was moved to the new workspace_metas table
ALTER TABLE workspaces DROP COLUMN setting_sync_dir;

View File

@@ -0,0 +1,11 @@
CREATE TABLE plugin_key_values
(
model TEXT DEFAULT 'plugin_key_value' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
plugin_name TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (plugin_name, key)
);

View File

@@ -0,0 +1,66 @@
CREATE TABLE websocket_requests
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'websocket_request' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
folder_id TEXT
REFERENCES folders
ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
authentication TEXT DEFAULT '{}' NOT NULL,
authentication_type TEXT,
description TEXT NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL,
headers TEXT NOT NULL,
message TEXT NOT NULL,
sort_priority REAL NOT NULL,
url_parameters TEXT DEFAULT '[]' NOT NULL
);
CREATE TABLE websocket_connections
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'websocket_connection' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
request_id TEXT NOT NULL
REFERENCES websocket_requests
ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
url TEXT NOT NULL,
state TEXT NOT NULL,
status INTEGER DEFAULT -1 NOT NULL,
error TEXT NULL,
elapsed INTEGER DEFAULT 0 NOT NULL,
headers TEXT DEFAULT '{}' NOT NULL
);
CREATE TABLE websocket_events
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'websocket_event' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
request_id TEXT NOT NULL
REFERENCES websocket_requests
ON DELETE CASCADE,
connection_id TEXT NOT NULL
REFERENCES websocket_connections
ON DELETE CASCADE,
created_at DATETIME DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
updated_at DATETIME DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
is_server BOOLEAN NOT NULL,
message_type TEXT NOT NULL,
message BLOB NOT NULL
);

View File

@@ -0,0 +1,2 @@
ALTER TABLE settings
ADD COLUMN hide_window_controls BOOLEAN DEFAULT FALSE NOT NULL;

View File

@@ -0,0 +1,43 @@
-- 1. Create the new table with `id` as the primary key
CREATE TABLE key_values_new
(
id TEXT PRIMARY KEY,
model TEXT DEFAULT 'key_value' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
namespace TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL
);
-- 2. Copy data from the old table
INSERT INTO key_values_new (id, model, created_at, updated_at, deleted_at, namespace, key, value)
SELECT (
-- This is the best way to generate a random string in SQLite, apparently
'kv_' || SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1) ||
SUBSTR('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23457789', (ABS(RANDOM()) % 57) + 1, 1)
) AS id,
model,
created_at,
updated_at,
deleted_at,
namespace,
key,
value
FROM key_values;
-- 3. Drop the old table
DROP TABLE key_values;
-- 4. Rename the new table
ALTER TABLE key_values_new
RENAME TO key_values;

View File

@@ -0,0 +1 @@
ALTER TABLE workspace_metas ADD COLUMN encryption_key TEXT NULL DEFAULT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE workspaces ADD COLUMN encryption_key_challenge TEXT NULL;

View File

@@ -0,0 +1,245 @@
-- NOTE: SQLite does not support dropping foreign keys, so we need to create new
-- tables and copy data instead. To prevent cascade deletes from wrecking stuff,
-- we start with the leaf tables and finish with the parent tables (eg. folder).
----------------------------
-- Remove http request FK --
----------------------------
CREATE TABLE http_requests_dg_tmp
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'http_request' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
name TEXT NOT NULL,
url TEXT NOT NULL,
method TEXT NOT NULL,
headers TEXT NOT NULL,
body_type TEXT,
sort_priority REAL DEFAULT 0 NOT NULL,
authentication TEXT DEFAULT '{}' NOT NULL,
authentication_type TEXT,
folder_id TEXT,
body TEXT DEFAULT '{}' NOT NULL,
url_parameters TEXT DEFAULT '[]' NOT NULL,
description TEXT DEFAULT '' NOT NULL
);
INSERT INTO http_requests_dg_tmp(id, model, workspace_id, created_at, updated_at, deleted_at, name, url, method,
headers, body_type, sort_priority, authentication, authentication_type, folder_id,
body, url_parameters, description)
SELECT id,
model,
workspace_id,
created_at,
updated_at,
deleted_at,
name,
url,
method,
headers,
body_type,
sort_priority,
authentication,
authentication_type,
folder_id,
body,
url_parameters,
description
FROM http_requests;
DROP TABLE http_requests;
ALTER TABLE http_requests_dg_tmp
RENAME TO http_requests;
----------------------------
-- Remove grpc request FK --
----------------------------
CREATE TABLE grpc_requests_dg_tmp
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'grpc_request' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
folder_id TEXT,
created_at DATETIME DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
updated_at DATETIME DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
name TEXT NOT NULL,
sort_priority REAL NOT NULL,
url TEXT NOT NULL,
service TEXT,
method TEXT,
message TEXT NOT NULL,
authentication TEXT DEFAULT '{}' NOT NULL,
authentication_type TEXT,
metadata TEXT DEFAULT '[]' NOT NULL,
description TEXT DEFAULT '' NOT NULL
);
INSERT INTO grpc_requests_dg_tmp(id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority, url,
service, method, message, authentication, authentication_type, metadata, description)
SELECT id,
model,
workspace_id,
folder_id,
created_at,
updated_at,
name,
sort_priority,
url,
service,
method,
message,
authentication,
authentication_type,
metadata,
description
FROM grpc_requests;
DROP TABLE grpc_requests;
ALTER TABLE grpc_requests_dg_tmp
RENAME TO grpc_requests;
---------------------------------
-- Remove websocket request FK --
---------------------------------
CREATE TABLE websocket_requests_dg_tmp
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'websocket_request' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
folder_id TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
authentication TEXT DEFAULT '{}' NOT NULL,
authentication_type TEXT,
description TEXT NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL,
headers TEXT NOT NULL,
message TEXT NOT NULL,
sort_priority REAL NOT NULL,
url_parameters TEXT DEFAULT '[]' NOT NULL
);
INSERT INTO websocket_requests_dg_tmp(id, model, workspace_id, folder_id, created_at, updated_at, deleted_at,
authentication, authentication_type, description, name, url, headers, message,
sort_priority, url_parameters)
SELECT id,
model,
workspace_id,
folder_id,
created_at,
updated_at,
deleted_at,
authentication,
authentication_type,
description,
name,
url,
headers,
message,
sort_priority,
url_parameters
FROM websocket_requests;
DROP TABLE websocket_requests;
ALTER TABLE websocket_requests_dg_tmp
RENAME TO websocket_requests;
PRAGMA foreign_keys = ON;
---------------------------
-- Remove environment FK --
---------------------------
CREATE TABLE environments_dg_tmp
(
id TEXT NOT NULL
PRIMARY KEY,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
name TEXT NOT NULL,
variables DEFAULT '[]' NOT NULL,
model TEXT DEFAULT 'environment',
environment_id TEXT
);
INSERT INTO environments_dg_tmp(id, created_at, updated_at, deleted_at, workspace_id, name, variables, model,
environment_id)
SELECT id,
created_at,
updated_at,
deleted_at,
workspace_id,
name,
variables,
model,
environment_id
FROM environments;
DROP TABLE environments;
ALTER TABLE environments_dg_tmp
RENAME TO environments;
----------------------
-- Remove folder FK --
----------------------
CREATE TABLE folders_dg_tmp
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'folder' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
folder_id TEXT,
name TEXT NOT NULL,
sort_priority REAL DEFAULT 0 NOT NULL,
description TEXT DEFAULT '' NOT NULL
);
INSERT INTO folders_dg_tmp(id, model, created_at, updated_at, deleted_at, workspace_id, folder_id, name, sort_priority,
description)
SELECT id,
model,
created_at,
updated_at,
deleted_at,
workspace_id,
folder_id,
name,
sort_priority,
description
FROM folders;
DROP TABLE folders;
ALTER TABLE folders_dg_tmp
RENAME TO folders;

View File

@@ -0,0 +1,11 @@
-- There used to be sync code that skipped over environments because we didn't
-- want to sync potentially insecure data. With encryption, it is now possible
-- to sync environments securely. However, there were already sync states in the
-- DB that marked environments as "Synced". Running the sync code on these envs
-- would mark them as deleted by FS (exist in SyncState but not on FS).
--
-- To undo this mess, we have this migration to delete all environment-related
-- sync states so we can sync from a clean slate.
DELETE
FROM sync_states
WHERE model_id LIKE 'ev_%';

View File

@@ -0,0 +1,20 @@
-- Add a public column to represent whether an environment can be shared or exported
ALTER TABLE environments
ADD COLUMN public BOOLEAN DEFAULT FALSE;
-- Add a base column to represent whether an environment is a base or sub environment. We used to
-- do this with environment_id, but we need a more flexible solution now that envs can be optionally
-- synced. E.g., it's now possible to only import a sub environment from a different client without
-- its base environment "parent."
ALTER TABLE environments
ADD COLUMN base BOOLEAN DEFAULT FALSE;
-- SQLite doesn't support dynamic default values, so we update `base` based on the value of
-- environment_id.
UPDATE environments
SET base = TRUE
WHERE environment_id IS NULL;
-- Finally, we drop the old `environment_id` column that will no longer be used
ALTER TABLE environments
DROP COLUMN environment_id;

View File

@@ -1,243 +0,0 @@
use std::fmt::Display;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tauri::{Manager, Runtime, WebviewWindow};
use ts_rs::TS;
use yaak_models::queries::{
generate_id, get_key_value_int, get_key_value_string, get_or_create_settings,
set_key_value_int, set_key_value_string, UpdateSource,
};
use crate::is_dev;
const NAMESPACE: &str = "analytics";
const NUM_LAUNCHES_KEY: &str = "num_launches";
// serializable
#[derive(Serialize, Deserialize, Debug, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "analytics.ts")]
pub enum AnalyticsResource {
App,
Appearance,
Button,
Checkbox,
CookieJar,
Dialog,
Environment,
Folder,
GrpcConnection,
GrpcEvent,
GrpcRequest,
HttpRequest,
HttpResponse,
KeyValue,
Link,
Mutation,
Plugin,
Select,
Setting,
Sidebar,
Tab,
Theme,
Workspace,
}
impl AnalyticsResource {
pub fn from_str(s: &str) -> serde_json::Result<AnalyticsResource> {
serde_json::from_str(format!("\"{s}\"").as_str())
}
}
impl Display for AnalyticsResource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", serde_json::to_string(self).unwrap().replace("\"", ""))
}
}
#[derive(Serialize, Deserialize, Debug, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "analytics.ts")]
pub enum AnalyticsAction {
Cancel,
Click,
Commit,
Create,
Delete,
DeleteMany,
Duplicate,
Error,
Export,
Hide,
Import,
Launch,
LaunchFirst,
LaunchUpdate,
Send,
Show,
Toggle,
Update,
Upsert,
}
impl AnalyticsAction {
pub fn from_str(s: &str) -> serde_json::Result<AnalyticsAction> {
serde_json::from_str(format!("\"{s}\"").as_str())
}
}
impl Display for AnalyticsAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", serde_json::to_string(self).unwrap().replace("\"", ""))
}
}
#[derive(Default, Debug)]
pub struct LaunchEventInfo {
pub current_version: String,
pub previous_version: String,
pub launched_after_update: bool,
pub num_launches: i32,
}
pub async fn track_launch_event<R: Runtime>(w: &WebviewWindow<R>) -> LaunchEventInfo {
let last_tracked_version_key = "last_tracked_version";
let mut info = LaunchEventInfo::default();
info.num_launches = get_num_launches(w).await + 1;
info.previous_version = get_key_value_string(w, NAMESPACE, last_tracked_version_key, "").await;
info.current_version = w.package_info().version.to_string();
if info.previous_version.is_empty() {
track_event(w, AnalyticsResource::App, AnalyticsAction::LaunchFirst, None).await;
} else {
info.launched_after_update = info.current_version != info.previous_version;
if info.launched_after_update {
track_event(
w,
AnalyticsResource::App,
AnalyticsAction::LaunchUpdate,
Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })),
)
.await;
}
};
// Track a launch event in all cases
track_event(
w,
AnalyticsResource::App,
AnalyticsAction::Launch,
Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })),
)
.await;
// Update key values
set_key_value_string(
w,
NAMESPACE,
last_tracked_version_key,
info.current_version.as_str(),
&UpdateSource::Background,
)
.await;
set_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, &UpdateSource::Background)
.await;
info
}
pub async fn track_event<R: Runtime>(
w: &WebviewWindow<R>,
resource: AnalyticsResource,
action: AnalyticsAction,
attributes: Option<Value>,
) {
let id = get_id(w).await;
let event = format!("{}.{}", resource, action);
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
let info = w.app_handle().package_info();
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
let site = match is_dev() {
true => "site_TkHWjoXwZPq3HfhERb",
false => "site_zOK0d7jeBy2TLxFCnZ",
};
let base_url = match is_dev() {
true => "http://localhost:7194",
false => "https://t.yaak.app",
};
let params = vec![
("u", id),
("e", event.clone()),
("a", attributes_json.clone()),
("id", site.to_string()),
("v", info.version.clone().to_string()),
("os", get_os().to_string()),
("tz", tz),
("xy", get_window_size(w)),
];
let req =
reqwest::Client::builder().build().unwrap().get(format!("{base_url}/t/e")).query(&params);
let settings = get_or_create_settings(w).await;
if !settings.telemetry {
info!("Track event (disabled): {}", event);
return;
}
// Disable analytics actual sending in dev
if is_dev() {
debug!("Track event: {} {}", event, attributes_json);
return;
}
if let Err(e) = req.send().await {
info!("Error sending analytics event: {}", e);
}
}
fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unknown"
}
}
fn get_window_size<R: Runtime>(w: &WebviewWindow<R>) -> String {
let current_monitor = match w.current_monitor() {
Ok(Some(m)) => m,
_ => return "unknown".to_string(),
};
let scale_factor = current_monitor.scale_factor();
let size = current_monitor.size();
let width: f64 = size.width as f64 / scale_factor;
let height: f64 = size.height as f64 / scale_factor;
format!("{}x{}", (width / 100.0).round() * 100.0, (height / 100.0).round() * 100.0)
}
async fn get_id<R: Runtime>(w: &WebviewWindow<R>) -> String {
let id = get_key_value_string(w, "analytics", "id", "").await;
if id.is_empty() {
let new_id = generate_id();
set_key_value_string(w, "analytics", "id", new_id.as_str(), &UpdateSource::Background)
.await;
new_id
} else {
id
}
}
pub async fn get_num_launches<R: Runtime>(w: &WebviewWindow<R>) -> i32 {
get_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, 0).await
}

40
src-tauri/src/commands.rs Normal file
View File

@@ -0,0 +1,40 @@
use crate::error::Result;
use tauri::{command, AppHandle, Manager, Runtime, WebviewWindow};
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
use yaak_crypto::manager::EncryptionManagerExt;
use yaak_plugins::events::PluginWindowContext;
use yaak_plugins::native_template_functions::{decrypt_secure_template_function, encrypt_secure_template_function};
#[command]
pub(crate) async fn cmd_show_workspace_key<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: &str,
) -> Result<()> {
let key = window.crypto().reveal_workspace_key(workspace_id)?;
window
.dialog()
.message(format!("Your workspace key is \n\n{}", key))
.kind(MessageDialogKind::Info)
.show(|_v| {});
Ok(())
}
#[command]
pub(crate) async fn cmd_decrypt_template<R: Runtime>(
window: WebviewWindow<R>,
template: &str,
) -> Result<String> {
let app_handle = window.app_handle();
let window_context = &PluginWindowContext::new(&window);
Ok(decrypt_secure_template_function(&app_handle, window_context, template)?)
}
#[command]
pub(crate) async fn cmd_secure_template<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
template: &str,
) -> Result<String> {
let window_context = &PluginWindowContext::new(&window);
Ok(encrypt_secure_template_function(&app_handle, window_context, template)?)
}

16
src-tauri/src/encoding.rs Normal file
View File

@@ -0,0 +1,16 @@
use encoding_rs::SHIFT_JIS;
use tokio::fs;
use yaak_models::models::HttpResponse;
pub async fn read_response_body<'a>(
response: HttpResponse,
) -> Option<String> {
let body_path = match response.body_path {
None => return None,
Some(p) => p,
};
let body = fs::read(body_path).await.unwrap();
let (s, _, _) = SHIFT_JIS.decode(body.as_slice());
Some(s.to_string())
}

62
src-tauri/src/error.rs Normal file
View File

@@ -0,0 +1,62 @@
use std::io;
use serde::{Serialize, Serializer};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
TemplateError(#[from] yaak_templates::error::Error),
#[error(transparent)]
ModelError(#[from] yaak_models::error::Error),
#[error(transparent)]
SyncError(#[from] yaak_sync::error::Error),
#[error(transparent)]
CryptoError(#[from] yaak_crypto::error::Error),
#[error(transparent)]
GitError(#[from] yaak_git::error::Error),
#[error(transparent)]
WebsocketError(#[from] yaak_ws::error::Error),
#[error(transparent)]
LicenseError(#[from] yaak_license::error::Error),
#[error(transparent)]
PluginError(#[from] yaak_plugins::error::Error),
#[error("Updater error: {0}")]
UpdaterError(#[from] tauri_plugin_updater::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::error::Error),
#[error("Tauri error: {0}")]
TauriError(#[from] tauri::Error),
#[error("Event source error: {0}")]
EventSourceError(#[from] eventsource_client::Error),
#[error("I/O error: {0}")]
IOError(#[from] io::Error),
#[error("Request error: {0}")]
RequestError(#[from] reqwest::Error),
#[error("Generic error: {0}")]
GenericError(String),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -1,10 +1,14 @@
use std::collections::BTreeMap;
use crate::error::Result;
use KeyAndValueRef::{Ascii, Binary};
use tauri::{Manager, Runtime, WebviewWindow};
use yaak_grpc::{KeyAndValueRef, MetadataMap};
use yaak_models::models::GrpcRequest;
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader};
use yaak_plugins::manager::PluginManager;
pub fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String> {
pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String> {
let mut entries = BTreeMap::new();
for r in metadata.iter() {
match r {
@@ -14,3 +18,48 @@ pub fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String> {
}
entries
}
pub(crate) async fn build_metadata<R: Runtime>(
window: &WebviewWindow<R>,
request: &GrpcRequest,
) -> Result<BTreeMap<String, String>> {
let plugin_manager = window.state::<PluginManager>();
let mut metadata = BTreeMap::new();
// Add the rest of metadata
for h in request.clone().metadata {
if h.name.is_empty() && h.value.is_empty() {
continue;
}
if !h.enabled {
continue;
}
metadata.insert(h.name, h.value);
}
if let Some(auth_name) = request.authentication_type.clone() {
let auth = request.authentication.clone();
let plugin_req = CallHttpAuthenticationRequest {
context_id: format!("{:x}", md5::compute(request.id.clone())),
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
method: "POST".to_string(),
url: request.url.clone(),
headers: metadata
.iter()
.map(|(name, value)| HttpHeader {
name: name.to_string(),
value: value.to_string(),
})
.collect(),
};
let plugin_result =
plugin_manager.call_http_authentication(&window, &auth_name, plugin_req).await?;
for header in plugin_result.set_headers {
metadata.insert(header.name, header.value);
}
}
Ok(metadata)
}

60
src-tauri/src/history.rs Normal file
View File

@@ -0,0 +1,60 @@
use tauri::{AppHandle, Runtime};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
const NAMESPACE: &str = "analytics";
const NUM_LAUNCHES_KEY: &str = "num_launches";
#[derive(Default, Debug)]
pub struct LaunchEventInfo {
pub current_version: String,
pub previous_version: String,
pub launched_after_update: bool,
pub num_launches: i32,
}
pub async fn store_launch_history<R: Runtime>(app_handle: &AppHandle<R>) -> LaunchEventInfo {
let last_tracked_version_key = "last_tracked_version";
let mut info = LaunchEventInfo::default();
info.num_launches = get_num_launches(app_handle).await + 1;
info.current_version = app_handle.package_info().version.to_string();
app_handle
.with_tx(|tx| {
info.previous_version =
tx.get_key_value_string(NAMESPACE, last_tracked_version_key, "");
if !info.previous_version.is_empty() {
info.launched_after_update = info.current_version != info.previous_version;
};
// Update key values
let source = &UpdateSource::Background;
let version = info.current_version.as_str();
tx.set_key_value_string(NAMESPACE, last_tracked_version_key, version, source);
tx.set_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, source);
Ok(())
})
.unwrap();
info
}
pub fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unknown"
}
}
pub async fn get_num_launches<R: Runtime>(app_handle: &AppHandle<R>) -> i32 {
app_handle.db().get_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, 0)
}

View File

@@ -1,15 +1,17 @@
use crate::error::Error::GenericError;
use crate::error::Result;
use crate::render::render_http_request;
use crate::response_err;
use crate::template_callback::PluginTemplateCallback;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue, Uri};
use http::{HeaderMap, HeaderName, HeaderValue};
use log::{debug, error, warn};
use mime_guess::Mime;
use reqwest::redirect::Policy;
use reqwest::{multipart, Proxy, Url};
use reqwest::{Method, Response};
use reqwest::{Proxy, Url, multipart};
use rustls::ClientConfig;
use rustls_platform_verifier::ConfigVerifierExt;
use rustls::crypto::ring;
use rustls_platform_verifier::BuilderVerifierExt;
use serde_json::Value;
use std::collections::BTreeMap;
use std::path::PathBuf;
@@ -18,22 +20,21 @@ use std::sync::Arc;
use std::time::Duration;
use tauri::{Manager, Runtime, WebviewWindow};
use tokio::fs;
use tokio::fs::{create_dir_all, File};
use tokio::fs::{File, create_dir_all};
use tokio::io::AsyncWriteExt;
use tokio::sync::watch::Receiver;
use tokio::sync::{oneshot, Mutex};
use tokio::sync::{Mutex, oneshot};
use yaak_models::models::{
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader,
HttpResponseState, ProxySetting, ProxySettingAuth,
};
use yaak_models::queries::{
get_base_environment, get_http_response, get_or_create_settings, get_workspace,
update_response_if_id, upsert_cookie_jar, UpdateSource,
};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
use yaak_plugins::events::{
CallHttpAuthenticationRequest, HttpHeader, RenderPurpose, WindowContext,
CallHttpAuthenticationRequest, HttpHeader, PluginWindowContext, RenderPurpose,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::template_callback::PluginTemplateCallback;
pub async fn send_http_request<R: Runtime>(
window: &WebviewWindow<R>,
@@ -42,27 +43,46 @@ pub async fn send_http_request<R: Runtime>(
environment: Option<Environment>,
cookie_jar: Option<CookieJar>,
cancelled_rx: &mut Receiver<bool>,
) -> Result<HttpResponse, String> {
let plugin_manager = window.state::<PluginManager>();
let workspace = get_workspace(window, &unrendered_request.workspace_id)
.await
.expect("Failed to get Workspace");
let base_environment = get_base_environment(window, &unrendered_request.workspace_id)
.await
.expect("Failed to get base environment");
let settings = get_or_create_settings(window).await;
let cb = PluginTemplateCallback::new(
window.app_handle(),
&WindowContext::from_window(window),
RenderPurpose::Send,
);
) -> Result<HttpResponse> {
let app_handle = window.app_handle().clone();
let plugin_manager = app_handle.state::<PluginManager>();
let (settings, workspace) = {
let db = window.db();
let settings = db.get_settings();
let workspace = db.get_workspace(&unrendered_request.workspace_id)?;
(settings, workspace)
};
let base_environment =
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
let response_id = og_response.id.clone();
let response = Arc::new(Mutex::new(og_response.clone()));
let request =
render_http_request(&unrendered_request, &base_environment, environment.as_ref(), &cb)
.await;
let cb = PluginTemplateCallback::new(
window.app_handle(),
&PluginWindowContext::new(window),
RenderPurpose::Send,
);
let update_source = UpdateSource::from_window(window);
let request = match render_http_request(
&unrendered_request,
&base_environment,
environment.as_ref(),
&cb,
)
.await
{
Ok(r) => r,
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
&update_source,
));
}
};
let mut url_string = request.url;
@@ -70,7 +90,7 @@ pub async fn send_http_request<R: Runtime>(
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string);
}
debug!("Sending request to {url_string}");
debug!("Sending request to {} {url_string}", request.method);
let mut client_builder = reqwest::Client::builder()
.redirect(match workspace.setting_follow_redirects {
@@ -86,11 +106,15 @@ pub async fn send_http_request<R: Runtime>(
if workspace.setting_validate_certificates {
// Use platform-native verifier to validate certificates
client_builder =
client_builder.use_preconfigured_tls(ClientConfig::with_platform_verifier())
let arc_crypto_provider = Arc::new(ring::default_provider());
let config = ClientConfig::builder_with_provider(arc_crypto_provider)
.with_safe_default_protocol_versions()
.unwrap()
.with_platform_verifier()
.with_no_client_auth();
client_builder = client_builder.use_preconfigured_tls(config)
} else {
// Use rustls to skip validation because rustls_platform_verifier does not have this
// ability
// Use rustls to skip validation because rustls_platform_verifier does not have this ability
client_builder = client_builder
.use_rustls_tls()
.danger_accept_invalid_hostnames(true)
@@ -99,7 +123,12 @@ pub async fn send_http_request<R: Runtime>(
match settings.proxy {
Some(ProxySetting::Disabled) => client_builder = client_builder.no_proxy(),
Some(ProxySetting::Enabled { http, https, auth }) => {
Some(ProxySetting::Enabled {
http,
https,
auth,
disabled,
}) if !disabled => {
debug!("Using proxy http={http} https={https}");
let mut proxy = Proxy::custom(move |url| {
let http = if http.is_empty() { None } else { Some(http.to_owned()) };
@@ -119,7 +148,7 @@ pub async fn send_http_request<R: Runtime>(
client_builder = client_builder.proxy(proxy);
}
None => {} // Nothing to do for this one, as it is the default
_ => {} // Nothing to do for this one, as it is the default
}
// Add cookie store if specified
@@ -134,10 +163,9 @@ pub async fn send_http_request<R: Runtime>(
serde_json::from_value(json_cookie).expect("Failed to deserialize cookie")
})
.map(|c| Ok(c))
.collect::<Vec<Result<_, ()>>>();
.collect::<Vec<Result<_>>>();
let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true)
.expect("Failed to create cookie store");
let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true)?;
let cookie_store = reqwest_cookie_store::CookieStoreMutex::new(store);
let cookie_store = Arc::new(cookie_store);
client_builder = client_builder.cookie_provider(Arc::clone(&cookie_store));
@@ -153,7 +181,7 @@ pub async fn send_http_request<R: Runtime>(
));
}
let client = client_builder.build().expect("Failed to build client");
let client = client_builder.build()?;
// Render query parameters
let mut query_params = Vec::new();
@@ -164,32 +192,20 @@ pub async fn send_http_request<R: Runtime>(
query_params.push((p.name, p.value));
}
let uri = match Uri::from_str(url_string.as_str()) {
let url = match Url::from_str(&url_string) {
Ok(u) => u,
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
window,
)
.await);
}
};
// Yes, we're parsing both URI and URL because they could return different errors
let url = match Url::from_str(uri.to_string().as_str()) {
Ok(u) => u,
Err(e) => {
return Ok(response_err(
&*response.lock().await,
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
window,
)
.await);
&update_source,
));
}
};
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
.expect("Failed to create method");
let m = Method::from_str(&request.method.to_uppercase())
.map_err(|e| GenericError(e.to_string()))?;
let mut request_builder = client.request(m, url).query(&query_params);
let mut headers = HeaderMap::new();
@@ -220,14 +236,14 @@ pub async fn send_http_request<R: Runtime>(
continue;
}
let header_name = match HeaderName::from_bytes(h.name.as_bytes()) {
let header_name = match HeaderName::from_str(&h.name) {
Ok(n) => n,
Err(e) => {
error!("Failed to create header name: {}", e);
continue;
}
};
let header_value = match HeaderValue::from_str(h.value.as_str()) {
let header_value = match HeaderValue::from_str(&h.value) {
Ok(n) => n,
Err(e) => {
error!("Failed to create header value: {}", e);
@@ -262,7 +278,7 @@ pub async fn send_http_request<R: Runtime>(
None => {}
Some(a) => {
for p in a {
let enabled = get_bool(p, "enabled");
let enabled = get_bool(p, "enabled", true);
let name = get_str(p, "name");
if !enabled || name.is_empty() {
continue;
@@ -277,7 +293,7 @@ pub async fn send_http_request<R: Runtime>(
} else if body_type == "binary" && request_body.contains_key("filePath") {
let file_path = request_body
.get("filePath")
.ok_or("filePath not set")?
.ok_or(GenericError("filePath not set".to_string()))?
.as_str()
.unwrap_or_default();
@@ -286,7 +302,12 @@ pub async fn send_http_request<R: Runtime>(
request_builder = request_builder.body(f);
}
Err(e) => {
return Ok(response_err(&*response.lock().await, e, window).await);
return Ok(response_err(
&app_handle,
&*response.lock().await,
e,
&update_source,
));
}
}
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
@@ -296,7 +317,7 @@ pub async fn send_http_request<R: Runtime>(
None => {}
Some(fd) => {
for p in fd {
let enabled = get_bool(p, "enabled");
let enabled = get_bool(p, "enabled", true);
let name = get_str(p, "name").to_string();
if !enabled || name.is_empty() {
@@ -313,11 +334,11 @@ pub async fn send_http_request<R: Runtime>(
Ok(f) => multipart::Part::bytes(f),
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
window,
)
.await);
&update_source,
));
}
}
};
@@ -326,17 +347,36 @@ pub async fn send_http_request<R: Runtime>(
// Set or guess mimetype
if !content_type.is_empty() {
part = part.mime_str(content_type).map_err(|e| e.to_string())?;
part = match part.mime_str(content_type) {
Ok(p) => p,
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
format!("Invalid mime for multi-part entry {e:?}"),
&update_source,
));
}
};
} else if !file_path.is_empty() {
let default_mime =
Mime::from_str("application/octet-stream").unwrap();
let mime =
mime_guess::from_path(file_path.clone()).first_or(default_mime);
part =
part.mime_str(mime.essence_str()).map_err(|e| e.to_string())?;
part = match part.mime_str(mime.essence_str()) {
Ok(p) => p,
Err(e) => {
return Ok(response_err(
&app_handle,
&*response.lock().await,
format!("Invalid mime for multi-part entry {e:?}"),
&update_source,
));
}
};
}
// Set file path if not empty
// Set file path if it is not empty
if !file_path.is_empty() {
let filename = PathBuf::from(file_path)
.file_name()
@@ -359,6 +399,15 @@ pub async fn send_http_request<R: Runtime>(
} else {
warn!("Unsupported body type: {}", body_type);
}
} else {
// No body set
let method = request.method.to_ascii_lowercase();
let is_body_method = method == "post" || method == "put" || method == "patch";
// Add Content-Length for methods that commonly accept a body because some servers
// will error if they don't receive it.
if is_body_method && !headers.contains_key("content-length") {
headers.insert("Content-Length", HeaderValue::from_static("0"));
}
}
// Add headers last, because previous steps may modify them
@@ -368,7 +417,12 @@ pub async fn send_http_request<R: Runtime>(
Ok(r) => r,
Err(e) => {
warn!("Failed to build request builder {e:?}");
return Ok(response_err(&*response.lock().await, e.to_string(), window).await);
return Ok(response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
&update_source,
));
}
};
@@ -376,13 +430,11 @@ pub async fn send_http_request<R: Runtime>(
if let Some(auth_name) = request.authentication_type.to_owned() {
let req = CallHttpAuthenticationRequest {
config: serde_json::to_value(&request.authentication)
.unwrap()
.as_object()
.unwrap()
.to_owned(),
method: sendable_req.method().to_string(),
context_id: format!("{:x}", md5::compute(request.id)),
values: serde_json::from_value(serde_json::to_value(&request.authentication).unwrap())
.unwrap(),
url: sendable_req.url().to_string(),
method: sendable_req.method().to_string(),
headers: sendable_req
.headers()
.iter()
@@ -392,11 +444,16 @@ pub async fn send_http_request<R: Runtime>(
})
.collect(),
};
let auth_result = plugin_manager.call_http_authentication(window, &auth_name, req).await;
let auth_result = plugin_manager.call_http_authentication(&window, &auth_name, req).await;
let plugin_result = match auth_result {
Ok(r) => r,
Err(e) => {
return Ok(response_err(&*response.lock().await, e.to_string(), window).await);
return Ok(response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
&update_source,
));
}
};
@@ -409,7 +466,7 @@ pub async fn send_http_request<R: Runtime>(
}
}
let (resp_tx, resp_rx) = oneshot::channel::<Result<Response, reqwest::Error>>();
let (resp_tx, resp_rx) = oneshot::channel::<std::result::Result<Response, reqwest::Error>>();
let (done_tx, done_rx) = oneshot::channel::<HttpResponse>();
let start = std::time::Instant::now();
@@ -421,22 +478,26 @@ pub async fn send_http_request<R: Runtime>(
let raw_response = tokio::select! {
Ok(r) = resp_rx => r,
_ = cancelled_rx.changed() => {
debug!("Request cancelled");
return Ok(response_err(&*response.lock().await, "Request was cancelled".to_string(), window).await);
let mut r = response.lock().await;
r.elapsed_headers = start.elapsed().as_millis() as i32;
r.elapsed = start.elapsed().as_millis() as i32;
return Ok(response_err(&app_handle, &r, "Request was cancelled".to_string(), &update_source));
}
};
{
let app_handle = app_handle.clone();
let window = window.clone();
let cancelled_rx = cancelled_rx.clone();
let response_id = response_id.clone();
let response = response.clone();
let update_source = update_source.clone();
tokio::spawn(async move {
match raw_response {
Ok(mut v) => {
let content_length = v.content_length();
let response_headers = v.headers().clone();
let dir = window.app_handle().path().app_data_dir().unwrap();
let dir = app_handle.path().app_data_dir().unwrap();
let base_dir = dir.join("responses");
create_dir_all(base_dir.clone()).await.expect("Failed to create responses dir");
let body_path = if response_id.is_empty() {
@@ -449,6 +510,7 @@ pub async fn send_http_request<R: Runtime>(
let mut r = response.lock().await;
r.body_path = Some(body_path.to_str().unwrap().to_string());
r.elapsed_headers = start.elapsed().as_millis() as i32;
r.elapsed = start.elapsed().as_millis() as i32;
r.status = v.status().as_u16() as i32;
r.status_reason = v.status().canonical_reason().map(|s| s.to_string());
r.headers = response_headers
@@ -470,8 +532,9 @@ pub async fn send_http_request<R: Runtime>(
};
r.state = HttpResponseState::Connected;
update_response_if_id(&window, &r, &UpdateSource::Window)
.await
app_handle
.db()
.update_http_response_if_id(&r, &update_source)
.expect("Failed to update response after connected");
}
@@ -499,21 +562,27 @@ pub async fn send_http_request<R: Runtime>(
f.flush().await.expect("Failed to flush file");
written_bytes += bytes.len();
r.content_length = Some(written_bytes as i32);
update_response_if_id(&window, &r, &UpdateSource::Window)
.await
app_handle
.db()
.update_http_response_if_id(&r, &update_source)
.expect("Failed to update response");
}
Ok(None) => {
break;
}
Err(e) => {
response_err(&*response.lock().await, e.to_string(), &window).await;
response_err(
&app_handle,
&*response.lock().await,
e.to_string(),
&update_source,
);
break;
}
}
}
// Set final content length
// Set the final content length
{
let mut r = response.lock().await;
r.content_length = match content_length {
@@ -521,8 +590,9 @@ pub async fn send_http_request<R: Runtime>(
None => Some(written_bytes as i32),
};
r.state = HttpResponseState::Closed;
update_response_if_id(&window, &r, &UpdateSource::Window)
.await
app_handle
.db()
.update_http_response_if_id(&r, &UpdateSource::from_window(&window))
.expect("Failed to update response");
};
@@ -547,8 +617,9 @@ pub async fn send_http_request<R: Runtime>(
})
.collect::<Vec<_>>();
cookie_jar.cookies = json_cookies;
if let Err(e) =
upsert_cookie_jar(&window, &cookie_jar, &UpdateSource::Window).await
if let Err(e) = app_handle
.db()
.upsert_cookie_jar(&cookie_jar, &UpdateSource::from_window(&window))
{
error!("Failed to update cookie jar: {}", e);
};
@@ -556,7 +627,12 @@ pub async fn send_http_request<R: Runtime>(
}
Err(e) => {
warn!("Failed to execute request {e}");
response_err(&*response.lock().await, format!("{e}{e:?}"), &window).await;
response_err(
&app_handle,
&*response.lock().await,
format!("{e}{e:?}"),
&update_source,
);
}
};
@@ -565,16 +641,20 @@ pub async fn send_http_request<R: Runtime>(
});
};
let app_handle = app_handle.clone();
Ok(tokio::select! {
Ok(r) = done_rx => r,
_ = cancelled_rx.changed() => {
match get_http_response(window, response_id.as_str()).await {
match app_handle.with_db(|c| c.get_http_response(&response_id)) {
Ok(mut r) => {
r.state = HttpResponseState::Closed;
update_response_if_id(&window, &r, &UpdateSource::Window).await.expect("Failed to update response")
r.elapsed = start.elapsed().as_millis() as i32;
r.elapsed_headers = start.elapsed().as_millis() as i32;
app_handle.db().update_http_response_if_id(&r, &UpdateSource::from_window(window))
.expect("Failed to update response")
},
_ => {
response_err(&*response.lock().await, "Ephemeral request was cancelled".to_string(), &window).await
response_err(&app_handle, &*response.lock().await, "Ephemeral request was cancelled".to_string(), &update_source)
}.clone(),
}
}
@@ -604,10 +684,10 @@ fn ensure_proto(url_str: &str) -> String {
format!("http://{url_str}")
}
fn get_bool(v: &Value, key: &str) -> bool {
fn get_bool(v: &Value, key: &str, fallback: bool) -> bool {
match v.get(key) {
None => false,
Some(v) => v.as_bool().unwrap_or_default(),
None => fallback,
Some(v) => v.as_bool().unwrap_or(fallback),
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,16 @@
use std::time::SystemTime;
use crate::analytics::get_num_launches;
use crate::error::Result;
use crate::history::{get_num_launches, get_os};
use chrono::{DateTime, Duration, Utc};
use log::debug;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use tauri::{Emitter, Manager, Runtime, WebviewWindow};
use yaak_models::queries::{get_key_value_raw, set_key_value_raw, UpdateSource};
use serde_json::Value;
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
use yaak_license::{LicenseCheckStatus, check_license};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
// Check for updates every hour
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60;
@@ -42,16 +46,23 @@ impl YaakNotifier {
}
}
pub async fn seen<R: Runtime>(&mut self, w: &WebviewWindow<R>, id: &str) -> Result<(), String> {
let mut seen = get_kv(w).await?;
pub async fn seen<R: Runtime>(&mut self, window: &WebviewWindow<R>, id: &str) -> Result<()> {
let app_handle = window.app_handle();
let mut seen = get_kv(app_handle).await?;
seen.push(id.to_string());
debug!("Marked notification as seen {}", id);
let seen_json = serde_json::to_string(&seen).map_err(|e| e.to_string())?;
set_key_value_raw(w, KV_NAMESPACE, KV_KEY, seen_json.as_str(), &UpdateSource::Window).await;
let seen_json = serde_json::to_string(&seen)?;
window.db().set_key_value_raw(
KV_NAMESPACE,
KV_KEY,
seen_json.as_str(),
&UpdateSource::from_window(window),
);
Ok(())
}
pub async fn check<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<(), String> {
pub async fn check<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<()> {
let app_handle = window.app_handle();
let ignore_check = self.last_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
if ignore_check {
@@ -60,39 +71,62 @@ impl YaakNotifier {
self.last_check = SystemTime::now();
let num_launches = get_num_launches(window).await;
let info = window.app_handle().package_info().clone();
let license_check = match check_license(window).await? {
LicenseCheckStatus::PersonalUse { .. } => "personal".to_string(),
LicenseCheckStatus::CommercialUse => "commercial".to_string(),
LicenseCheckStatus::InvalidLicense => "invalid_license".to_string(),
LicenseCheckStatus::Trialing { .. } => "trialing".to_string(),
};
let settings = window.db().get_settings();
let num_launches = get_num_launches(app_handle).await;
let info = app_handle.package_info().clone();
let req = reqwest::Client::default()
.request(Method::GET, "https://notify.yaak.app/notifications")
.query(&[
("version", info.version.to_string()),
("launches", num_launches.to_string()),
("version", info.version.to_string().as_str()),
("launches", num_launches.to_string().as_str()),
("installed", settings.created_at.format("%Y-%m-%d").to_string().as_str()),
("license", &license_check),
("platform", get_os()),
]);
let resp = req.send().await.map_err(|e| e.to_string())?;
let resp = req.send().await?;
if resp.status() != 200 {
debug!("Skipping notification status code {}", resp.status());
return Ok(());
}
let notification = resp.json::<YaakNotification>().await.map_err(|e| e.to_string())?;
let result = resp.json::<Value>().await?;
let age = notification.timestamp.signed_duration_since(Utc::now());
let seen = get_kv(window).await?;
if seen.contains(&notification.id) || (age > Duration::days(2)) {
debug!("Already seen notification {}", notification.id);
return Ok(());
// Support both single and multiple notifications.
// TODO: Remove support for single after April 2025
let notifications = match result {
Value::Array(a) => a
.into_iter()
.map(|a| serde_json::from_value(a).unwrap())
.collect::<Vec<YaakNotification>>(),
a @ _ => vec![serde_json::from_value(a).unwrap()],
};
for notification in notifications {
let age = notification.timestamp.signed_duration_since(Utc::now());
let seen = get_kv(app_handle).await?;
if seen.contains(&notification.id) || (age > Duration::days(2)) {
debug!("Already seen notification {}", notification.id);
continue;
}
debug!("Got notification {:?}", notification);
let _ = app_handle.emit_to(window.label(), "notification", notification.clone());
break; // Only show one notification
}
debug!("Got notification {:?}", notification);
let _ = window.emit_to(window.label(), "notification", notification.clone());
Ok(())
}
}
async fn get_kv<R: Runtime>(w: &WebviewWindow<R>) -> Result<Vec<String>, String> {
match get_key_value_raw(w, "notifications", "seen").await {
async fn get_kv<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Vec<String>> {
match app_handle.db().get_key_value_raw("notifications", "seen") {
None => Ok(Vec::new()),
Some(v) => serde_json::from_str(&v.value).map_err(|e| e.to_string()),
Some(v) => Ok(serde_json::from_str(&v.value)?),
}
}

View File

@@ -0,0 +1,281 @@
use crate::http_request::send_http_request;
use crate::render::{render_http_request, render_json_value};
use crate::window::{CreateWindowConfig, create_window};
use crate::{
call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_window_context,
workspace_from_window,
};
use chrono::Utc;
use log::warn;
use tauri::{AppHandle, Emitter, Manager, Runtime, State};
use tauri_plugin_clipboard_manager::ClipboardExt;
use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
use yaak_plugins::events::{
Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse,
GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload,
PluginWindowContext, RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse,
ShowToastRequest, TemplateRenderResponse, WindowNavigateEvent,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle;
use yaak_plugins::template_callback::PluginTemplateCallback;
pub(crate) async fn handle_plugin_event<R: Runtime>(
app_handle: &AppHandle<R>,
event: &InternalEvent,
plugin_handle: &PluginHandle,
) {
// debug!("Got event to app {event:?}");
let window_context = event.window_context.to_owned();
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
app_handle
.clipboard()
.write_text(req.text.as_str())
.expect("Failed to write text to clipboard");
Some(InternalEventPayload::CopyTextResponse(EmptyPayload {}))
}
InternalEventPayload::ShowToastRequest(req) => {
match window_context {
PluginWindowContext::Label { label, .. } => app_handle
.emit_to(label, "show_toast", req)
.expect("Failed to emit show_toast to window"),
_ => app_handle.emit("show_toast", req).expect("Failed to emit show_toast"),
};
Some(InternalEventPayload::ShowToastResponse(EmptyPayload {}))
}
InternalEventPayload::PromptTextRequest(_) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
call_frontend(window, event).await
}
InternalEventPayload::FindHttpResponsesRequest(req) => {
let http_responses = app_handle
.db()
.list_http_responses_for_request(&req.request_id, req.limit.map(|l| l as u64))
.unwrap_or_default();
Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
}))
}
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
let http_request = app_handle.db().get_http_request(&req.id).ok();
Some(InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse {
http_request,
}))
}
InternalEventPayload::RenderHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render http request");
let workspace =
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window);
let base_environment = app_handle
.db()
.get_base_environment(&workspace.id)
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let http_request = render_http_request(
&req.http_request,
&base_environment,
environment.as_ref(),
&cb,
)
.await
.expect("Failed to render http request");
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request,
}))
}
InternalEventPayload::TemplateRenderRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let workspace =
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window);
let base_environment = app_handle
.db()
.get_base_environment(&workspace.id)
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let data = render_json_value(req.data, &base_environment, environment.as_ref(), &cb)
.await
.expect("Failed to render template");
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
}
InternalEventPayload::ErrorResponse(resp) => {
let toast_event = plugin_handle.build_event_to_send(
&window_context,
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!(
"Plugin error from {}: {}",
plugin_handle.name().await,
resp.error
),
color: Some(Color::Danger),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::ReloadResponse(_) => {
let plugins = app_handle.db().list_plugins().unwrap();
for plugin in plugins {
if plugin.directory != plugin_handle.dir {
continue;
}
let new_plugin = Plugin {
updated_at: Utc::now().naive_utc(), // TODO: Add reloaded_at field to use instead
..plugin
};
app_handle.db().upsert_plugin(&new_plugin, &UpdateSource::Plugin).unwrap();
}
let toast_event = plugin_handle.build_event_to_send(
&window_context,
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!("Reloaded plugin {}", plugin_handle.dir),
icon: Some(Icon::Info),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::SendHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for sending HTTP request");
let mut http_request = req.http_request;
let workspace =
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
let cookie_jar = cookie_jar_from_window(&window);
let environment = environment_from_window(&window);
if http_request.workspace_id.is_empty() {
http_request.workspace_id = workspace.id;
}
let http_response = if http_request.id.is_empty() {
HttpResponse::default()
} else {
window
.db()
.upsert_http_response(
&HttpResponse {
request_id: http_request.id.clone(),
workspace_id: http_request.workspace_id.clone(),
..Default::default()
},
&UpdateSource::Plugin,
)
.unwrap()
};
let result = send_http_request(
&window,
&http_request,
&http_response,
environment,
cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
)
.await;
let http_response = match result {
Ok(r) => r,
Err(_e) => return,
};
Some(InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse {
http_response,
}))
}
InternalEventPayload::OpenWindowRequest(req) => {
let label = req.label;
let (navigation_tx, mut navigation_rx) = tokio::sync::mpsc::channel(128);
let (close_tx, mut close_rx) = tokio::sync::mpsc::channel(128);
let win_config = CreateWindowConfig {
url: &req.url,
label: &label.clone(),
title: &req.title.unwrap_or_default(),
navigation_tx: Some(navigation_tx),
close_tx: Some(close_tx),
inner_size: req.size.map(|s| (s.width, s.height)),
data_dir_key: req.data_dir_key,
..Default::default()
};
create_window(app_handle, win_config);
{
let event_id = event.id.clone();
let plugin_handle = plugin_handle.clone();
let window_context = window_context.clone();
tauri::async_runtime::spawn(async move {
while let Some(url) = navigation_rx.recv().await {
let url = url.to_string();
let event_to_send = plugin_handle.build_event_to_send(
&window_context, // NOTE: Sending existing context on purpose here
&InternalEventPayload::WindowNavigateEvent(WindowNavigateEvent { url }),
Some(event_id.clone()),
);
plugin_handle.send(&event_to_send).await.unwrap();
}
});
}
{
let event_id = event.id.clone();
let plugin_handle = plugin_handle.clone();
let window_context = window_context.clone();
tauri::async_runtime::spawn(async move {
while let Some(_) = close_rx.recv().await {
let event_to_send = plugin_handle.build_event_to_send(
&window_context,
&InternalEventPayload::WindowCloseEvent,
Some(event_id.clone()),
);
plugin_handle.send(&event_to_send).await.unwrap();
}
});
}
None
}
InternalEventPayload::CloseWindowRequest(req) => {
if let Some(window) = app_handle.webview_windows().get(&req.label) {
window.close().expect("Failed to close window");
}
None
}
InternalEventPayload::SetKeyValueRequest(req) => {
let name = plugin_handle.name().await;
app_handle.db().set_plugin_key_value(&name, &req.key, &req.value);
Some(InternalEventPayload::SetKeyValueResponse(SetKeyValueResponse {}))
}
InternalEventPayload::GetKeyValueRequest(req) => {
let name = plugin_handle.name().await;
let value = app_handle.db().get_plugin_key_value(&name, &req.key).map(|v| v.value);
Some(InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value }))
}
InternalEventPayload::DeleteKeyValueRequest(req) => {
let name = plugin_handle.name().await;
let deleted = app_handle.db().delete_plugin_key_value(&name, &req.key).unwrap();
Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted }))
}
_ => None,
};
if let Some(e) = response_event {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
if let Err(e) = plugin_manager.reply(&event, &e).await {
warn!("Failed to reply to plugin manager: {:?}", e)
}
}
}

View File

@@ -1,18 +1,18 @@
use crate::template_callback::PluginTemplateCallback;
use serde_json::{json, Map, Value};
use serde_json::Value;
use std::collections::{BTreeMap, HashMap};
use yaak_http::apply_path_placeholders;
use yaak_models::models::{
Environment, EnvironmentVariable, GrpcMetadataEntry, GrpcRequest, HttpRequest,
HttpRequestHeader, HttpUrlParameter,
Environment, GrpcMetadataEntry, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter,
};
use yaak_templates::{parse_and_render, TemplateCallback};
use yaak_models::render::make_vars_hashmap;
use yaak_templates::{parse_and_render, render_json_value_raw, TemplateCallback};
pub async fn render_template<T: TemplateCallback>(
template: &str,
base_environment: &Environment,
environment: Option<&Environment>,
cb: &T,
) -> String {
) -> yaak_templates::error::Result<String> {
let vars = &make_vars_hashmap(base_environment, environment);
render(template, vars, cb).await
}
@@ -22,7 +22,7 @@ pub async fn render_json_value<T: TemplateCallback>(
base_environment: &Environment,
environment: Option<&Environment>,
cb: &T,
) -> Value {
) -> yaak_templates::error::Result<Value> {
let vars = &make_vars_hashmap(base_environment, environment);
render_json_value_raw(value, vars, cb).await
}
@@ -32,48 +32,48 @@ pub async fn render_grpc_request<T: TemplateCallback>(
base_environment: &Environment,
environment: Option<&Environment>,
cb: &T,
) -> GrpcRequest {
) -> yaak_templates::error::Result<GrpcRequest> {
let vars = &make_vars_hashmap(base_environment, environment);
let mut metadata = Vec::new();
for p in r.metadata.clone() {
metadata.push(GrpcMetadataEntry {
enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await,
value: render(p.value.as_str(), vars, cb).await,
name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await?,
id: p.id,
})
}
let mut authentication = BTreeMap::new();
for (k, v) in r.authentication.clone() {
authentication.insert(k, render_json_value_raw(v, vars, cb).await);
authentication.insert(k, render_json_value_raw(v, vars, cb).await?);
}
let url = render(r.url.as_str(), vars, cb).await;
let url = render(r.url.as_str(), vars, cb).await?;
GrpcRequest {
Ok(GrpcRequest {
url,
metadata,
authentication,
..r.to_owned()
}
})
}
pub async fn render_http_request(
pub async fn render_http_request<T: TemplateCallback>(
r: &HttpRequest,
base_environment: &Environment,
environment: Option<&Environment>,
cb: &PluginTemplateCallback,
) -> HttpRequest {
cb: &T,
) -> yaak_templates::error::Result<HttpRequest> {
let vars = &make_vars_hashmap(base_environment, environment);
let mut url_parameters = Vec::new();
for p in r.url_parameters.clone() {
url_parameters.push(HttpUrlParameter {
enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await,
value: render(p.value.as_str(), vars, cb).await,
name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await?,
id: p.id,
})
}
@@ -82,351 +82,41 @@ pub async fn render_http_request(
for p in r.headers.clone() {
headers.push(HttpRequestHeader {
enabled: p.enabled,
name: render(p.name.as_str(), vars, cb).await,
value: render(p.value.as_str(), vars, cb).await,
name: render(p.name.as_str(), vars, cb).await?,
value: render(p.value.as_str(), vars, cb).await?,
id: p.id,
})
}
let mut body = BTreeMap::new();
for (k, v) in r.body.clone() {
body.insert(k, render_json_value_raw(v, vars, cb).await);
body.insert(k, render_json_value_raw(v, vars, cb).await?);
}
let mut authentication = BTreeMap::new();
for (k, v) in r.authentication.clone() {
authentication.insert(k, render_json_value_raw(v, vars, cb).await);
authentication.insert(k, render_json_value_raw(v, vars, cb).await?);
}
let url = render(r.url.clone().as_str(), vars, cb).await;
let req = HttpRequest {
let url = render(r.url.clone().as_str(), vars, cb).await?;
// This doesn't fit perfectly with the concept of "rendering" but it kind of does
let (url, url_parameters) = apply_path_placeholders(&url, url_parameters);
Ok(HttpRequest {
url,
url_parameters,
headers,
body,
authentication,
..r.to_owned()
};
// This doesn't fit perfectly with the concept of "rendering" but it kind of does
apply_path_placeholders(req)
}
pub fn make_vars_hashmap(
base_environment: &Environment,
environment: Option<&Environment>,
) -> HashMap<String, String> {
let mut variables = HashMap::new();
variables = add_variable_to_map(variables, &base_environment.variables);
if let Some(e) = environment {
variables = add_variable_to_map(variables, &e.variables);
}
variables
})
}
pub async fn render<T: TemplateCallback>(
template: &str,
vars: &HashMap<String, String>,
cb: &T,
) -> String {
) -> yaak_templates::error::Result<String> {
parse_and_render(template, vars, cb).await
}
fn add_variable_to_map(
m: HashMap<String, String>,
variables: &Vec<EnvironmentVariable>,
) -> HashMap<String, String> {
let mut map = m.clone();
for variable in variables {
if !variable.enabled || variable.value.is_empty() {
continue;
}
let name = variable.name.as_str();
let value = variable.value.as_str();
map.insert(name.into(), value.into());
}
map
}
async fn render_json_value_raw<T: TemplateCallback>(
v: Value,
vars: &HashMap<String, String>,
cb: &T,
) -> Value {
match v {
Value::String(s) => json!(render(s.as_str(), vars, cb).await),
Value::Array(a) => {
let mut new_a = Vec::new();
for v in a {
new_a.push(Box::pin(render_json_value_raw(v, vars, cb)).await)
}
json!(new_a)
}
Value::Object(o) => {
let mut new_o = Map::new();
for (k, v) in o {
let key = Box::pin(render(k.as_str(), vars, cb)).await;
let value = Box::pin(render_json_value_raw(v, vars, cb)).await;
new_o.insert(key, value);
}
json!(new_o)
}
v => v,
}
}
#[cfg(test)]
mod render_tests {
use serde_json::json;
use std::collections::HashMap;
use yaak_templates::TemplateCallback;
struct EmptyCB {}
impl TemplateCallback for EmptyCB {
async fn run(
&self,
_fn_name: &str,
_args: HashMap<String, String>,
) -> Result<String, String> {
todo!()
}
}
#[tokio::test]
async fn render_json_value_string() {
let v = json!("${[a]}");
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = super::render_json_value_raw(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!("aaa"))
}
#[tokio::test]
async fn render_json_value_array() {
let v = json!(["${[a]}", "${[a]}"]);
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = super::render_json_value_raw(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!(["aaa", "aaa"]))
}
#[tokio::test]
async fn render_json_value_object() {
let v = json!({"${[a]}": "${[a]}"});
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = super::render_json_value_raw(v, &vars, &EmptyCB {}).await;
assert_eq!(result, json!({"aaa": "aaa"}))
}
#[tokio::test]
async fn render_json_value_nested() {
let v = json!([
123,
{"${[a]}": "${[a]}"},
null,
"${[a]}",
false,
{"x": ["${[a]}"]}
]);
let mut vars = HashMap::new();
vars.insert("a".to_string(), "aaa".to_string());
let result = super::render_json_value_raw(v, &vars, &EmptyCB {}).await;
assert_eq!(
result,
json!([
123,
{"aaa": "aaa"},
null,
"aaa",
false,
{"x": ["aaa"]}
])
)
}
}
fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String {
if !p.enabled {
return url.to_string();
}
if !p.name.starts_with(":") {
return url.to_string();
}
let re = regex::Regex::new(format!("(/){}([/?#]|$)", p.name).as_str()).unwrap();
let result = re
.replace_all(url, |cap: &regex::Captures| {
format!(
"{}{}{}",
cap[1].to_string(),
urlencoding::encode(p.value.as_str()),
cap[2].to_string()
)
})
.into_owned();
result
}
fn apply_path_placeholders(rendered_request: HttpRequest) -> HttpRequest {
let mut url = rendered_request.url.to_owned();
let mut url_parameters = Vec::new();
for p in rendered_request.url_parameters.clone() {
if !p.enabled || p.name.is_empty() {
continue;
}
// Replace path parameters with values from URL parameters
let old_url_string = url.clone();
url = replace_path_placeholder(&p, url.as_str());
// Remove as param if it modified the URL
if old_url_string == url {
url_parameters.push(p);
}
}
let mut request = rendered_request.clone();
request.url_parameters = url_parameters;
request.url = url;
request
}
#[cfg(test)]
mod placeholder_tests {
use crate::render::{apply_path_placeholders, replace_path_placeholder};
use yaak_models::models::{HttpRequest, HttpUrlParameter};
#[test]
fn placeholder_middle() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo/bar"),
"https://example.com/xxx/bar",
);
}
#[test]
fn placeholder_end() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
"https://example.com/xxx",
);
}
#[test]
fn placeholder_query() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo?:foo"),
"https://example.com/xxx?:foo",
);
}
#[test]
fn placeholder_missing() {
let p = HttpUrlParameter {
enabled: true,
name: "".to_string(),
value: "".to_string(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:missing"),
"https://example.com/:missing",
);
}
#[test]
fn placeholder_disabled() {
let p = HttpUrlParameter {
enabled: false,
name: ":foo".to_string(),
value: "xxx".to_string(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
"https://example.com/:foo",
);
}
#[test]
fn placeholder_prefix() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foooo"),
"https://example.com/:foooo",
);
}
#[test]
fn placeholder_encode() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "Hello World".into(),
enabled: true,
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
"https://example.com/Hello%20World",
);
}
#[test]
fn apply_placeholder() {
let result = apply_path_placeholders(HttpRequest {
url: "example.com/:a/bar".to_string(),
url_parameters: vec![
HttpUrlParameter {
name: "b".to_string(),
value: "bbb".to_string(),
enabled: true,
id: None,
},
HttpUrlParameter {
name: ":a".to_string(),
value: "aaa".to_string(),
enabled: true,
id: None,
},
],
..Default::default()
});
assert_eq!(result.url, "example.com/aaa/bar");
assert_eq!(result.url_parameters.len(), 1);
assert_eq!(result.url_parameters[0].name, "b");
assert_eq!(result.url_parameters[0].value, "bbb");
}
}

View File

@@ -1,75 +0,0 @@
use std::collections::HashMap;
use tauri::{AppHandle, Manager, Runtime};
use yaak_plugins::events::{FormInput, RenderPurpose, WindowContext};
use yaak_plugins::manager::PluginManager;
use yaak_templates::TemplateCallback;
#[derive(Clone)]
pub struct PluginTemplateCallback {
plugin_manager: PluginManager,
window_context: WindowContext,
render_purpose: RenderPurpose,
}
impl PluginTemplateCallback {
pub fn new<R: Runtime>(
app_handle: &AppHandle<R>,
window_context: &WindowContext,
render_purpose: RenderPurpose,
) -> PluginTemplateCallback {
let plugin_manager = &*app_handle.state::<PluginManager>();
PluginTemplateCallback {
plugin_manager: plugin_manager.to_owned(),
window_context: window_context.to_owned(),
render_purpose,
}
}
}
impl TemplateCallback for PluginTemplateCallback {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, String> {
let window_context = self.window_context.to_owned();
// The beta named the function `Response` but was changed in stable.
// Keep this here for a while because there's no easy way to migrate
let fn_name = if fn_name == "Response" { "response" } else { fn_name };
let function = self
.plugin_manager
.get_template_functions_with_context(window_context.to_owned())
.await
.map_err(|e| e.to_string())?
.iter()
.flat_map(|f| f.functions.clone())
.find(|f| f.name == fn_name)
.ok_or("")?;
let mut args_with_defaults = args.clone();
// Fill in default values for all args
for arg in function.args {
let base = match arg {
FormInput::Text(a) => a.base,
FormInput::Editor(a) => a.base,
FormInput::Select(a) => a.base,
FormInput::Checkbox(a) => a.base,
FormInput::File(a) => a.base,
FormInput::HttpRequest(a) => a.base,
};
if let None = args_with_defaults.get(base.name.as_str()) {
args_with_defaults.insert(base.name, base.default_value.unwrap_or_default());
}
}
let resp = self
.plugin_manager
.call_template_function(
window_context,
fn_name,
args_with_defaults,
self.render_purpose.to_owned(),
)
.await
.map_err(|e| e.to_string())?;
Ok(resp.unwrap_or_default())
}
}

View File

@@ -1,11 +1,13 @@
use std::fmt::{Display, Formatter};
use std::time::SystemTime;
use crate::error::Result;
use log::info;
use tauri::{AppHandle, Manager};
use tauri::{Manager, Runtime, WebviewWindow};
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
use tauri_plugin_updater::UpdaterExt;
use tokio::task::block_in_place;
use yaak_models::query_manager::QueryManagerExt;
use yaak_plugins::manager::PluginManager;
use crate::is_dev;
@@ -46,6 +48,11 @@ impl UpdateMode {
}
}
pub enum UpdateTrigger {
Background,
User,
}
impl YaakUpdater {
pub fn new() -> Self {
Self {
@@ -53,41 +60,52 @@ impl YaakUpdater {
}
}
pub async fn force_check(
pub async fn check_now<R: Runtime>(
&mut self,
app_handle: &AppHandle,
window: &WebviewWindow<R>,
mode: UpdateMode,
) -> Result<bool, tauri_plugin_updater::Error> {
update_trigger: UpdateTrigger,
) -> Result<bool> {
let settings = window.db().get_settings();
let update_key = format!("{:x}", md5::compute(settings.id));
self.last_update_check = SystemTime::now();
info!("Checking for updates mode={}", mode);
let h = app_handle.clone();
let update_check_result = app_handle
let w = window.clone();
let update_check_result = w
.updater_builder()
.on_before_exit(move || {
// Kill plugin manager before exit or NSIS installer will fail to replace sidecar
// while it's running.
// NOTE: This is only called on Windows
let h = h.clone();
let w = w.clone();
block_in_place(|| {
tauri::async_runtime::block_on(async move {
info!("Shutting down plugin manager before update");
let plugin_manager = h.state::<PluginManager>();
let plugin_manager = w.state::<PluginManager>();
plugin_manager.terminate().await;
});
});
})
.header("X-Update-Mode", mode.to_string())?
.header("X-Update-Key", update_key)?
.header(
"X-Update-Trigger",
match update_trigger {
UpdateTrigger::Background => "background",
UpdateTrigger::User => "user",
},
)?
.build()?
.check()
.await;
match update_check_result {
Ok(Some(update)) => {
let h = app_handle.clone();
app_handle
.dialog()
let result = match update_check_result? {
None => false,
Some(update) => {
let w = window.clone();
w.dialog()
.message(format!(
"{} is available. Would you like to download and install it now?",
update.version
@@ -104,7 +122,7 @@ impl YaakUpdater {
tauri::async_runtime::spawn(async move {
match update.download_and_install(|_, _| {}, || {}).await {
Ok(_) => {
if h.dialog()
if w.dialog()
.message("Would you like to restart the app?")
.title("Update Installed")
.buttons(MessageDialogButtons::OkCancelCustom(
@@ -113,27 +131,27 @@ impl YaakUpdater {
))
.blocking_show()
{
h.restart();
w.app_handle().restart();
}
}
Err(e) => {
h.dialog()
w.dialog()
.message(format!("The update failed to install: {}", e));
}
}
});
});
Ok(true)
true
}
Ok(None) => Ok(false),
Err(e) => Err(e),
}
};
Ok(result)
}
pub async fn check(
pub async fn maybe_check<R: Runtime>(
&mut self,
app_handle: &AppHandle,
window: &WebviewWindow<R>,
mode: UpdateMode,
) -> Result<bool, tauri_plugin_updater::Error> {
) -> Result<bool> {
let update_period_seconds = match mode {
UpdateMode::Stable => MAX_UPDATE_CHECK_HOURS_STABLE,
UpdateMode::Beta => MAX_UPDATE_CHECK_HOURS_BETA,
@@ -150,6 +168,6 @@ impl YaakUpdater {
return Ok(false);
}
self.force_check(app_handle, mode).await
self.check_now(window, mode, UpdateTrigger::Background).await
}
}

View File

@@ -0,0 +1,25 @@
use log::{info, warn};
use tauri::{Manager, Runtime, UriSchemeContext};
pub(crate) fn handle_uri_scheme<R: Runtime>(
a: UriSchemeContext<R>,
req: http::Request<Vec<u8>>,
) -> http::Response<Vec<u8>> {
println!("------------- Yaak URI scheme invoked!");
let uri = req.uri();
let window = a
.app_handle()
.get_webview_window(a.webview_label())
.expect("Failed to get webview window for URI scheme event");
info!("Yaak URI scheme invoked with {uri:?} {window:?}");
let path = uri.path();
if path == "/data/import" {
warn!("TODO: import data")
} else if path == "/plugins/install" {
warn!("TODO: install plugin")
}
let msg = format!("No handler found for {path}");
tauri::http::Response::builder().status(404).body(msg.as_bytes().to_vec()).unwrap()
}

264
src-tauri/src/window.rs Normal file
View File

@@ -0,0 +1,264 @@
use crate::window_menu::app_menu;
use log::{info, warn};
use rand::random;
use std::process::exit;
use tauri::{
AppHandle, Emitter, LogicalSize, Manager, Runtime, WebviewUrl, WebviewWindow, WindowEvent,
};
use tauri_plugin_opener::OpenerExt;
use tokio::sync::mpsc;
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
const MIN_WINDOW_WIDTH: f64 = 300.0;
const MIN_WINDOW_HEIGHT: f64 = 300.0;
pub(crate) const MAIN_WINDOW_PREFIX: &str = "main_";
const OTHER_WINDOW_PREFIX: &str = "other_";
#[derive(Default, Debug)]
pub(crate) struct CreateWindowConfig<'s> {
pub url: &'s str,
pub label: &'s str,
pub title: &'s str,
pub inner_size: Option<(f64, f64)>,
pub position: Option<(f64, f64)>,
pub navigation_tx: Option<mpsc::Sender<String>>,
pub close_tx: Option<mpsc::Sender<()>>,
pub data_dir_key: Option<String>,
pub hide_titlebar: bool,
}
pub(crate) fn create_window<R: Runtime>(
handle: &AppHandle<R>,
config: CreateWindowConfig,
) -> WebviewWindow<R> {
#[allow(unused_variables)]
let menu = app_menu(handle).unwrap();
// This causes the window to not be clickable (in AppImage), so disable on Linux
#[cfg(not(target_os = "linux"))]
handle.set_menu(menu).expect("Failed to set app menu");
info!("Create new window label={}", config.label);
let mut win_builder =
tauri::WebviewWindowBuilder::new(handle, config.label, WebviewUrl::App(config.url.into()))
.title(config.title)
.resizable(true)
.visible(false) // To prevent theme flashing, the frontend code calls show() immediately after configuring the theme
.fullscreen(false)
.disable_drag_drop_handler() // Required for frontend Dnd on windows
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
if let Some(key) = config.data_dir_key {
#[cfg(not(target_os = "macos"))]
{
use std::fs;
let dir = handle.path().temp_dir().unwrap().join("yaak_sessions").join(key);
fs::create_dir_all(dir.clone()).unwrap();
win_builder = win_builder.data_directory(dir);
}
// macOS doesn't support data dir so must use this fn instead
#[cfg(target_os = "macos")]
{
let hash = md5::compute(key.as_bytes());
let mut id = [0u8; 16];
id.copy_from_slice(&hash[..16]); // Take the first 16 bytes of the hash
win_builder = win_builder.data_store_identifier(id);
}
}
if let Some((w, h)) = config.inner_size {
win_builder = win_builder.inner_size(w, h);
} else {
win_builder = win_builder.inner_size(600.0, 600.0);
}
if let Some((x, y)) = config.position {
win_builder = win_builder.position(x, y);
} else {
win_builder = win_builder.center();
}
if let Some(tx) = config.navigation_tx {
win_builder = win_builder.on_navigation(move |url| {
let url = url.to_string();
let tx = tx.clone();
tauri::async_runtime::block_on(async move {
tx.send(url).await.unwrap();
});
true
});
}
if config.hide_titlebar {
#[cfg(target_os = "macos")]
{
use tauri::TitleBarStyle;
win_builder = win_builder.hidden_title(true).title_bar_style(TitleBarStyle::Overlay);
}
#[cfg(not(target_os = "macos"))]
{
// Doesn't seem to work from Rust, here, so we do it in main.tsx
win_builder = win_builder.decorations(false);
}
}
if let Some(w) = handle.webview_windows().get(config.label) {
info!("Webview with label {} already exists. Focusing existing", config.label);
w.set_focus().unwrap();
return w.to_owned();
}
let win = win_builder.build().unwrap();
if let Some(tx) = config.close_tx {
win.on_window_event(move |event| match event {
WindowEvent::CloseRequested { .. } => {
let tx = tx.clone();
tauri::async_runtime::block_on(async move {
tx.send(()).await.unwrap();
});
}
_ => {}
});
}
let webview_window = win.clone();
win.on_menu_event(move |w, event| {
if !w.is_focused().unwrap() {
return;
}
let event_id = event.id().0.as_str();
match event_id {
"quit" => exit(0),
"close" => w.close().unwrap(),
"zoom_reset" => w.emit("zoom_reset", true).unwrap(),
"zoom_in" => w.emit("zoom_in", true).unwrap(),
"zoom_out" => w.emit("zoom_out", true).unwrap(),
"settings" => w.emit("settings", true).unwrap(),
"open_feedback" => {
if let Err(e) =
w.app_handle().opener().open_url("https://yaak.app/feedback", None::<&str>)
{
warn!("Failed to open feedback {e:?}")
}
}
// Commands for development
"dev.reset_size" => webview_window
.set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT))
.unwrap(),
"dev.refresh" => webview_window.eval("location.reload()").unwrap(),
"dev.generate_theme_css" => {
w.emit("generate_theme_css", true).unwrap();
}
"dev.toggle_devtools" => {
if webview_window.is_devtools_open() {
webview_window.close_devtools();
} else {
webview_window.open_devtools();
}
}
_ => {}
}
});
win
}
pub(crate) fn create_main_window(handle: &AppHandle, url: &str) -> WebviewWindow {
let mut counter = 0;
let label = loop {
let label = format!("{MAIN_WINDOW_PREFIX}{counter}");
match handle.webview_windows().get(label.as_str()) {
None => break Some(label),
Some(_) => counter += 1,
}
}
.expect("Failed to generate label for new window");
let config = CreateWindowConfig {
url,
label: label.as_str(),
title: "Yaak",
inner_size: Some((DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)),
position: Some((
// Offset by random amount so it's easier to differentiate
100.0 + random::<f64>() * 20.0,
100.0 + random::<f64>() * 20.0,
)),
hide_titlebar: true,
..Default::default()
};
create_window(handle, config)
}
pub(crate) fn create_child_window(parent_window: &WebviewWindow, url: &str, label: &str, title: &str, inner_size: (f64, f64)) -> WebviewWindow {
let app_handle = parent_window.app_handle();
let label = format!("{OTHER_WINDOW_PREFIX}_{label}");
let scale_factor = parent_window.scale_factor().unwrap();
let current_pos = parent_window.inner_position().unwrap().to_logical::<f64>(scale_factor);
let current_size = parent_window.inner_size().unwrap().to_logical::<f64>(scale_factor);
// Position the new window in the middle of the parent
let position = (
current_pos.x + current_size.width / 2.0 - inner_size.0 / 2.0,
current_pos.y + current_size.height / 2.0 - inner_size.1 / 2.0,
);
let config = CreateWindowConfig {
label: label.as_str(),
title,
url,
inner_size: Some(inner_size),
position: Some(position),
hide_titlebar: true,
..Default::default()
};
let child_window = create_window(&app_handle, config);
// NOTE: These listeners will remain active even when the windows close. Unfortunately,
// there's no way to unlisten to events for now, so we just have to be defensive.
{
let parent_window = parent_window.clone();
let child_window = child_window.clone();
child_window.clone().on_window_event(move |e| match e {
// When the new window is destroyed, bring the other up behind it
WindowEvent::Destroyed => {
if let Some(w) = parent_window.get_webview_window(child_window.label()) {
w.set_focus().unwrap();
}
}
_ => {}
});
}
{
let parent_window = parent_window.clone();
let child_window = child_window.clone();
parent_window.clone().on_window_event(move |e| match e {
// When the parent window is closed, close the child
WindowEvent::CloseRequested { .. } => child_window.destroy().unwrap(),
// When the parent window is focused, bring the child above
WindowEvent::Focused(focus) => {
if *focus {
if let Some(w) = parent_window.get_webview_window(child_window.label()) {
w.set_focus().unwrap();
};
}
}
_ => {}
});
}
child_window
}

View File

@@ -3,9 +3,9 @@ use tauri::menu::{
WINDOW_SUBMENU_ID,
};
pub use tauri::AppHandle;
use tauri::Wry;
use tauri::Runtime;
pub fn app_menu(app_handle: &AppHandle) -> tauri::Result<Menu<Wry>> {
pub fn app_menu<R: Runtime>(app_handle: &AppHandle<R>) -> tauri::Result<Menu<R>> {
let pkg_info = app_handle.package_info();
let config = app_handle.config();
let about_metadata = AboutMetadata {
@@ -37,7 +37,7 @@ pub fn app_menu(app_handle: &AppHandle) -> tauri::Result<Menu<Wry>> {
true,
&[
#[cfg(not(target_os = "macos"))]
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?,
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata.clone()))?,
#[cfg(target_os = "macos")]
&MenuItemBuilder::with_id("open_feedback".to_string(), "Give Feedback")
.build(app_handle)?,

View File

@@ -28,7 +28,7 @@ var plugin = {
name: "basic",
label: "Basic Auth",
shortLabel: "Basic",
config: [{
args: [{
type: "text",
name: "username",
label: "Username",
@@ -40,8 +40,8 @@ var plugin = {
optional: true,
password: true
}],
async onApply(_ctx, args) {
const { username, password } = args.config;
async onApply(_ctx, { values }) {
const { username, password } = values;
const value = "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
return { setHeaders: [{ name: "Authorization", value }] };
}

View File

@@ -28,15 +28,15 @@ var plugin = {
name: "bearer",
label: "Bearer Token",
shortLabel: "Bearer",
config: [{
args: [{
type: "text",
name: "token",
label: "Token",
optional: true,
password: true
}],
async onApply(_ctx, args) {
const { token } = args.config;
async onApply(_ctx, { values }) {
const { token } = values;
const value = `Bearer ${token}`.trim();
return { setHeaders: [{ name: "Authorization", value }] };
}

View File

@@ -1044,6 +1044,7 @@ var require_re = __commonJS({
var re = exports2.re = [];
var safeRe = exports2.safeRe = [];
var src = exports2.src = [];
var safeSrc = exports2.safeSrc = [];
var t = exports2.t = {};
var R = 0;
var LETTERDASHNUMBER = "[a-zA-Z0-9-]";
@@ -1064,6 +1065,7 @@ var require_re = __commonJS({
debug(name, index, value);
t[name] = index;
src[index] = value;
safeSrc[index] = safe;
re[index] = new RegExp(value, isGlobal ? "g" : void 0);
safeRe[index] = new RegExp(safe, isGlobal ? "g" : void 0);
};
@@ -1160,7 +1162,7 @@ var require_semver = __commonJS({
"../../node_modules/semver/classes/semver.js"(exports2, module2) {
var debug = require_debug();
var { MAX_LENGTH, MAX_SAFE_INTEGER } = require_constants();
var { safeRe: re, t } = require_re();
var { safeRe: re, safeSrc: src, t } = require_re();
var parseOptions = require_parse_options();
var { compareIdentifiers } = require_identifiers();
var SemVer = class _SemVer {
@@ -1300,6 +1302,18 @@ var require_semver = __commonJS({
// preminor will bump the version up to the next minor release, and immediately
// down to pre-release. premajor and prepatch work the same way.
inc(release, identifier, identifierBase) {
if (release.startsWith("pre")) {
if (!identifier && identifierBase === false) {
throw new Error("invalid increment argument: identifier is empty");
}
if (identifier) {
const r = new RegExp(`^${this.options.loose ? src[t.PRERELEASELOOSE] : src[t.PRERELEASE]}$`);
const match = `-${identifier}`.match(r);
if (!match || match[1] !== identifier) {
throw new Error(`invalid identifier: ${identifier}`);
}
}
}
switch (release) {
case "premajor":
this.prerelease.length = 0;
@@ -1325,6 +1339,12 @@ var require_semver = __commonJS({
}
this.inc("pre", identifier, identifierBase);
break;
case "release":
if (this.prerelease.length === 0) {
throw new Error(`version ${this.raw} is not a prerelease`);
}
this.prerelease.length = 0;
break;
case "major":
if (this.minor !== 0 || this.patch !== 0 || this.prerelease.length === 0) {
this.major++;
@@ -1348,9 +1368,6 @@ var require_semver = __commonJS({
break;
case "pre": {
const base = Number(identifierBase) ? 1 : 0;
if (!identifier && identifierBase === false) {
throw new Error("invalid increment argument: identifier is empty");
}
if (this.prerelease.length === 0) {
this.prerelease = [base];
} else {
@@ -1485,13 +1502,12 @@ var require_diff = __commonJS({
if (!lowVersion.patch && !lowVersion.minor) {
return "major";
}
if (highVersion.patch) {
if (lowVersion.compareMain(highVersion) === 0) {
if (lowVersion.minor && !lowVersion.patch) {
return "minor";
}
return "patch";
}
if (highVersion.minor) {
return "minor";
}
return "major";
}
const prefix = highHasPre ? "pre" : "";
if (v1.major !== v2.major) {
@@ -3805,7 +3821,8 @@ var algorithms = [
"PS512",
"ES256",
"ES384",
"ES512"
"ES512",
"none"
];
var defaultAlgorithm = algorithms[0];
var plugin = {
@@ -3813,35 +3830,39 @@ var plugin = {
name: "jwt",
label: "JWT Bearer",
shortLabel: "JWT",
config: [
args: [
{
type: "select",
name: "algorithm",
label: "Algorithm",
hideLabel: true,
defaultValue: defaultAlgorithm,
options: algorithms.map((value) => ({ name: value, value }))
options: algorithms.map((value) => ({ label: value === "none" ? "None" : value, value }))
},
{
type: "text",
name: "secret",
label: "Secret",
optional: true
label: "Secret or Private Key",
password: true,
optional: true,
multiLine: true
},
{
type: "checkbox",
name: "secretBase64",
label: "Secret Base64 Encoded"
label: "Secret is base64 encoded"
},
{
type: "editor",
name: "payload",
label: "Payload",
language: "json",
optional: true
defaultValue: '{\n "foo": "bar"\n}',
placeholder: "{ }"
}
],
async onApply(_ctx, args) {
const { algorithm, secret: _secret, secretBase64, payload } = args.config;
async onApply(_ctx, { values }) {
const { algorithm, secret: _secret, secretBase64, payload } = values;
const secret = secretBase64 ? Buffer.from(`${_secret}`, "base64") : `${_secret}`;
const token = import_jsonwebtoken.default.sign(`${payload}`, secret, { algorithm });
const value = `Bearer ${token}`;

View File

@@ -0,0 +1,701 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
// src/grants/authorizationCode.ts
var import_node_crypto = require("node:crypto");
// src/getAccessToken.ts
var import_node_fs = require("node:fs");
async function getAccessToken(ctx, {
accessTokenUrl,
scope,
params,
grantType,
credentialsInBody,
clientId,
clientSecret
}) {
console.log("Getting access token", accessTokenUrl);
const httpRequest = {
method: "POST",
url: accessTokenUrl,
bodyType: "application/x-www-form-urlencoded",
body: {
form: [
{ name: "grant_type", value: grantType },
...params
]
},
headers: [
{ name: "User-Agent", value: "yaak" },
{ name: "Accept", value: "application/x-www-form-urlencoded, application/json" },
{ name: "Content-Type", value: "application/x-www-form-urlencoded" }
]
};
if (scope) httpRequest.body.form.push({ name: "scope", value: scope });
if (credentialsInBody) {
httpRequest.body.form.push({ name: "client_id", value: clientId });
httpRequest.body.form.push({ name: "client_secret", value: clientSecret });
} else {
const value = "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
httpRequest.headers.push({ name: "Authorization", value });
}
const resp = await ctx.httpRequest.send({ httpRequest });
if (resp.status < 200 || resp.status >= 300) {
throw new Error("Failed to fetch access token with status=" + resp.status);
}
const body = (0, import_node_fs.readFileSync)(resp.bodyPath ?? "", "utf8");
let response;
try {
response = JSON.parse(body);
} catch {
response = Object.fromEntries(new URLSearchParams(body));
}
if (response.error) {
throw new Error("Failed to fetch access token with " + response.error);
}
return response;
}
// src/getOrRefreshAccessToken.ts
var import_node_fs2 = require("node:fs");
// src/store.ts
async function storeToken(ctx, contextId, response) {
if (!response.access_token) {
throw new Error(`Token not found in response`);
}
const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1e3 : null;
const token = {
response,
expiresAt
};
await ctx.store.set(tokenStoreKey(contextId), token);
return token;
}
async function getToken(ctx, contextId) {
return ctx.store.get(tokenStoreKey(contextId));
}
async function deleteToken(ctx, contextId) {
return ctx.store.delete(tokenStoreKey(contextId));
}
async function resetDataDirKey(ctx, contextId) {
const key = (/* @__PURE__ */ new Date()).toISOString();
return ctx.store.set(dataDirStoreKey(contextId), key);
}
async function getDataDirKey(ctx, contextId) {
const key = await ctx.store.get(dataDirStoreKey(contextId)) ?? "default";
return `${contextId}::${key}`;
}
function tokenStoreKey(context_id) {
return ["token", context_id].join("::");
}
function dataDirStoreKey(context_id) {
return ["data_dir", context_id].join("::");
}
// src/getOrRefreshAccessToken.ts
async function getOrRefreshAccessToken(ctx, contextId, {
scope,
accessTokenUrl,
credentialsInBody,
clientId,
clientSecret,
forceRefresh
}) {
const token = await getToken(ctx, contextId);
if (token == null) {
return null;
}
const now = Date.now();
const isExpired = token.expiresAt && now > token.expiresAt;
if (!isExpired && !forceRefresh) {
return token;
}
if (!token.response.refresh_token) {
return null;
}
const httpRequest = {
method: "POST",
url: accessTokenUrl,
bodyType: "application/x-www-form-urlencoded",
body: {
form: [
{ name: "grant_type", value: "refresh_token" },
{ name: "refresh_token", value: token.response.refresh_token }
]
},
headers: [
{ name: "User-Agent", value: "yaak" },
{ name: "Accept", value: "application/x-www-form-urlencoded, application/json" },
{ name: "Content-Type", value: "application/x-www-form-urlencoded" }
]
};
if (scope) httpRequest.body.form.push({ name: "scope", value: scope });
if (credentialsInBody) {
httpRequest.body.form.push({ name: "client_id", value: clientId });
httpRequest.body.form.push({ name: "client_secret", value: clientSecret });
} else {
const value = "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
httpRequest.headers.push({ name: "Authorization", value });
}
const resp = await ctx.httpRequest.send({ httpRequest });
if (resp.status === 401) {
console.log("Unauthorized refresh_token request");
await deleteToken(ctx, contextId);
return null;
}
if (resp.status < 200 || resp.status >= 300) {
throw new Error("Failed to fetch access token with status=" + resp.status);
}
const body = (0, import_node_fs2.readFileSync)(resp.bodyPath ?? "", "utf8");
let response;
try {
response = JSON.parse(body);
} catch {
response = Object.fromEntries(new URLSearchParams(body));
}
if (response.error) {
throw new Error(`Failed to fetch access token with ${response.error} -> ${response.error_description}`);
}
const newResponse = {
...response,
// Assign a new one or keep the old one,
refresh_token: response.refresh_token ?? token.response.refresh_token
};
return storeToken(ctx, contextId, newResponse);
}
// src/grants/authorizationCode.ts
var PKCE_SHA256 = "S256";
var PKCE_PLAIN = "plain";
var DEFAULT_PKCE_METHOD = PKCE_SHA256;
async function getAuthorizationCode(ctx, contextId, {
authorizationUrl: authorizationUrlRaw,
accessTokenUrl,
clientId,
clientSecret,
redirectUri,
scope,
state,
credentialsInBody,
pkce
}) {
const token = await getOrRefreshAccessToken(ctx, contextId, {
accessTokenUrl,
scope,
clientId,
clientSecret,
credentialsInBody
});
if (token != null) {
return token;
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`);
authorizationUrl.searchParams.set("response_type", "code");
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 (pkce) {
const verifier = pkce.codeVerifier || createPkceCodeVerifier();
const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD;
authorizationUrl.searchParams.set("code_challenge", createPkceCodeChallenge(verifier, challengeMethod));
authorizationUrl.searchParams.set("code_challenge_method", challengeMethod);
}
return new Promise(async (resolve, reject) => {
const authorizationUrlStr = authorizationUrl.toString();
console.log("Authorizing", authorizationUrlStr);
let foundCode = false;
let { close } = await ctx.window.openUrl({
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 (url.searchParams.has("error")) {
return reject(new Error(`Failed to authorize: ${url.searchParams.get("error")}`));
}
const code = url.searchParams.get("code");
if (!code) {
return;
}
foundCode = true;
close();
const response = await getAccessToken(ctx, {
grantType: "authorization_code",
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsInBody,
params: [
{ name: "code", value: code },
...redirectUri ? [{ name: "redirect_uri", value: redirectUri }] : []
]
});
try {
resolve(await storeToken(ctx, contextId, response));
} catch (err) {
reject(err);
}
}
});
});
}
function createPkceCodeVerifier() {
return encodeForPkce((0, import_node_crypto.randomBytes)(32));
}
function createPkceCodeChallenge(verifier, method) {
if (method === "plain") {
return verifier;
}
const hash = encodeForPkce((0, import_node_crypto.createHash)("sha256").update(verifier).digest());
return hash.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
function encodeForPkce(bytes) {
return bytes.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
// src/grants/clientCredentials.ts
async function getClientCredentials(ctx, contextId, {
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsInBody
}) {
const token = await getToken(ctx, contextId);
if (token) {
}
const response = await getAccessToken(ctx, {
grantType: "client_credentials",
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsInBody,
params: []
});
return storeToken(ctx, contextId, response);
}
// src/grants/implicit.ts
function getImplicit(ctx, contextId, {
authorizationUrl: authorizationUrlRaw,
responseType,
clientId,
redirectUri,
scope,
state
}) {
return new Promise(async (resolve, reject) => {
const token = await getToken(ctx, contextId);
if (token) {
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`);
authorizationUrl.searchParams.set("response_type", "code");
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 (responseType.includes("id_token")) {
authorizationUrl.searchParams.set("nonce", String(Math.floor(Math.random() * 9999999999999) + 1));
}
const authorizationUrlStr = authorizationUrl.toString();
let { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: "oauth-authorization-url",
async onNavigate({ url: urlStr }) {
const url = new URL(urlStr);
if (url.searchParams.has("error")) {
return reject(Error(`Failed to authorize: ${url.searchParams.get("error")}`));
}
close();
const hash = url.hash.slice(1);
const params = new URLSearchParams(hash);
const idToken = params.get("id_token");
if (idToken) {
params.set("access_token", idToken);
params.delete("id_token");
}
const response = Object.fromEntries(params);
try {
resolve(await storeToken(ctx, contextId, response));
} catch (err) {
reject(err);
}
}
});
});
}
// src/grants/password.ts
async function getPassword(ctx, contextId, {
accessTokenUrl,
clientId,
clientSecret,
username,
password,
credentialsInBody,
scope
}) {
const token = await getOrRefreshAccessToken(ctx, contextId, {
accessTokenUrl,
scope,
clientId,
clientSecret,
credentialsInBody
});
if (token != null) {
return token;
}
const response = await getAccessToken(ctx, {
accessTokenUrl,
clientId,
clientSecret,
scope,
grantType: "password",
credentialsInBody,
params: [
{ name: "username", value: username },
{ name: "password", value: password }
]
});
return storeToken(ctx, contextId, response);
}
// src/index.ts
var grantTypes = [
{ label: "Authorization Code", value: "authorization_code" },
{ label: "Implicit", value: "implicit" },
{ label: "Resource Owner Password Credential", value: "password" },
{ label: "Client Credentials", value: "client_credentials" }
];
var defaultGrantType = grantTypes[0].value;
function hiddenIfNot(grantTypes2, ...other) {
return (_ctx, { values }) => {
const hasGrantType = grantTypes2.find((t) => t === String(values.grantType ?? defaultGrantType));
const hasOtherBools = other.every((t) => t(values));
const show = hasGrantType && hasOtherBools;
return { hidden: !show };
};
}
var authorizationUrls = [
"https://github.com/login/oauth/authorize",
"https://account.box.com/api/oauth2/authorize",
"https://accounts.google.com/o/oauth2/v2/auth",
"https://api.imgur.com/oauth2/authorize",
"https://bitly.com/oauth/authorize",
"https://gitlab.example.com/oauth/authorize",
"https://medium.com/m/oauth/authorize",
"https://public-api.wordpress.com/oauth2/authorize",
"https://slack.com/oauth/authorize",
"https://todoist.com/oauth/authorize",
"https://www.dropbox.com/oauth2/authorize",
"https://www.linkedin.com/oauth/v2/authorization",
"https://MY_SHOP.myshopify.com/admin/oauth/access_token"
];
var accessTokenUrls = [
"https://github.com/login/oauth/access_token",
"https://api-ssl.bitly.com/oauth/access_token",
"https://api.box.com/oauth2/token",
"https://api.dropboxapi.com/oauth2/token",
"https://api.imgur.com/oauth2/token",
"https://api.medium.com/v1/tokens",
"https://gitlab.example.com/oauth/token",
"https://public-api.wordpress.com/oauth2/token",
"https://slack.com/api/oauth.access",
"https://todoist.com/oauth/access_token",
"https://www.googleapis.com/oauth2/v4/token",
"https://www.linkedin.com/oauth/v2/accessToken",
"https://MY_SHOP.myshopify.com/admin/oauth/authorize"
];
var plugin = {
authentication: {
name: "oauth2",
label: "OAuth 2.0",
shortLabel: "OAuth 2",
actions: [
{
label: "Copy Current Token",
async onSelect(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
if (token == null) {
await ctx.toast.show({ message: "No token to copy", color: "warning" });
} else {
await ctx.clipboard.copyText(token.response.access_token);
await ctx.toast.show({ message: "Token copied to clipboard", icon: "copy", color: "success" });
}
}
},
{
label: "Delete Token",
async onSelect(ctx, { contextId }) {
if (await deleteToken(ctx, contextId)) {
await ctx.toast.show({ message: "Token deleted", color: "success" });
} else {
await ctx.toast.show({ message: "No token to delete", color: "warning" });
}
}
},
{
label: "Clear Window Session",
async onSelect(ctx, { contextId }) {
await resetDataDirKey(ctx, contextId);
}
}
],
args: [
{
type: "select",
name: "grantType",
label: "Grant Type",
hideLabel: true,
defaultValue: defaultGrantType,
options: grantTypes
},
// Always-present fields
{
type: "text",
name: "clientId",
label: "Client ID",
optional: true
},
{
type: "text",
name: "clientSecret",
label: "Client Secret",
optional: true,
password: true,
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"])
},
{
type: "text",
name: "authorizationUrl",
optional: true,
label: "Authorization URL",
dynamic: hiddenIfNot(["authorization_code", "implicit"]),
placeholder: authorizationUrls[0],
completionOptions: authorizationUrls.map((url) => ({ label: url, value: url }))
},
{
type: "text",
name: "accessTokenUrl",
optional: true,
label: "Access Token URL",
placeholder: accessTokenUrls[0],
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"]),
completionOptions: accessTokenUrls.map((url) => ({ label: url, value: url }))
},
{
type: "text",
name: "redirectUri",
label: "Redirect URI",
optional: true,
dynamic: hiddenIfNot(["authorization_code", "implicit"])
},
{
type: "text",
name: "state",
label: "State",
optional: true,
dynamic: hiddenIfNot(["authorization_code", "implicit"])
},
{
type: "checkbox",
name: "usePkce",
label: "Use PKCE",
dynamic: hiddenIfNot(["authorization_code"])
},
{
type: "select",
name: "pkceChallengeMethod",
label: "Code Challenge Method",
options: [{ label: "SHA-256", value: PKCE_SHA256 }, { label: "Plain", value: PKCE_PLAIN }],
defaultValue: DEFAULT_PKCE_METHOD,
dynamic: hiddenIfNot(["authorization_code"], ({ usePkce }) => !!usePkce)
},
{
type: "text",
name: "pkceCodeVerifier",
label: "Code Verifier",
placeholder: "Automatically generated if not provided",
optional: true,
dynamic: hiddenIfNot(["authorization_code"], ({ usePkce }) => !!usePkce)
},
{
type: "text",
name: "username",
label: "Username",
optional: true,
dynamic: hiddenIfNot(["password"])
},
{
type: "text",
name: "password",
label: "Password",
password: true,
optional: true,
dynamic: hiddenIfNot(["password"])
},
{
type: "select",
name: "responseType",
label: "Response Type",
defaultValue: "token",
options: [
{ label: "Access Token", value: "token" },
{ label: "ID Token", value: "id_token" },
{ label: "ID and Access Token", value: "id_token token" }
],
dynamic: hiddenIfNot(["implicit"])
},
{
type: "accordion",
label: "Advanced",
inputs: [
{ type: "text", name: "scope", label: "Scope", optional: true },
{ type: "text", name: "headerPrefix", label: "Header Prefix", optional: true, defaultValue: "Bearer" },
{
type: "select",
name: "credentials",
label: "Send Credentials",
defaultValue: "body",
options: [
{ label: "In Request Body", value: "body" },
{ label: "As Basic Authentication", value: "basic" }
]
}
]
},
{
type: "accordion",
label: "Access Token Response",
async dynamic(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
if (token == null) {
return { hidden: true };
}
return {
label: "Access Token Response",
inputs: [
{
type: "editor",
defaultValue: JSON.stringify(token.response, null, 2),
hideLabel: true,
readOnly: true,
language: "json"
}
]
};
}
}
],
async onApply(ctx, { values, contextId }) {
const headerPrefix = stringArg(values, "headerPrefix");
const grantType = stringArg(values, "grantType");
const credentialsInBody = values.credentials === "body";
let token;
if (grantType === "authorization_code") {
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}`,
clientId: stringArg(values, "clientId"),
clientSecret: stringArg(values, "clientSecret"),
redirectUri: stringArgOrNull(values, "redirectUri"),
scope: stringArgOrNull(values, "scope"),
state: stringArgOrNull(values, "state"),
credentialsInBody,
pkce: values.usePkce ? {
challengeMethod: stringArg(values, "pkceChallengeMethod"),
codeVerifier: stringArgOrNull(values, "pkceCodeVerifier")
} : null
});
} else if (grantType === "implicit") {
const authorizationUrl = stringArg(values, "authorizationUrl");
token = await getImplicit(ctx, contextId, {
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
clientId: stringArg(values, "clientId"),
redirectUri: stringArgOrNull(values, "redirectUri"),
responseType: stringArg(values, "responseType"),
scope: stringArgOrNull(values, "scope"),
state: stringArgOrNull(values, "state")
});
} else if (grantType === "client_credentials") {
const accessTokenUrl = stringArg(values, "accessTokenUrl");
token = await getClientCredentials(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
clientId: stringArg(values, "clientId"),
clientSecret: stringArg(values, "clientSecret"),
scope: stringArgOrNull(values, "scope"),
credentialsInBody
});
} else if (grantType === "password") {
const accessTokenUrl = stringArg(values, "accessTokenUrl");
token = await getPassword(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
clientId: stringArg(values, "clientId"),
clientSecret: stringArg(values, "clientSecret"),
username: stringArg(values, "username"),
password: stringArg(values, "password"),
scope: stringArgOrNull(values, "scope"),
credentialsInBody
});
} else {
throw new Error("Invalid grant type " + grantType);
}
const headerValue = `${headerPrefix} ${token.response.access_token}`.trim();
return {
setHeaders: [{
name: "Authorization",
value: headerValue
}]
};
}
}
};
function stringArgOrNull(values, name) {
const arg = values[name];
if (arg == null || arg == "") return null;
return `${arg}`;
}
function stringArg(values, name) {
const arg = stringArgOrNull(values, name);
if (!arg) return "";
return arg;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

@@ -1,5 +1,5 @@
{
"name": "@yaakapp/template-function-file",
"name": "@yaakapp/auth-oauth2",
"private": true,
"version": "0.0.1",
"scripts": {

View File

@@ -20,25 +20,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
// src/index.ts
var src_exports = {};
__export(src_exports, {
plugin: () => plugin,
pluginHookExport: () => pluginHookExport
convertToCurl: () => convertToCurl,
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
var NEWLINE = "\\\n ";
var plugin = {
httpRequestActions: [{
key: "export-curl",
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 pluginHookExport(ctx, rendered_request);
ctx.clipboard.copyText(data);
ctx.toast.show({ message: "Curl copied to clipboard", icon: "copy" });
const data = await convertToCurl(rendered_request);
await ctx.clipboard.copyText(data);
await ctx.toast.show({ message: "Curl copied to clipboard", icon: "copy", color: "success" });
}
}]
};
async function pluginHookExport(_ctx, request) {
async function convertToCurl(request) {
const xs = ["curl"];
if (request.method) xs.push("-X", request.method);
if (request.url) xs.push(quote(request.url));
@@ -104,6 +103,6 @@ function maybeParseJSON(v, fallback) {
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin,
pluginHookExport
convertToCurl,
plugin
});

View File

@@ -30,7 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
// src/index.ts
var src_exports = {};
__export(src_exports, {
pluginHookResponseFilter: () => pluginHookResponseFilter
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
@@ -499,12 +499,18 @@ JSONPath.prototype.safeVm = import_vm.default;
var SafeScript = import_vm.default.Script;
// src/index.ts
function pluginHookResponseFilter(_ctx, args) {
const parsed = JSON.parse(args.body);
const filtered = JSONPath({ path: args.filter, json: parsed });
return JSON.stringify(filtered, null, 2);
}
var plugin = {
filter: {
name: "JSONPath",
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) };
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
pluginHookResponseFilter
plugin
});

View File

@@ -8346,21 +8346,27 @@ var require_xpath = __commonJS({
// src/index.ts
var src_exports = {};
__export(src_exports, {
pluginHookResponseFilter: () => pluginHookResponseFilter
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
var import_xmldom = __toESM(require_lib());
var import_xpath = __toESM(require_xpath());
function pluginHookResponseFilter(_ctx, { filter, body }) {
const doc = new import_xmldom.DOMParser().parseFromString(body, "text/xml");
const result = import_xpath.default.select(filter, doc, false);
if (Array.isArray(result)) {
return result.map((r) => String(r)).join("\n");
} else {
return String(result);
var plugin = {
filter: {
name: "XPath",
description: "Filter XPath",
onFilter(_ctx, args) {
const doc = new import_xmldom.DOMParser().parseFromString(args.payload, "text/xml");
const result = import_xpath.default.select(args.filter, doc, false);
if (Array.isArray(result)) {
return { filtered: result.map((r) => String(r)).join("\n") };
} else {
return { filtered: String(result) };
}
}
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
pluginHookResponseFilter
plugin
});

View File

@@ -36,6 +36,9 @@ var require_quote = __commonJS({
"use strict";
module2.exports = function quote(xs) {
return xs.map(function(s) {
if (s === "") {
return "''";
}
if (s && typeof s === "object") {
return s.op.replace(/(.)/g, "\\$1");
}
@@ -260,7 +263,8 @@ var require_shell_quote = __commonJS({
// src/index.ts
var src_exports = {};
__export(src_exports, {
pluginHookImport: () => pluginHookImport
convertCurl: () => convertCurl,
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
var import_shell_quote = __toESM(require_shell_quote());
@@ -290,7 +294,16 @@ var SUPPORTED_FLAGS = [
DATA_FLAGS
].flatMap((v) => v);
var BOOLEAN_FLAGS = ["G", "get", "digest"];
function pluginHookImport(_ctx, rawData) {
var plugin = {
importer: {
name: "cURL",
description: "Import cURL commands",
onImport(_ctx, args) {
return convertCurl(args.text);
}
}
};
function convertCurl(rawData) {
if (!rawData.match(/^\s*curl /)) {
return null;
}
@@ -570,5 +583,6 @@ function generateId(model) {
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
pluginHookImport
convertCurl,
plugin
});

File diff suppressed because it is too large Load Diff

View File

@@ -49782,7 +49782,11 @@ var require_property_base = __commonJS({
* @returns {*|undefined}
*/
parent() {
return this && this.__parent && (this.__parent.__parent || this.__parent) || void 0;
let parent = this.__parent;
if (parent && parent._postman_propertyIsList) {
parent = parent.__parent || parent;
}
return parent || void 0;
},
/**
* Accepts an object and sets it as the parent of the current property.
@@ -77141,7 +77145,7 @@ var require_dynamic_variables = __commonJS({
description: "A random avatar image",
generator: () => {
return faker.random.arrayElement([
// eslint-disable-next-line max-len
// eslint-disable-next-line @stylistic/js/max-len
`https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/${faker.datatype.number(1249)}.jpg`,
`https://avatars.githubusercontent.com/u/${faker.datatype.number(1e8)}`
]);
@@ -77494,7 +77498,7 @@ var require_superstring = __commonJS({
* @readOnly
* @type {RegExp}
*/
REGEX_EXTRACT_VARS: /\{\{([^{}]*?)}}/g,
REGEX_EXTRACT_VARS: /{{([^{}]*?)}}/g,
/**
* Defines the number of times the variable substitution mechanism will repeat until all tokens are resolved
*
@@ -77589,7 +77593,7 @@ var require_property = __commonJS({
} else if (typeof value === "object") {
seen.add(value);
for (const key in value) {
if (Object.hasOwnProperty.call(value, key)) {
if (Object.hasOwn(value, key)) {
_findSubstitutions(value[key], seen, result);
}
}
@@ -77894,7 +77898,7 @@ var require_property_list = __commonJS({
before > -1 ? this.members.splice(before, 0, item) : this.members.push(item);
if ((index = item[this._postman_listIndexKey]) && (index = String(index))) {
this._postman_listIndexCaseInsensitive && (index = index.toLowerCase());
if (this._postman_listAllowsMultipleValues && Object.hasOwnProperty.call(this.reference, index)) {
if (this._postman_listAllowsMultipleValues && Object.hasOwn(this.reference, index)) {
!_2.isArray(this.reference[index]) && (this.reference[index] = [this.reference[index]]);
this.reference[index].push(item);
} else {
@@ -78458,7 +78462,7 @@ var require_parser = __commonJS({
var ReplacementTracker = require_replacement_tracker();
var REGEX_ALL_BACKSLASHES = /\\/g;
var REGEX_LEADING_SLASHES = /^\/+/;
var REGEX_ALL_VARIABLES = /{{[^{}]*[.:/?#@&\]][^{}]*}}/g;
var REGEX_ALL_VARIABLES = /{{[^{}]*}}/g;
var HASH_SEPARATOR = "#";
var PATH_SEPARATOR = "/";
var PORT_SEPARATOR = ":";
@@ -78613,6 +78617,7 @@ var require_query_param = __commonJS({
"../../node_modules/postman-collection/lib/collection/query-param.js"(exports2, module2) {
var _2 = require_util2().lodash;
var Property = require_property().Property;
var Substitutor = require_superstring().Substitutor;
var PropertyList = require_property_list().PropertyList;
var E = "";
var AMPERSAND = "&";
@@ -78623,7 +78628,7 @@ var require_query_param = __commonJS({
var REGEX_HASH = /#/g;
var REGEX_EQUALS = /=/g;
var REGEX_AMPERSAND = /&/g;
var REGEX_EXTRACT_VARS = /{{[^{}]*[&#=][^{}]*}}/g;
var REGEX_EXTRACT_VARS = Substitutor.REGEX_EXTRACT_VARS;
var QueryParam;
var encodeReservedChars = function(str, encodeEquals) {
if (!str) {
@@ -92126,7 +92131,7 @@ var require_db3 = __commonJS({
type: "embed",
format: "pdf"
},
"application/ecmascript": {
"text/javascript": {
type: "text",
format: "script"
},
@@ -92134,6 +92139,66 @@ var require_db3 = __commonJS({
type: "text",
format: "script"
},
"application/ecmascript": {
type: "text",
format: "script"
},
"application/x-ecmascript": {
type: "text",
format: "script"
},
"application/x-javascript": {
type: "text",
format: "script"
},
"text/ecmascript": {
type: "text",
format: "script"
},
"text/javascript1.0": {
type: "text",
format: "script"
},
"text/javascript1.1": {
type: "text",
format: "script"
},
"text/javascript1.2": {
type: "text",
format: "script"
},
"text/javascript1.3": {
type: "text",
format: "script"
},
"text/javascript1.4": {
type: "text",
format: "script"
},
"text/javascript1.5": {
type: "text",
format: "script"
},
"text/jscript": {
type: "text",
format: "script"
},
"text/livescript": {
type: "text",
format: "script"
},
"text/x-ecmascript": {
type: "text",
format: "script"
},
"text/x-javascript": {
type: "text",
format: "script"
},
"text/css": {
type: "text",
format: "stylesheet"
},
"application/json": {
type: "text",
format: "json"
@@ -94086,7 +94151,7 @@ var require_content_info = __commonJS({
* egHeader: inline; filename="test Response.json"
* Reference: https://github.com/jshttp/content-disposition
*/
// eslint-disable-next-line max-len
// eslint-disable-next-line @stylistic/js/max-len
fileNameRegex: /;[ \t]*(?:filename)[ \t]*=[ \t]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[ \t]*/,
/**
* RegExp for extracting filename* from content-disposition header
@@ -94119,7 +94184,7 @@ var require_content_info = __commonJS({
* egHeader: attachment;filename*=utf-8''%E4%BD%A0%E5%A5%BD.txt
* Reference: https://github.com/jshttp/content-disposition
*/
// eslint-disable-next-line max-len, security/detect-unsafe-regex
// eslint-disable-next-line @stylistic/js/max-len, security/detect-unsafe-regex
encodedFileNameRegex: /;[ \t]*(?:filename\*)[ \t]*=[ \t]*([A-Za-z0-9!#$%&+\-^_`{}~]+)'.*'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)[ \t]*/,
/**
* RegExp to match quoted-pair in RFC 2616
@@ -95313,6 +95378,7 @@ var require_re = __commonJS({
var re = exports2.re = [];
var safeRe = exports2.safeRe = [];
var src = exports2.src = [];
var safeSrc = exports2.safeSrc = [];
var t = exports2.t = {};
var R = 0;
var LETTERDASHNUMBER = "[a-zA-Z0-9-]";
@@ -95333,6 +95399,7 @@ var require_re = __commonJS({
debug(name, index, value);
t[name] = index;
src[index] = value;
safeSrc[index] = safe;
re[index] = new RegExp(value, isGlobal ? "g" : void 0);
safeRe[index] = new RegExp(safe, isGlobal ? "g" : void 0);
};
@@ -95429,7 +95496,7 @@ var require_semver = __commonJS({
"../../node_modules/semver/classes/semver.js"(exports2, module2) {
var debug = require_debug();
var { MAX_LENGTH, MAX_SAFE_INTEGER } = require_constants();
var { safeRe: re, t } = require_re();
var { safeRe: re, safeSrc: src, t } = require_re();
var parseOptions = require_parse_options();
var { compareIdentifiers } = require_identifiers();
var SemVer = class _SemVer {
@@ -95569,6 +95636,18 @@ var require_semver = __commonJS({
// preminor will bump the version up to the next minor release, and immediately
// down to pre-release. premajor and prepatch work the same way.
inc(release, identifier, identifierBase) {
if (release.startsWith("pre")) {
if (!identifier && identifierBase === false) {
throw new Error("invalid increment argument: identifier is empty");
}
if (identifier) {
const r = new RegExp(`^${this.options.loose ? src[t.PRERELEASELOOSE] : src[t.PRERELEASE]}$`);
const match = `-${identifier}`.match(r);
if (!match || match[1] !== identifier) {
throw new Error(`invalid identifier: ${identifier}`);
}
}
}
switch (release) {
case "premajor":
this.prerelease.length = 0;
@@ -95594,6 +95673,12 @@ var require_semver = __commonJS({
}
this.inc("pre", identifier, identifierBase);
break;
case "release":
if (this.prerelease.length === 0) {
throw new Error(`version ${this.raw} is not a prerelease`);
}
this.prerelease.length = 0;
break;
case "major":
if (this.minor !== 0 || this.patch !== 0 || this.prerelease.length === 0) {
this.major++;
@@ -95617,9 +95702,6 @@ var require_semver = __commonJS({
break;
case "pre": {
const base = Number(identifierBase) ? 1 : 0;
if (!identifier && identifierBase === false) {
throw new Error("invalid increment argument: identifier is empty");
}
if (this.prerelease.length === 0) {
this.prerelease = [base];
} else {
@@ -95754,13 +95836,12 @@ var require_diff = __commonJS({
if (!lowVersion.patch && !lowVersion.minor) {
return "major";
}
if (highVersion.patch) {
if (lowVersion.compareMain(highVersion) === 0) {
if (lowVersion.minor && !lowVersion.patch) {
return "minor";
}
return "patch";
}
if (highVersion.minor) {
return "minor";
}
return "major";
}
const prefix = highHasPre ? "pre" : "";
if (v12.major !== v2.major) {
@@ -113483,30 +113564,30 @@ var require_json_schema_faker = __commonJS({
});
};
exports5.filter = function(plugins, method, file) {
return plugins.filter(function(plugin) {
return !!getResult(plugin, method, file);
return plugins.filter(function(plugin2) {
return !!getResult(plugin2, method, file);
});
};
exports5.sort = function(plugins) {
plugins.forEach(function(plugin) {
plugin.order = plugin.order || Number.MAX_SAFE_INTEGER;
plugins.forEach(function(plugin2) {
plugin2.order = plugin2.order || Number.MAX_SAFE_INTEGER;
});
return plugins.sort(function(a, b) {
return a.order - b.order;
});
};
exports5.run = function(plugins, method, file) {
var plugin, lastError, index = 0;
var plugin2, lastError, index = 0;
return new Promise(function(resolve2, reject) {
runNextPlugin();
function runNextPlugin() {
plugin = plugins[index++];
if (!plugin) {
plugin2 = plugins[index++];
if (!plugin2) {
return reject(lastError);
}
try {
debug(" %s", plugin.name);
var result = getResult(plugin, method, file, callback);
debug(" %s", plugin2.name);
var result = getResult(plugin2, method, file, callback);
if (result && typeof result.then === "function") {
result.then(onSuccess, onError);
} else if (result !== void 0) {
@@ -113526,7 +113607,7 @@ var require_json_schema_faker = __commonJS({
function onSuccess(result) {
debug(" success");
resolve2({
plugin,
plugin: plugin2,
result
});
}
@@ -136895,8 +136976,6 @@ var require_utils3 = __commonJS({
originalRequest.url.query = [];
originalRequest.header = _2.get(response, "originalRequest.headers", []);
originalRequest.body = requestItem.request.body;
response.code = response.code.replace(/X|x/g, "0");
response.code = response.code === "default" ? 500 : _2.toSafeInteger(response.code);
let sdkResponse = new Response({
name: response.name,
code: response.code,
@@ -137692,7 +137771,7 @@ var require_schemaUtils2 = __commonJS({
}
exampleKey = Object.keys(exampleObj)[0];
example = exampleObj[exampleKey];
if (example.$ref) {
if (example && example.$ref) {
example = resolveExampleData(context, example);
}
if (_2.get(example, "value")) {
@@ -137881,6 +137960,65 @@ var require_schemaUtils2 = __commonJS({
}
return schema2;
};
var processSchema = (resolvedSchema) => {
if (resolvedSchema.type === "object" && resolvedSchema.properties) {
const schemaDetails = {
type: resolvedSchema.type,
properties: {},
required: []
}, requiredProperties = new Set(resolvedSchema.required || []);
for (let [propName, propValue] of Object.entries(resolvedSchema.properties)) {
if (!propValue.type) {
continue;
}
const propertyDetails = {
type: propValue.type,
deprecated: propValue.deprecated,
enum: propValue.enum || void 0,
minLength: propValue.minLength,
maxLength: propValue.maxLength,
minimum: propValue.minimum,
maximum: propValue.maximum,
pattern: propValue.pattern,
example: propValue.example,
description: propValue.description,
format: propValue.format
};
if (requiredProperties.has(propName)) {
schemaDetails.required.push(propName);
}
if (propValue.properties) {
let processedProperties = processSchema(propValue);
propertyDetails.properties = processedProperties.properties;
if (processedProperties.required) {
propertyDetails.required = processedProperties.required;
}
} else if (propValue.type === "array" && propValue.items) {
propertyDetails.items = processSchema(propValue.items);
}
schemaDetails.properties[propName] = propertyDetails;
}
if (schemaDetails.required && schemaDetails.required.length === 0) {
schemaDetails.required = void 0;
}
return schemaDetails;
} else if (resolvedSchema.type === "array" && resolvedSchema.items) {
const arrayDetails = {
type: resolvedSchema.type,
items: processSchema(resolvedSchema.items)
};
if (resolvedSchema.minItems !== void 0) {
arrayDetails.minItems = resolvedSchema.minItems;
}
if (resolvedSchema.maxItems !== void 0) {
arrayDetails.maxItems = resolvedSchema.maxItems;
}
return arrayDetails;
}
return {
type: resolvedSchema.type
};
};
var resolveSchema = (context, schema2, { stack = 0, resolveFor = CONVERSION, seenRef = {}, isResponseSchema = false } = {}) => {
resetReadWritePropCache(context);
let resolvedSchema = _resolveSchema(context, schema2, stack, resolveFor, seenRef);
@@ -138227,12 +138365,16 @@ var require_schemaUtils2 = __commonJS({
return pmExamples;
};
var resolveBodyData = (context, requestBodySchema, bodyType, isExampleBody = false, responseCode = null, requestBodyExamples = {}) => {
let { parametersResolution, indentCharacter } = context.computedOptions, headerFamily = getHeaderFamily(bodyType), bodyData = "", shouldGenerateFromExample = parametersResolution === "example", isBodyTypeXML = bodyType === APP_XML || bodyType === TEXT_XML || headerFamily === HEADER_TYPE.XML, bodyKey = isExampleBody ? "response" : "request", responseExamples, example, examples;
let { parametersResolution, indentCharacter } = context.computedOptions, headerFamily = getHeaderFamily(bodyType), bodyData = "", shouldGenerateFromExample = parametersResolution === "example", isBodyTypeXML = bodyType === APP_XML || bodyType === TEXT_XML || headerFamily === HEADER_TYPE.XML, bodyKey = isExampleBody ? "response" : "request", responseExamples, example, examples, resolvedSchemaTypes = [];
if (_2.isEmpty(requestBodySchema)) {
return [{ [bodyKey]: bodyData }];
}
if (requestBodySchema.$ref) {
requestBodySchema = resolveSchema(context, requestBodySchema, { isResponseSchema: isExampleBody });
requestBodySchema = resolveSchema(
context,
requestBodySchema,
{ isResponseSchema: isExampleBody }
);
}
if (requestBodySchema.example !== void 0) {
const shouldResolveValueKey = _2.has(requestBodySchema.example, "value") && _2.keys(requestBodySchema.example).length <= 1;
@@ -138243,7 +138385,11 @@ var require_schemaUtils2 = __commonJS({
}
examples = requestBodySchema.examples || _2.get(requestBodySchema, "schema.examples");
requestBodySchema = requestBodySchema.schema || requestBodySchema;
requestBodySchema = resolveSchema(context, requestBodySchema, { isResponseSchema: isExampleBody });
requestBodySchema = resolveSchema(
context,
requestBodySchema,
{ isResponseSchema: isExampleBody }
);
if (example === void 0 && _2.get(requestBodySchema, "example") !== void 0) {
example = requestBodySchema.example;
}
@@ -138257,7 +138403,11 @@ var require_schemaUtils2 = __commonJS({
} else if (requestBodySchema) {
requestBodySchema = requestBodySchema.schema || requestBodySchema;
if (requestBodySchema.$ref) {
requestBodySchema = resolveSchema(context, requestBodySchema, { isResponseSchema: isExampleBody });
requestBodySchema = resolveSchema(
context,
requestBodySchema,
{ isResponseSchema: isExampleBody }
);
}
if (isBodyTypeXML) {
bodyData = xmlFaker(null, requestBodySchema, indentCharacter, parametersResolution);
@@ -138285,6 +138435,10 @@ var require_schemaUtils2 = __commonJS({
}
}
}
if (context.enableTypeFetching && requestBodySchema.type !== void 0) {
const requestBodySchemaTypes = processSchema(requestBodySchema);
resolvedSchemaTypes.push(requestBodySchemaTypes);
}
if (isExampleBody && shouldGenerateFromExample && (_2.size(examples) > 1 || _2.size(requestBodyExamples) > 1)) {
responseExamples = [{
key: "_default",
@@ -138305,22 +138459,37 @@ var require_schemaUtils2 = __commonJS({
if (_2.isEmpty(matchedRequestBodyExamples)) {
matchedRequestBodyExamples = requestBodyExamples;
}
return generateExamples(context, responseExamples, matchedRequestBodyExamples, requestBodySchema, isBodyTypeXML);
const generatedBody = generateExamples(
context,
responseExamples,
matchedRequestBodyExamples,
requestBodySchema,
isBodyTypeXML
);
return {
generatedBody,
resolvedSchemaType: resolvedSchemaTypes[0]
};
}
return [{ [bodyKey]: bodyData }];
return {
generatedBody: [{ [bodyKey]: bodyData }],
resolvedSchemaType: resolvedSchemaTypes[0]
};
};
var resolveUrlEncodedRequestBodyForPostmanRequest = (context, requestBodyContent) => {
let bodyData = "", urlEncodedParams = [], requestBodyData = {
mode: "urlencoded",
urlencoded: urlEncodedParams
}, resolvedBody;
}, resolvedBody, resolvedBodyResult, resolvedSchemaTypeObject;
if (_2.isEmpty(requestBodyContent)) {
return requestBodyData;
}
if (_2.has(requestBodyContent, "schema.$ref")) {
requestBodyContent.schema = resolveSchema(context, requestBodyContent.schema);
}
resolvedBody = resolveBodyData(context, requestBodyContent.schema)[0];
resolvedBodyResult = resolveBodyData(context, requestBodyContent.schema);
resolvedBody = resolvedBodyResult && Array.isArray(resolvedBodyResult.generatedBody) && resolvedBodyResult.generatedBody[0];
resolvedSchemaTypeObject = resolvedBodyResult && resolvedBodyResult.resolvedSchemaType;
resolvedBody && (bodyData = resolvedBody.request);
const encoding = requestBodyContent.encoding || {};
_2.forOwn(bodyData, (value, key) => {
@@ -138342,18 +138511,21 @@ var require_schemaUtils2 = __commonJS({
headers: [{
key: "Content-Type",
value: URLENCODED
}]
}],
resolvedSchemaTypeObject
};
};
var resolveFormDataRequestBodyForPostmanRequest = (context, requestBodyContent) => {
let bodyData = "", formDataParams = [], encoding = {}, requestBodyData = {
mode: "formdata",
formdata: formDataParams
}, resolvedBody;
}, resolvedBody, resolvedBodyResult, resolvedSchemaTypeObject;
if (_2.isEmpty(requestBodyContent)) {
return requestBodyData;
}
resolvedBody = resolveBodyData(context, requestBodyContent.schema)[0];
resolvedBodyResult = resolveBodyData(context, requestBodyContent.schema);
resolvedBody = resolvedBodyResult && Array.isArray(resolvedBodyResult.generatedBody) && resolvedBodyResult.generatedBody[0];
resolvedSchemaTypeObject = resolvedBodyResult && resolvedBodyResult.resolvedSchemaType;
resolvedBody && (bodyData = resolvedBody.request);
encoding = _2.get(requestBodyContent, "encoding", {});
_2.forOwn(bodyData, (value, key) => {
@@ -138389,7 +138561,8 @@ var require_schemaUtils2 = __commonJS({
headers: [{
key: "Content-Type",
value: FORM_DATA
}]
}],
resolvedSchemaTypeObject
};
};
var getRawBodyType = (content) => {
@@ -138425,14 +138598,16 @@ var require_schemaUtils2 = __commonJS({
return bodyType;
};
var resolveRawModeRequestBodyForPostmanRequest = (context, requestContent) => {
let bodyType = getRawBodyType(requestContent), bodyData, headerFamily, dataToBeReturned = {}, { concreteUtils } = context, resolvedBody;
let bodyType = getRawBodyType(requestContent), bodyData, headerFamily, dataToBeReturned = {}, { concreteUtils } = context, resolvedBody, resolvedBodyResult, resolvedSchemaTypeObject;
headerFamily = getHeaderFamily(bodyType);
if (concreteUtils.isBinaryContentType(bodyType, requestContent)) {
dataToBeReturned = {
mode: "file"
};
} else {
resolvedBody = resolveBodyData(context, requestContent[bodyType], bodyType)[0];
resolvedBodyResult = resolveBodyData(context, requestContent[bodyType], bodyType);
resolvedBody = resolvedBodyResult && Array.isArray(resolvedBodyResult.generatedBody) && resolvedBodyResult.generatedBody[0];
resolvedSchemaTypeObject = resolvedBodyResult && resolvedBodyResult.resolvedSchemaType;
resolvedBody && (bodyData = resolvedBody.request);
if (bodyType === TEXT_XML || bodyType === APP_XML || headerFamily === HEADER_TYPE.XML) {
bodyData = getXmlVersionContent(bodyData);
@@ -138456,7 +138631,8 @@ var require_schemaUtils2 = __commonJS({
headers: [{
key: "Content-Type",
value: bodyType
}]
}],
resolvedSchemaTypeObject
};
};
var resolveRequestBodyForPostmanRequest = (context, operationItem) => {
@@ -138536,8 +138712,25 @@ var require_schemaUtils2 = __commonJS({
});
return reqParam;
};
var createProperties = (param) => {
const { schema: schema2 } = param;
return {
type: schema2.type,
format: schema2.format,
default: schema2.default,
required: param.required || false,
deprecated: param.deprecated || false,
enum: schema2.enum || void 0,
minLength: schema2.minLength,
maxLength: schema2.maxLength,
minimum: schema2.minimum,
maximum: schema2.maximum,
pattern: schema2.pattern,
example: schema2.example
};
};
var resolveQueryParamsForPostmanRequest = (context, operationItem, method) => {
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters), pmParams = [], { includeDeprecated } = context.computedOptions;
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters), pmParams = [], queryParamTypes = [], { includeDeprecated } = context.computedOptions;
_2.forEach(params, (param) => {
if (!_2.isObject(param)) {
return;
@@ -138545,20 +138738,28 @@ var require_schemaUtils2 = __commonJS({
if (_2.has(param, "$ref")) {
param = resolveSchema(context, param);
}
if (_2.has(param.schema, "$ref")) {
param.schema = resolveSchema(context, param.schema);
}
if (param.in !== QUERYPARAM || !includeDeprecated && param.deprecated) {
return;
}
let paramValue = resolveValueOfParameter(context, param);
let queryParamTypeInfo = {}, properties = {}, paramValue = resolveValueOfParameter(context, param);
if (param && param.name && param.schema && param.schema.type) {
properties = createProperties(param);
queryParamTypeInfo = { keyName: param.name, properties };
queryParamTypes.push(queryParamTypeInfo);
}
if (typeof paramValue === "number" || typeof paramValue === "boolean") {
paramValue = paramValue.toString();
}
const deserialisedParams = serialiseParamsBasedOnStyle(context, param, paramValue);
pmParams.push(...deserialisedParams);
});
return pmParams;
return { queryParamTypes, queryParams: pmParams };
};
var resolvePathParamsForPostmanRequest = (context, operationItem, method) => {
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters), pmParams = [];
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters), pmParams = [], pathParamTypes = [];
_2.forEach(params, (param) => {
if (!_2.isObject(param)) {
return;
@@ -138566,17 +138767,25 @@ var require_schemaUtils2 = __commonJS({
if (_2.has(param, "$ref")) {
param = resolveSchema(context, param);
}
if (_2.has(param.schema, "$ref")) {
param.schema = resolveSchema(context, param.schema);
}
if (param.in !== PATHPARAM) {
return;
}
let paramValue = resolveValueOfParameter(context, param);
let pathParamTypeInfo = {}, properties = {}, paramValue = resolveValueOfParameter(context, param);
if (param && param.name && param.schema && param.schema.type) {
properties = createProperties(param);
pathParamTypeInfo = { keyName: param.name, properties };
pathParamTypes.push(pathParamTypeInfo);
}
if (typeof paramValue === "number" || typeof paramValue === "boolean") {
paramValue = paramValue.toString();
}
const deserialisedParams = serialiseParamsBasedOnStyle(context, param, paramValue);
pmParams.push(...deserialisedParams);
});
return pmParams;
return { pathParamTypes, pathParams: pmParams };
};
var resolveNameForPostmanReqeust = (context, operationItem, requestUrl) => {
let reqName, { requestNameSource } = context.computedOptions;
@@ -138598,7 +138807,7 @@ var require_schemaUtils2 = __commonJS({
return reqName;
};
var resolveHeadersForPostmanRequest = (context, operationItem, method) => {
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters), pmParams = [], { keepImplicitHeaders, includeDeprecated } = context.computedOptions;
const params = resolvePathItemParams(context, operationItem[method].parameters, operationItem.parameters), pmParams = [], headerTypes = [], { keepImplicitHeaders, includeDeprecated } = context.computedOptions;
_2.forEach(params, (param) => {
if (!_2.isObject(param)) {
return;
@@ -138606,25 +138815,33 @@ var require_schemaUtils2 = __commonJS({
if (_2.has(param, "$ref")) {
param = resolveSchema(context, param);
}
if (_2.has(param.schema, "$ref")) {
param.schema = resolveSchema(context, param.schema);
}
if (param.in !== HEADER || !includeDeprecated && param.deprecated) {
return;
}
if (!keepImplicitHeaders && _2.includes(IMPLICIT_HEADERS, _2.toLower(_2.get(param, "name")))) {
return;
}
let paramValue = resolveValueOfParameter(context, param);
let headerTypeInfo = {}, properties = {}, paramValue = resolveValueOfParameter(context, param);
if (param && param.name && param.schema && param.schema.type) {
properties = createProperties(param);
headerTypeInfo = { keyName: param.name, properties };
headerTypes.push(headerTypeInfo);
}
if (typeof paramValue === "number" || typeof paramValue === "boolean") {
paramValue = paramValue.toString();
}
const deserialisedParams = serialiseParamsBasedOnStyle(context, param, paramValue);
pmParams.push(...deserialisedParams);
});
return pmParams;
return { headerTypes, headers: pmParams };
};
var resolveResponseBody = (context, responseBody = {}, requestBodyExamples = {}, code = null) => {
let responseContent, bodyType, allBodyData, headerFamily, acceptHeader, emptyResponse = [{
body: void 0
}];
}], resolvedResponseBodyResult, resolvedResponseBodyTypes;
if (_2.isEmpty(responseBody)) {
return emptyResponse;
}
@@ -138637,7 +138854,16 @@ var require_schemaUtils2 = __commonJS({
}
bodyType = getRawBodyType(responseContent);
headerFamily = getHeaderFamily(bodyType);
allBodyData = resolveBodyData(context, responseContent[bodyType], bodyType, true, code, requestBodyExamples);
resolvedResponseBodyResult = resolveBodyData(
context,
responseContent[bodyType],
bodyType,
true,
code,
requestBodyExamples
);
allBodyData = resolvedResponseBodyResult.generatedBody;
resolvedResponseBodyTypes = resolvedResponseBodyResult.resolvedSchemaType;
return _2.map(allBodyData, (bodyData) => {
let requestBodyData = bodyData.request, responseBodyData = bodyData.response, exampleName = bodyData.name;
if (bodyType === TEXT_XML || bodyType === APP_XML || headerFamily === HEADER_TYPE.XML) {
@@ -138663,12 +138889,13 @@ var require_schemaUtils2 = __commonJS({
}],
name: exampleName,
bodyType,
acceptHeader
acceptHeader,
resolvedResponseBodyTypes
};
});
};
var resolveResponseHeaders = (context, responseHeaders) => {
const headers = [], { includeDeprecated } = context.computedOptions;
const headers = [], { includeDeprecated } = context.computedOptions, headerTypes = [];
if (_2.has(responseHeaders, "$ref")) {
responseHeaders = resolveSchema(context, responseHeaders, { isResponseSchema: true });
}
@@ -138679,14 +138906,33 @@ var require_schemaUtils2 = __commonJS({
if (!includeDeprecated && value.deprecated) {
return;
}
let headerValue = resolveValueOfParameter(context, value, { isResponseSchema: true });
let headerValue = resolveValueOfParameter(context, value, { isResponseSchema: true }), headerTypeInfo = {}, properties = {};
if (typeof headerValue === "number" || typeof headerValue === "boolean") {
headerValue = headerValue.toString();
}
const headerData = Object.assign({}, value, { name: headerName }), serialisedHeader = serialiseParamsBasedOnStyle(context, headerData, headerValue, { isResponseSchema: true });
headers.push(...serialisedHeader);
if (headerData && headerData.name && headerData.schema && headerData.schema.type) {
const { schema: schema2 } = headerData;
properties = {
type: schema2.type,
format: schema2.format,
default: schema2.default,
required: schema2.required || false,
deprecated: schema2.deprecated || false,
enum: schema2.enum || void 0,
minLength: schema2.minLength,
maxLength: schema2.maxLength,
minimum: schema2.minimum,
maximum: schema2.maximum,
pattern: schema2.pattern,
example: schema2.example
};
headerTypeInfo = { keyName: headerData.name, properties };
headerTypes.push(headerTypeInfo);
}
});
return headers;
return { resolvedHeaderTypes: headerTypes, headers };
};
var getPreviewLangugaForResponseBody = (bodyType) => {
const headerFamily = getHeaderFamily(bodyType);
@@ -138753,7 +138999,7 @@ var require_schemaUtils2 = __commonJS({
return responseAuthHelper;
};
var resolveResponseForPostmanRequest = (context, operationItem, request) => {
let responses = [], requestBodyExamples = [], requestAcceptHeader, requestBody = operationItem.requestBody, requestContent, rawBodyType, headerFamily, isBodyTypeXML;
let responses = [], requestBodyExamples = [], requestAcceptHeader, requestBody = operationItem.requestBody, requestContent, rawBodyType, headerFamily, isBodyTypeXML, resolvedExamplesObject = {}, responseTypes = {};
if (typeof requestBody === "object") {
if (requestBody.$ref) {
requestBody = resolveSchema(context, requestBody, { isResponseSchema: true });
@@ -138789,7 +139035,15 @@ var require_schemaUtils2 = __commonJS({
}
}
_2.forOwn(operationItem.responses, (responseObj, code) => {
let responseSchema = _2.has(responseObj, "$ref") ? resolveSchema(context, responseObj, { isResponseSchema: true }) : responseObj, { includeAuthInfoInExample } = context.computedOptions, auth = request.auth, resolvedExamples = resolveResponseBody(context, responseSchema, requestBodyExamples, code) || {}, headers = resolveResponseHeaders(context, responseSchema.headers);
let responseSchema = _2.has(responseObj, "$ref") ? resolveSchema(context, responseObj, { isResponseSchema: true }) : responseObj, { includeAuthInfoInExample } = context.computedOptions, auth = request.auth, resolvedExamples = resolveResponseBody(context, responseSchema, requestBodyExamples, code) || {}, { resolvedHeaderTypes, headers } = resolveResponseHeaders(context, responseSchema.headers), responseBodyHeaderObj;
resolvedExamplesObject = resolvedExamples[0] && resolvedExamples[0].resolvedResponseBodyTypes;
responseBodyHeaderObj = {
body: JSON.stringify(resolvedExamplesObject, null, 2),
headers: JSON.stringify(resolvedHeaderTypes, null, 2)
};
code = code.replace(/X|x/g, "0");
code = code === "default" ? 500 : _2.toSafeInteger(code);
Object.assign(responseTypes, { [code]: responseBodyHeaderObj });
_2.forOwn(resolvedExamples, (resolvedExample = {}) => {
let { body, contentHeader = [], bodyType, acceptHeader, name } = resolvedExample, resolvedRequestBody = _2.get(resolvedExample, "request.body"), originalRequest, response, responseAuthHelper, requestBodyObj = {}, reqHeaders = _2.clone(request.headers) || [], reqQueryParams = _2.clone(_2.get(request, "params.queryParams", []));
_2.isArray(acceptHeader) && reqHeaders.push(...acceptHeader);
@@ -138829,13 +139083,17 @@ var require_schemaUtils2 = __commonJS({
responses.push(response);
});
});
return { responses, acceptHeader: requestAcceptHeader };
return {
responses,
acceptHeader: requestAcceptHeader,
responseTypes
};
};
module2.exports = {
resolvePostmanRequest: function(context, operationItem, path, method) {
context.schemaCache = context.schemaCache || {};
context.schemaFakerCache = context.schemaFakerCache || {};
let url = resolveUrlForPostmanRequest(path), baseUrlData = resolveBaseUrlForPostmanRequest(operationItem[method]), requestName = resolveNameForPostmanReqeust(context, operationItem[method], url), queryParams = resolveQueryParamsForPostmanRequest(context, operationItem, method), headers = resolveHeadersForPostmanRequest(context, operationItem, method), pathParams = resolvePathParamsForPostmanRequest(context, operationItem, method), { pathVariables, collectionVariables } = filterCollectionAndPathVariables(url, pathParams), requestBody = resolveRequestBodyForPostmanRequest(context, operationItem[method]), request, securitySchema = _2.get(operationItem, [method, "security"]), authHelper = generateAuthForCollectionFromOpenAPI(context.openapi, securitySchema), { alwaysInheritAuthentication } = context.computedOptions;
let url = resolveUrlForPostmanRequest(path), baseUrlData = resolveBaseUrlForPostmanRequest(operationItem[method]), requestName = resolveNameForPostmanReqeust(context, operationItem[method], url), { queryParamTypes, queryParams } = resolveQueryParamsForPostmanRequest(context, operationItem, method), { headerTypes, headers } = resolveHeadersForPostmanRequest(context, operationItem, method), { pathParamTypes, pathParams } = resolvePathParamsForPostmanRequest(context, operationItem, method), { pathVariables, collectionVariables } = filterCollectionAndPathVariables(url, pathParams), requestBody = resolveRequestBodyForPostmanRequest(context, operationItem[method]), requestBodyTypes = requestBody && requestBody.resolvedSchemaTypeObject, request, securitySchema = _2.get(operationItem, [method, "security"]), authHelper = generateAuthForCollectionFromOpenAPI(context.openapi, securitySchema), { alwaysInheritAuthentication } = context.computedOptions, requestIdentifier, requestTypesObject = {};
headers.push(..._2.get(requestBody, "headers", []));
pathVariables.push(...baseUrlData.pathVariables);
collectionVariables.push(...baseUrlData.collectionVariables);
@@ -138853,7 +139111,21 @@ var require_schemaUtils2 = __commonJS({
body: _2.get(requestBody, "body"),
auth: alwaysInheritAuthentication ? void 0 : authHelper
};
const { responses, acceptHeader } = resolveResponseForPostmanRequest(context, operationItem[method], request);
const requestTypes = {
body: JSON.stringify(requestBodyTypes, null, 2),
headers: JSON.stringify(headerTypes, null, 2),
pathParam: JSON.stringify(pathParamTypes, null, 2),
queryParam: JSON.stringify(queryParamTypes, null, 2)
}, {
responses,
acceptHeader,
responseTypes
} = resolveResponseForPostmanRequest(context, operationItem[method], request);
requestIdentifier = method + path;
Object.assign(
requestTypesObject,
{ [requestIdentifier]: { request: requestTypes, response: responseTypes } }
);
if (!_2.isEmpty(acceptHeader)) {
request.headers = _2.concat(request.headers, acceptHeader);
}
@@ -138864,7 +139136,8 @@ var require_schemaUtils2 = __commonJS({
responses
})
},
collectionVariables
collectionVariables,
requestTypesObject
};
},
resolveResponseForPostmanRequest,
@@ -141220,7 +141493,7 @@ var require_libV2 = __commonJS({
convertV2: function(context, cb) {
let collectionTree = generateSkeletonTreeFromOpenAPI(context.openapi, context.computedOptions);
let preOrderTraversal = GraphLib.alg.preorder(collectionTree, "root:collection");
let collection = {};
let collection = {}, extractedTypesObject = {};
_2.forEach(preOrderTraversal, function(nodeIdentified) {
let node = collectionTree.node(nodeIdentified);
switch (node.type) {
@@ -141254,15 +141527,16 @@ var require_libV2 = __commonJS({
break;
}
case "request": {
let request = {}, collectionVariables = [], requestObject = {};
let request = {}, collectionVariables = [], requestObject = {}, requestTypesObject = {};
try {
({ request, collectionVariables } = resolvePostmanRequest(
({ request, collectionVariables, requestTypesObject } = resolvePostmanRequest(
context,
context.openapi.paths[node.meta.path],
node.meta.path,
node.meta.method
));
requestObject = generateRequestItemObject(request);
extractedTypesObject = Object.assign({}, extractedTypesObject, requestTypesObject);
} catch (error) {
console.error(error);
break;
@@ -141337,6 +141611,17 @@ var require_libV2 = __commonJS({
if (!_2.isEmpty(collection.variable)) {
collection.variable = _2.uniqBy(collection.variable, "key");
}
if (context.enableTypeFetching) {
return cb(null, {
result: true,
output: [{
type: "collection",
data: collection
}],
analytics: this.analytics || {},
extractedTypes: extractedTypesObject || {}
});
}
return cb(null, {
result: true,
output: [{
@@ -145118,7 +145403,7 @@ var require_schemapack = __commonJS({
var concreteUtils;
var pathBrowserify = require_path_browserify();
var SchemaPack = class {
constructor(input, options = {}, moduleVersion = MODULE_VERSION.V1) {
constructor(input, options = {}, moduleVersion = MODULE_VERSION.V1, enableTypeFetching = false) {
if (input.type === schemaUtils.MULTI_FILE_API_TYPE_ALLOWED_VALUE && input.data && input.data[0] && input.data[0].path) {
input = schemaUtils.mapDetectRootFilesInputToFolderInput(input);
}
@@ -145136,6 +145421,7 @@ var require_schemapack = __commonJS({
actualStack: 0,
numberOfRequests: 0
};
this.enableTypeFetching = enableTypeFetching;
this.computedOptions = utils.mergeOptions(
// predefined options
_2.keyBy(this.definedOptions, "id"),
@@ -145814,6 +146100,14 @@ var require_openapi_to_postmanv2 = __commonJS({
}
return cb(new UserError(_2.get(schema2, "validationResult.reason", DEFAULT_INVALID_ERROR)));
},
convertV2WithTypes: function(input, options, cb) {
const enableTypeFetching = true;
var schema2 = new SchemaPack(input, options, MODULE_VERSION.V2, enableTypeFetching);
if (schema2.validated) {
return schema2.convertV2(cb);
}
return cb(new UserError(_2.get(schema2, "validationResult.reason", DEFAULT_INVALID_ERROR)));
},
validate: function(input) {
var schema2 = new SchemaPack(input);
return schema2.validationResult;
@@ -145850,7 +146144,8 @@ var require_openapi_to_postmanv2 = __commonJS({
// src/index.ts
var src_exports = {};
__export(src_exports, {
pluginHookImport: () => pluginHookImport2
convertOpenApi: () => convertOpenApi,
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
var import_openapi_to_postmanv2 = __toESM(require_openapi_to_postmanv2());
@@ -145859,7 +146154,7 @@ var import_openapi_to_postmanv2 = __toESM(require_openapi_to_postmanv2());
var POSTMAN_2_1_0_SCHEMA = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json";
var POSTMAN_2_0_0_SCHEMA = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json";
var VALID_SCHEMAS = [POSTMAN_2_0_0_SCHEMA, POSTMAN_2_1_0_SCHEMA];
function pluginHookImport(_ctx, contents) {
function convertPostman(contents) {
const root = parseJSONToRecord(contents);
if (root == null) return;
const info = toRecord(root.info);
@@ -146150,7 +146445,16 @@ function generateId(model) {
}
// src/index.ts
async function pluginHookImport2(ctx, contents) {
var plugin = {
importer: {
name: "OpenAPI",
description: "Import OpenAPI collections",
onImport(_ctx, args) {
return convertOpenApi(args.text);
}
}
};
async function convertOpenApi(contents) {
let postmanCollection;
try {
postmanCollection = await new Promise((resolve, reject) => {
@@ -146164,11 +146468,12 @@ async function pluginHookImport2(ctx, contents) {
} catch (err) {
return void 0;
}
return pluginHookImport(ctx, JSON.stringify(postmanCollection));
return convertPostman(JSON.stringify(postmanCollection));
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
pluginHookImport
convertOpenApi,
plugin
});
/*! Bundled license information:

View File

@@ -7,7 +7,7 @@
"dev": "yaakcli dev ./src/index.js"
},
"dependencies": {
"openapi-to-postmanv2": "^4.23.1",
"openapi-to-postmanv2": "^5.0.0",
"yaml": "^2.4.2"
},
"devDependencies": {

View File

@@ -20,13 +20,23 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
// src/index.ts
var src_exports = {};
__export(src_exports, {
pluginHookImport: () => pluginHookImport
convertPostman: () => convertPostman,
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
var POSTMAN_2_1_0_SCHEMA = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json";
var POSTMAN_2_0_0_SCHEMA = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json";
var VALID_SCHEMAS = [POSTMAN_2_0_0_SCHEMA, POSTMAN_2_1_0_SCHEMA];
function pluginHookImport(_ctx, contents) {
var plugin = {
importer: {
name: "Postman",
description: "Import postman collections",
onImport(_ctx, args) {
return convertPostman(args.text);
}
}
};
function convertPostman(contents) {
const root = parseJSONToRecord(contents);
if (root == null) return;
const info = toRecord(root.info);
@@ -317,5 +327,6 @@ function generateId(model) {
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
pluginHookImport
convertPostman,
plugin
});

View File

@@ -20,10 +20,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
// src/index.ts
var src_exports = {};
__export(src_exports, {
pluginHookImport: () => pluginHookImport
migrateImport: () => migrateImport,
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
function pluginHookImport(_ctx, contents) {
var plugin = {
importer: {
name: "Yaak",
description: "Yaak official format",
onImport(_ctx, args) {
return migrateImport(args.text);
}
}
};
function migrateImport(contents) {
let parsed;
try {
parsed = JSON.parse(contents);
@@ -59,6 +69,12 @@ function pluginHookImport(_ctx, contents) {
}
}
}
for (const environment of parsed.resources.environments ?? []) {
if ("environmentId" in environment) {
environment.base = environment.environmentId == null;
delete environment.environmentId;
}
}
return { resources: parsed.resources };
}
function isJSObject(obj) {
@@ -66,5 +82,6 @@ function isJSObject(obj) {
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
pluginHookImport
migrateImport,
plugin
});

View File

@@ -1,54 +0,0 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
var import_node_fs = __toESM(require("node:fs"));
var plugin = {
templateFunctions: [{
name: "fs.readFile",
args: [{ title: "Select File", type: "file", name: "path", label: "File" }],
async onRender(_ctx, args) {
if (!args.values.path) return null;
try {
return import_node_fs.default.promises.readFile(args.values.path, "utf-8");
} catch (err) {
return null;
}
}
}]
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

@@ -8824,8 +8824,8 @@ var behaviorArg = {
label: "Sending Behavior",
defaultValue: "smart",
options: [
{ name: "When no responses", value: "smart" },
{ name: "Always", value: "always" }
{ label: "When no responses", value: "smart" },
{ label: "Always", value: "always" }
]
};
var requestArg = {

View File

@@ -0,0 +1,9 @@
[package]
name = "yaak-common"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
tauri = { workspace = true }
regex = "1.11.0"

View File

@@ -0,0 +1 @@
pub mod window;

View File

@@ -0,0 +1,31 @@
use regex::Regex;
use tauri::{Runtime, WebviewWindow};
pub trait WorkspaceWindowTrait {
fn workspace_id(&self) -> Option<String>;
fn cookie_jar_id(&self) -> Option<String>;
fn environment_id(&self) -> Option<String>;
}
impl<R: Runtime> WorkspaceWindowTrait for WebviewWindow<R> {
fn workspace_id(&self) -> Option<String> {
let url = self.url().unwrap();
let re = Regex::new(r"/workspaces/(?<id>\w+)").unwrap();
match re.captures(url.as_str()) {
None => None,
Some(captures) => captures.name("id").map(|c| c.as_str().to_string()),
}
}
fn cookie_jar_id(&self) -> Option<String> {
let url = self.url().unwrap();
let mut query_pairs = url.query_pairs();
query_pairs.find(|(k, _v)| k == "cookie_jar_id").map(|(_k, v)| v.to_string())
}
fn environment_id(&self) -> Option<String> {
let url = self.url().unwrap();
let mut query_pairs = url.query_pairs();
query_pairs.find(|(k, _v)| k == "environment_id").map(|(_k, v)| v.to_string())
}
}

View File

@@ -0,0 +1,20 @@
[package]
name = "yaak-crypto"
links = "yaak-crypto"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
base32 = "0.5.1" # For encoding human-readable key
base64 = "0.22.1" # For encoding in the database
chacha20poly1305 = "0.10.1"
keyring = { version = "4.0.0-rc.1" }
log = "0.4.26"
serde = { workspace = true, features = ["derive"] }
tauri = { workspace = true }
thiserror = "2.0.12"
yaak-models = { workspace = true }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -0,0 +1,9 @@
const COMMANDS: &[&str] = &[
"enable_encryption",
"reveal_workspace_key",
"set_workspace_key",
];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();
}

View File

@@ -0,0 +1,13 @@
import { invoke } from '@tauri-apps/api/core';
export function enableEncryption(workspaceId: string) {
return invoke<void>('plugin:yaak-crypto|enable_encryption', { workspaceId });
}
export function revealWorkspaceKey(workspaceId: string) {
return invoke<string>('plugin:yaak-crypto|reveal_workspace_key', { workspaceId });
}
export function setWorkspaceKey(args: { workspaceId: string; key: string }) {
return invoke<void>('plugin:yaak-crypto|set_workspace_key', args);
}

View File

@@ -0,0 +1,6 @@
{
"name": "@yaakapp-internal/crypto",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

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