From a6147f4ce4b75c49760daf7448b713cbf33b12fd Mon Sep 17 00:00:00 2001 From: Shreyas Date: Fri, 28 Feb 2025 00:01:25 +0530 Subject: [PATCH] feat: platform independent core and the new desktop app (#4684) * feat(desktop): init * feat(desktop): external app download and setup * feat(desktop): offload app load to plugin system * perf(desktop): add rdbms facade and caching layer * feat: parallelize signing, shared trust, lru cache * feat: webapp encoder + compressor + hasher server * feat(desktop): app autoupdate with hashed loader * feat(kernel): init `hoppscotch-kernel` * feat(kernel): `io` * feat(kernel): `network` * feat(kernel): `network` - native interceptor * feat(kernel): `network` - interceptor - rest * feat(kernel): `network` - interceptor - graphql * feat(kernel): `network` - interceptor - capabilities * feat(kernel): `network` - interceptor - `FormData` * feat(kernel): `network` - interceptor - `oauth2.0` * feat(kernel): `store` * feat(desktop): dragging, traffic light, plugin workspaces * feat(kernel|wip): `store` * feat(kernel): `network` - capabilities - with active * feat(kernel|wip): `network` - interceptor - `proxy` * feat(kernel|wip): `network` - relay ext * feat(kernel): `network` - interceptor - `proxy` * feat(kernel): `network` - interceptor - decoding * feat(kernel): `network` - interceptor - Kernel Err * feat(kernel): `network` - flow transformation * feat(kernel): `network` - request status * fix(desktop): repositioning traffic lights on fullscreen exit * feat(kernel): `network` - interceptor - `agent` * feat(kernel): `store` - track updates * feat(kernel): `network` - interceptor - extension * feat(kernel): `network` - updates as overrides * feat(interceptor): pre-process request encoding * fix(ui): mismatched extension button size/position * feat(kernel): `network` - interceptor - `browser` * feat(native): common certs componsable * fix(kernel): interceptor selection store and json parse * feat(kernel): `network` - consistent multipart encoding * feat(kernel): `network` - interceptor - `OAuth2.0` * feat(kernel): `network` - interceptor - cookie support * feat(agent): registration list, log-sink, relay * feat(kernel): `network` - interceptor subtitles * feat(kernel): `store` - persist network settings * fix(agent): encrypted ser/de certificate requests * feat(kernel): `kernelInterceptor` spotlight service * fix(kernel): gql introspection edge-case schema * ref: conditionals for migrated components * feat(kernel): `localaccess` capability via relay * feat(kernel): `network` - explicit types and lint * feat(kernel): `store` - isolate host and platform * feat(kernel): `store` - persistence service * fix(infra): whitelisted origins, non-std engines * feat(desktop): impl deep-link callbacks * feat(kernel): `auth` * feat(kernel): `io` - event listeners * feat(kernel): platform migration * fix: dep `vue` import on Win 11 Fixes `error TS2305: Module '"vue"' has no exported member 'VueConstructor'.` arising from `splitpane` dependency. * fix(webapp-server): platform independent res paths * feat(desktop): auth and emit via embedded server * feat(platform): host, csp and bundle compatibility - Bundle name format for using as host - Windows UI handler HWND casting and version detection - CSP headers type handling in URI protocol - Protocol whitelist in env config * feat(desktop|wip): login flow with `auth-tokens` feat(desktop|wip): typesafe auth * feat(backend): `auth` token flow, gql/websocket feat(desktop): working auth for gql feat: gql client with refresh token * feat(backend): `auth` token flow, authorization bearer * fix(gen): qualifier clash when invalidating cache * feat(common): coordinated initialization service * fix(desktop): appload persistence in data json * feat(desktop|wip): desktop icons and updater * fix: typos in readme docs * fix: docker ignore copying on windows * fix: update `.lock` file after rebase * fix: `persistenceService` setup in tests * fix: remove old console logs * fix: console error on invalid schema Show console error if default value is used when loading invalid data from local storage * fix(test): `PersistenceService` methods * fix(test): `PersistenceService` rest tab state * fix(test): `PersistenceService` gql tab state * fix(test): `PersistenceService` global env * fix(test): `PersistenceService` mqtt request * fix(test): `PersistenceService` sse request * fix(test): `PersistenceService` socketio request * fix(test): `PersistenceService` websocket request * fix(test): `PersistenceService` secret environment * fix(test): `PersistenceService` selected env * fix(test): `PersistenceService` collections * fix(test): `PersistenceService` environments * fix(test): `PersistenceService` history * fix(test): `PersistenceService` settings * fix(test): `PersistenceService` migrations * fix(test): `InspectionService` request inspector * feat(desktop): button to clear bundle/key cache This is useful when there are partial updates to the web app or bundle gen server which haven't been correctly propagated when the app bundle was downloaded. If the user were to change the self host instance without updating the desktop app; which is possible albeit rarely under very certain circumstances, desktop app will refuse to load the bundle, this is because the desktop app cannot differentiate between partial updates vs incorrect bundle being hosted since both will fail verification. The button lets the user decide what should be the appropriate action, clear the bundle and trust the hosted app or make sure the app is built and hosted correctly. * fix(desktop): enforce one version per instance This was part of a leftover scaffolding from development. * fix(desktop): bundle url not stored after download * fix(desktop): stalling progress on updates * fix(backend): helper to parse cookie into kv-pairs * feat(desktop): launch session on working endpoints * fix(common): preserve `auth` structure and default * fix: loading native networking with kernel mode * fix: fallback for unhandled response error * fix: `urlencoded` content request processing * feat: `interceptor` - error mapping for `browser` * fix: backwards compatibility for `digest` auth * fix: platform check for `initializationService` * fix: `interceptor` - analytics `strategy` resolution * fix: `interceptor` - check for `cookies` component * fix: enable digest auth support for `native` * test: `interceptor` - kernel interceptor * fix(relay): `grantType` casing for OAuth2.0 * test(wip): kernel transformers * fix(relay): auth headers discarding others * fix(desktop): http version deserialization * fix(common): `grantType` extractor, auth processor * fix: `PersistenceService` - parsing edge cases * fix(infra): post rebase fixup * fix(web): component structure and lint * fix(desktop): cohesive splash opener, scroll url section * fix: explicit auto auth and docs on url auth * fix(relay): special chars failing proxy auth * fix: finer cert control setting option * fix: post-rebase fixup * feat(appload): ability to vendor pre-built bytes * fix: avoid copying over `target` dir in containers * fix: auth key missing in capability set * fix(desktop): relax `refresh_token` requirement This is to support Firebase token * fix(desktop): normalization for Windows WebView * feat(desktop): instance switcher and vendored app * fix(desktop): merge artifacts and conflicts * feat(desktop): instance switcher improvements * fix: derive instance name from normalized name * fix: pkg links, lints and UI edge cases * feat(desktop): restore window state after relaunch * fix(desktop): distinguish header for cloud/default * fix: instance switcher in web mode * fix: close dropdown on new instance modal * fix: whitelist vendored app origin * feat(desktop): platform parity - `collections` * fix: history entries population desync * fix(desktop): check for history storage status * fix(desktop): safe parse `globalEnv` * feat(desktop): platform parity - `environment` * fix: use settings store for proxy url * fix: lint, unused imports * fix: proxy input enabled for other interceptors * feat: reverse proxy for desktop app server * fix: duplicate entries after connecting to sh * fix: specify instance org qualified * fix: remove debugging logs * feat(desktop): enable `devtools` in release builds * fix(desktop): prepend protocol validation edgecase * feat(desktop): clear cache on removing instance * fix: better response toast message * fix: avoid reverse proxy for webapp server * fix(desktop): ignore subpath in instance name * feat: switcher ui/ux improvements * feat: more switcher ui/ux improvements * feat(server): specify bundle version at build time * fix(desktop): missing migration as rebase artifact * fix: minor switcher ui/ux improvement * fix: rebase artifacts * fix: consolidated toast on success * fix: missing i18n strings * fix(desktop): handle drag and drop fe side * feat: confirmation modal on instance removal * chore: minor UI update * chore: minor UI changes * fix: gql connection partial refactor * fix: resolve merge artifacts * chore: prod lint * feat(desktop): better desktop app update ux * fix: broken gql connection.ts --------- Co-authored-by: nivedin Co-authored-by: Andrew Bastin --- .dockerignore | 33 + .env.example | 9 +- .envrc | 3 + .gitignore | 10 + aio-subpath-access.Caddyfile | 5 + aio_run.mjs | 7 + devenv.lock | 140 + devenv.nix | 189 + .../devenv.yaml => devenv.yaml | 0 docker-compose.yml | 93 +- .../codemirror-lang-graphql/tsconfig.json | 1 + packages/hoppscotch-agent/.envrc | 3 - packages/hoppscotch-agent/package.json | 2 + .../hoppscotch-agent/src-tauri/Cargo.lock | 1645 ++-- .../hoppscotch-agent/src-tauri/Cargo.toml | 13 +- .../src-tauri/capabilities/default.json | 1 + .../hoppscotch-agent/src-tauri/src/command.rs | 40 + .../src-tauri/src/controller.rs | 367 +- .../hoppscotch-agent/src-tauri/src/dialog.rs | 8 +- .../hoppscotch-agent/src-tauri/src/error.rs | 16 +- .../hoppscotch-agent/src-tauri/src/global.rs | 1 + .../hoppscotch-agent/src-tauri/src/lib.rs | 228 +- .../hoppscotch-agent/src-tauri/src/main.rs | 12 + .../hoppscotch-agent/src-tauri/src/model.rs | 63 + .../hoppscotch-agent/src-tauri/src/route.rs | 12 +- .../hoppscotch-agent/src-tauri/src/server.rs | 36 +- .../hoppscotch-agent/src-tauri/src/state.rs | 214 +- .../hoppscotch-agent/src-tauri/src/tray.rs | 33 +- .../hoppscotch-agent/src-tauri/src/util.rs | 10 +- .../src-tauri/src/webview/mod.rs | 4 +- packages/hoppscotch-agent/src/App.vue | 194 +- packages/hoppscotch-agent/src/pages/otp.vue | 80 + .../src/pages/registrations.vue | 57 + packages/hoppscotch-backend/src/app.module.ts | 37 +- .../src/auth/auth.controller.ts | 20 + .../hoppscotch-backend/src/auth/helper.ts | 98 +- .../src/auth/strategies/jwt.strategy.ts | 84 +- .../src/auth/strategies/rt-jwt.strategy.ts | 1 + packages/hoppscotch-backend/src/errors.ts | 12 + packages/hoppscotch-common/locales/en.json | 133 +- packages/hoppscotch-common/package.json | 4 + .../hoppscotch-common/src/components.d.ts | 5 + .../src/components/app/ActionHandler.vue | 9 + .../src/components/app/Footer.vue | 2 +- .../src/components/app/Header.vue | 70 +- .../src/components/app/KernelInterceptor.vue | 97 + .../src/components/app/PaneLayout.vue | 25 +- .../src/components/app/WhatsNewDialog.vue | 2 +- .../src/components/app/spotlight/index.vue | 8 +- .../components/collections/ImportExport.vue | 2 +- .../src/components/collections/Properties.vue | 18 +- .../collections/graphql/ImportExport.vue | 2 +- .../components/collections/graphql/index.vue | 11 +- .../src/components/collections/index.vue | 13 +- .../src/components/cookies/AllModal.vue | 11 +- .../components/environments/ImportExport.vue | 2 +- .../src/components/firebase/Login.vue | 4 +- .../src/components/graphql/Request.vue | 6 +- .../src/components/graphql/RequestOptions.vue | 6 +- .../src/components/graphql/Sidebar.vue | 2 +- .../src/components/http/Request.vue | 6 +- .../src/components/http/ResponseMeta.vue | 20 + .../components/http/authorization/OAuth2.vue | 2 +- .../ImportExportSteps/UrlImport.vue | 35 +- .../src/components/instance/Switcher.vue | 380 + .../lenses/renderers/HTMLLensRenderer.vue | 6 +- .../src/components/settings/Agent.vue | 699 +- .../src/components/settings/AgentSubtitle.vue | 179 + .../src/components/settings/Extension.vue | 42 +- .../components/settings/ExtensionSubtitle.vue | 64 + .../src/components/settings/Native.vue | 642 ++ .../src/components/settings/Proxy.vue | 65 +- .../src/composables/lens-actions.ts | 2 +- .../src/composables/picker.ts | 138 + .../src/composables/whats-new.ts | 12 +- .../src/helpers/RequestRunner.ts | 5 +- .../hoppscotch-common/src/helpers/actions.ts | 1 + .../src/helpers/auth/digest.ts | 12 +- .../src/helpers/functional/domain-settings.ts | 228 + .../src/helpers/functional/filter-active.ts | 13 + .../src/helpers/functional/json.ts | 25 +- .../src/helpers/functional/parse.ts | 7 + .../src/helpers/functional/preprocess.ts | 69 + .../src/helpers/graphql/connection.ts | 245 +- .../src/helpers/import-export/export/index.ts | 2 +- .../import-export/export/testResults.ts | 2 +- .../import/import-sources/GistSource.ts | 32 +- .../helpers/kernel/__tests__/kernel.spec.ts | 353 + .../src/helpers/kernel/common/auth.ts | 286 + .../src/helpers/kernel/common/content.ts | 192 + .../src/helpers/kernel/common/index.ts | 2 + .../src/helpers/kernel/gql/request.ts | 48 + .../src/helpers/kernel/gql/response.ts | 89 + .../src/helpers/kernel/rest/index.ts | 2 + .../src/helpers/kernel/rest/request.ts | 40 + .../src/helpers/kernel/rest/response.ts | 54 + .../hoppscotch-common/src/helpers/network.ts | 129 +- .../hoppscotch-common/src/helpers/oauth.ts | 340 +- .../src/helpers/teams/TeamsSearch.service.ts | 7 +- .../src/helpers/types/HoppRESTResponse.ts | 106 +- packages/hoppscotch-common/src/index.ts | 34 +- .../hoppscotch-common/src/kernel/index.ts | 13 + packages/hoppscotch-common/src/kernel/io.ts | 34 + .../hoppscotch-common/src/kernel/relay.ts | 26 + .../hoppscotch-common/src/kernel/store.ts | 46 + .../hoppscotch-common/src/layouts/default.vue | 12 +- .../hoppscotch-common/src/modules/i18n.ts | 12 +- .../hoppscotch-common/src/modules/index.ts | 9 +- .../src/modules/interceptors.ts | 1 + .../src/modules/kernel-interceptors.ts | 68 + .../src/newstore/MQTTSession.ts | 2 +- .../src/newstore/settings.ts | 2 + .../hoppscotch-common/src/pages/index.vue | 8 +- .../hoppscotch-common/src/pages/oauth.vue | 4 +- .../hoppscotch-common/src/pages/settings.vue | 66 +- .../hoppscotch-common/src/platform/auth.ts | 2 +- .../hoppscotch-common/src/platform/index.ts | 15 +- .../src/platform/kernel-interceptors.ts | 19 + .../src/platform/kernel-io.ts | 91 + .../platform/std/interceptors/agent/index.ts | 2 - .../src/platform/std/interceptors/browser.ts | 77 - .../std/kernel-interceptors/agent/index.ts | 179 + .../std/kernel-interceptors/agent/store.ts | 367 + .../std/kernel-interceptors/browser/index.ts | 149 + .../std/kernel-interceptors/browser/store.ts | 57 + .../kernel-interceptors/extension/index.ts | 139 + .../kernel-interceptors/extension/store.ts | 134 + .../std/kernel-interceptors/native/index.ts | 182 + .../std/kernel-interceptors/native/store.ts | 188 + .../std/kernel-interceptors/proxy/index.ts | 300 + .../std/kernel-interceptors/proxy/store.ts | 104 + .../src/platform/std/kernel-io.ts | 11 + .../kernel-interceptor.service.spec.ts | 201 + .../src/services/initialization.service.ts | 173 + .../inspection/__tests__/index.spec.ts | 68 +- .../__tests__/header.inspector.spec.ts | 106 - .../__tests__/request.inspector.spec.ts | 290 + .../inspectors/authorization.inspector.ts | 106 - .../inspection/inspectors/header.inspector.ts | 85 - .../inspectors/interceptors.inspector.ts | 73 - .../inspectors/request.inspector.ts | 151 + .../src/services/instance-switcher.service.ts | 448 ++ .../services/kernel-interceptor.service.ts | 173 + .../src/services/oauth/flows/authCode.ts | 55 +- .../services/oauth/flows/clientCredentials.ts | 100 +- .../src/services/oauth/flows/implicit.ts | 4 +- .../src/services/oauth/flows/password.ts | 77 +- .../src/services/oauth/oauth.service.ts | 34 +- .../persistence/__tests__/__mocks__/index.ts | 14 +- .../persistence/__tests__/index.spec.ts | 857 +- .../src/services/persistence/index.ts | 1383 ++-- .../persistence/validation-schemas/index.ts | 4 + .../spotlight/searchers/general.searcher.ts | 2 +- .../searchers/kernel-interceptor.searcher.ts | 123 + .../src/services/tab/graphql.ts | 17 + .../src/services/tab/rest.ts | 17 + .../hoppscotch-common/src/services/tab/tab.ts | 9 + .../hoppscotch-common/src/types/kernel.d.ts | 9 + packages/hoppscotch-common/tsconfig.json | 3 +- packages/hoppscotch-desktop/.gitignore | 36 + packages/hoppscotch-desktop/README.md | 9 + .../crates/webapp-bundler/.gitignore | 21 + .../crates/webapp-bundler/Cargo.lock | 1169 +++ .../crates/webapp-bundler/Cargo.toml | 20 + .../crates/webapp-bundler/src/main.rs | 221 + packages/hoppscotch-desktop/index.html | 14 + packages/hoppscotch-desktop/package.json | 44 + .../plugin-workspace/relay/.envrc | 3 + .../plugin-workspace/relay/.gitignore | 19 + .../plugin-workspace/relay/Cargo.lock | 919 +++ .../plugin-workspace/relay/Cargo.toml | 31 + .../plugin-workspace/relay/LICENSE.md | 21 + .../plugin-workspace/relay/README.md | 101 + .../plugin-workspace/relay}/devenv.lock | 24 +- .../plugin-workspace/relay/devenv.nix | 72 + .../plugin-workspace/relay}/devenv.yaml | 0 .../plugin-workspace/relay/src/auth.rs | 268 + .../plugin-workspace/relay/src/content.rs | 294 + .../plugin-workspace/relay/src/error.rs | 70 + .../plugin-workspace/relay/src/header.rs | 66 + .../plugin-workspace/relay/src/interop.rs | 299 + .../plugin-workspace/relay/src/lib.rs | 14 + .../plugin-workspace/relay/src/relay.rs | 175 + .../plugin-workspace/relay/src/request.rs | 145 + .../plugin-workspace/relay/src/response.rs | 149 + .../plugin-workspace/relay/src/security.rs | 173 + .../plugin-workspace/relay/src/transfer.rs | 101 + .../plugin-workspace/relay/src/util.rs | 15 + .../tauri-plugin-appload/.envrc | 3 + .../tauri-plugin-appload/.gitignore | 22 + .../tauri-plugin-appload/Cargo.lock | 5456 +++++++++++++ .../tauri-plugin-appload/Cargo.toml | 52 + .../tauri-plugin-appload/LICENSE.md | 21 + .../tauri-plugin-appload/README.md | 105 + .../tauri-plugin-appload/build.rs | 8 + .../tauri-plugin-appload/devenv.lock | 153 + .../tauri-plugin-appload}/devenv.nix | 30 +- .../tauri-plugin-appload/devenv.yaml | 23 + .../tauri-plugin-appload/dist-js/index.cjs | 21 + .../tauri-plugin-appload/dist-js/index.d.ts | 37 + .../dist-js/index.d.ts.map | 1 + .../tauri-plugin-appload/dist-js/index.js | 16 + .../examples/tauri-app/.gitignore | 24 + .../tauri-app/.vscode/extensions.json | 7 + .../examples/tauri-app/README.md | 8 + .../examples/tauri-app/index.html | 14 + .../examples/tauri-app/jsconfig.json | 34 + .../examples/tauri-app/package.json | 22 + .../examples/tauri-app/public/svelte.svg | 1 + .../examples/tauri-app/public/tauri.svg | 6 + .../examples/tauri-app/public/vite.svg | 1 + .../examples/tauri-app/src-tauri/.gitignore | 4 + .../examples/tauri-app/src-tauri/Cargo.toml | 23 + .../examples/tauri-app/src-tauri/build.rs | 3 + .../src-tauri/capabilities/default.json | 12 + .../tauri-app/src-tauri/icons/128x128.png | Bin 0 -> 11059 bytes .../tauri-app/src-tauri/icons/128x128@2x.png | Bin 0 -> 23137 bytes .../tauri-app/src-tauri/icons/32x32.png | Bin 0 -> 2225 bytes .../tauri-app/src-tauri/icons/icon.icns | Bin 0 -> 277003 bytes .../tauri-app/src-tauri/icons/icon.ico | Bin 0 -> 37710 bytes .../tauri-app/src-tauri/icons/icon.png | Bin 0 -> 49979 bytes .../examples/tauri-app/src-tauri/src/lib.rs | 14 + .../examples/tauri-app/src-tauri/src/main.rs | 6 + .../tauri-app/src-tauri/tauri.conf.json | 37 + .../examples/tauri-app/src/App.svelte | 54 + .../examples/tauri-app/src/lib/Greet.svelte | 22 + .../examples/tauri-app/src/main.js | 8 + .../examples/tauri-app/src/style.css | 102 + .../examples/tauri-app/src/vite-env.d.ts | 2 + .../examples/tauri-app/vite.config.js | 24 + .../tauri-plugin-appload/guest-js/index.ts | 56 + .../tauri-plugin-appload/package.json | 33 + .../autogenerated/commands/clear.toml | 13 + .../autogenerated/commands/download.toml | 13 + .../autogenerated/commands/load.toml | 13 + .../autogenerated/commands/ping.toml | 13 + .../autogenerated/commands/remove.toml | 13 + .../permissions/autogenerated/reference.md | 148 + .../permissions/default.toml | 3 + .../permissions/schemas/schema.json | 355 + .../tauri-plugin-appload/pnpm-lock.yaml | 325 + .../tauri-plugin-appload/rollup.config.js | 31 + .../tauri-plugin-appload/src/api/client.rs | 126 + .../tauri-plugin-appload/src/api/error.rs | 50 + .../tauri-plugin-appload/src/api/mod.rs | 8 + .../tauri-plugin-appload/src/api/model.rs | 9 + .../tauri-plugin-appload/src/bundle/error.rs | 27 + .../tauri-plugin-appload/src/bundle/loader.rs | 184 + .../tauri-plugin-appload/src/bundle/mod.rs | 7 + .../src/bundle/verified.rs | 175 + .../tauri-plugin-appload/src/cache/error.rs | 27 + .../tauri-plugin-appload/src/cache/manager.rs | 80 + .../tauri-plugin-appload/src/cache/mod.rs | 12 + .../tauri-plugin-appload/src/cache/policy.rs | 36 + .../tauri-plugin-appload/src/cache/store.rs | 180 + .../tauri-plugin-appload/src/commands.rs | 249 + .../tauri-plugin-appload/src/config/mod.rs | 5 + .../tauri-plugin-appload/src/config/model.rs | 74 + .../tauri-plugin-appload/src/desktop.rs | 36 + .../tauri-plugin-appload/src/envvar.rs | 8 + .../tauri-plugin-appload/src/error.rs | 40 + .../tauri-plugin-appload/src/global.rs | 4 + .../tauri-plugin-appload/src/kernel.js | 4 + .../tauri-plugin-appload/src/lib.rs | 167 + .../tauri-plugin-appload/src/mobile.rs | 44 + .../tauri-plugin-appload/src/models.rs | 215 + .../tauri-plugin-appload/src/storage/error.rs | 36 + .../src/storage/layout.rs | 35 + .../src/storage/manager.rs | 176 + .../tauri-plugin-appload/src/storage/mod.rs | 9 + .../src/storage/registry.rs | 68 + .../tauri-plugin-appload/src/ui/macos/mod.rs | 1 + .../src/ui/macos/posit.rs | 429 + .../tauri-plugin-appload/src/ui/mod.rs | 4 + .../src/ui/windows/mod.rs | 1 + .../src/ui/windows/posit.rs | 118 + .../tauri-plugin-appload/src/uri/error.rs | 12 + .../tauri-plugin-appload/src/uri/handler.rs | 102 + .../tauri-plugin-appload/src/uri/mod.rs | 4 + .../src/vendor/builder.rs | 34 + .../tauri-plugin-appload/src/vendor/config.rs | 70 + .../tauri-plugin-appload/src/vendor/error.rs | 27 + .../tauri-plugin-appload/src/vendor/mod.rs | 7 + .../src/verification/bundle.rs | 32 + .../src/verification/error.rs | 40 + .../src/verification/file.rs | 52 + .../src/verification/key.rs | 57 + .../src/verification/mod.rs | 11 + .../tauri-plugin-appload/tsconfig.json | 17 + .../tauri-plugin-relay/.envrc | 3 + .../tauri-plugin-relay/.gitignore | 23 + .../tauri-plugin-relay/Cargo.lock | 4801 ++++++++++++ .../tauri-plugin-relay/Cargo.toml | 19 + .../tauri-plugin-relay/LICENSE.md | 21 + .../tauri-plugin-relay/README.md | 129 + .../tauri-plugin-relay/build.rs | 8 + .../tauri-plugin-relay}/devenv.lock | 24 +- .../tauri-plugin-relay/devenv.nix | 90 + .../tauri-plugin-relay/devenv.yaml | 23 + .../tauri-plugin-relay/dist-js/index.cjs | 27 + .../tauri-plugin-relay/dist-js/index.d.ts | 209 + .../tauri-plugin-relay/dist-js/index.d.ts.map | 1 + .../tauri-plugin-relay/dist-js/index.js | 24 + .../tauri-plugin-relay/guest-js/index.ts | 259 + .../tauri-plugin-relay/package.json | 33 + .../autogenerated/commands/cancel.toml | 13 + .../autogenerated/commands/execute.toml | 13 + .../autogenerated/commands/run.toml | 13 + .../autogenerated/commands/subscribe.toml | 13 + .../permissions/autogenerated/reference.md | 120 + .../permissions/default.toml | 3 + .../permissions/schemas/schema.json | 345 + .../tauri-plugin-relay/pnpm-lock.yaml | 325 + .../tauri-plugin-relay/rollup.config.js | 31 + .../tauri-plugin-relay/src/commands.rs | 38 + .../tauri-plugin-relay/src/desktop.rs | 42 + .../tauri-plugin-relay/src/error.rs | 24 + .../tauri-plugin-relay/src/lib.rs | 66 + .../tauri-plugin-relay/src/mobile.rs | 48 + .../tauri-plugin-relay/src/models.rs | 17 + .../tauri-plugin-relay/tsconfig.json | 17 + packages/hoppscotch-desktop/postcss.config.js | 6 + .../hoppscotch-desktop/public/favicon.ico | Bin 0 -> 15086 bytes .../public/images/add_group.svg | 37 + .../hoppscotch-desktop/public/images/pack.svg | 39 + .../public/images/youre_lost.svg | 76 + packages/hoppscotch-desktop/public/logo.svg | 50 + packages/hoppscotch-desktop/public/tauri.svg | 6 + packages/hoppscotch-desktop/public/vite.svg | 1 + .../hoppscotch-desktop/src-tauri/.gitignore | 7 + .../hoppscotch-desktop/src-tauri/Cargo.lock | 6924 +++++++++++++++++ .../hoppscotch-desktop/src-tauri/Cargo.toml | 42 + .../hoppscotch-desktop/src-tauri/build.rs | 3 + .../src-tauri/capabilities/default.json | 23 + .../src-tauri/capabilities/desktop.json | 11 + .../src-tauri/icons/128x128.png | Bin 0 -> 10503 bytes .../src-tauri/icons/128x128@2x.png | Bin 0 -> 28136 bytes .../src-tauri/icons/32x32.png | Bin 0 -> 1699 bytes .../src-tauri/icons/Square107x107Logo.png | Bin 0 -> 8175 bytes .../src-tauri/icons/Square142x142Logo.png | Bin 0 -> 12030 bytes .../src-tauri/icons/Square150x150Logo.png | Bin 0 -> 12915 bytes .../src-tauri/icons/Square284x284Logo.png | Bin 0 -> 33374 bytes .../src-tauri/icons/Square30x30Logo.png | Bin 0 -> 1563 bytes .../src-tauri/icons/Square310x310Logo.png | Bin 0 -> 39115 bytes .../src-tauri/icons/Square44x44Logo.png | Bin 0 -> 2513 bytes .../src-tauri/icons/Square71x71Logo.png | Bin 0 -> 4940 bytes .../src-tauri/icons/Square89x89Logo.png | Bin 0 -> 6504 bytes .../src-tauri/icons/StoreLogo.png | Bin 0 -> 3057 bytes .../src-tauri/icons/icon.icns | Bin 0 -> 730299 bytes .../src-tauri/icons/icon.ico | Bin 0 -> 40551 bytes .../src-tauri/icons/icon.png | Bin 0 -> 103397 bytes .../hoppscotch-desktop/src-tauri/src/lib.rs | 85 + .../hoppscotch-desktop/src-tauri/src/main.rs | 18 + .../src-tauri/src/server.rs | 94 + .../src-tauri/src/updater.rs | 71 + .../src-tauri/tauri.conf.json | 64 + packages/hoppscotch-desktop/src/App.vue | 24 + .../src/assets/scss/styles.scss | 103 + .../src/assets/scss/tailwind.scss | 3 + .../hoppscotch-desktop/src/assets/vue.svg | 1 + .../hoppscotch-desktop/src/components.d.ts | 16 + .../src/components/layout/LayoutHeader.vue | 35 + .../src/components/layout/LayoutSidebar.vue | 63 + packages/hoppscotch-desktop/src/main.ts | 14 + packages/hoppscotch-desktop/src/router.ts | 15 + .../hoppscotch-desktop/src/types/index.ts | 10 + .../hoppscotch-desktop/src/views/Home.vue | 236 + packages/hoppscotch-desktop/src/vite-env.d.ts | 7 + .../hoppscotch-desktop/tailwind.config.ts | 7 + packages/hoppscotch-desktop/tsconfig.json | 42 + .../hoppscotch-desktop/tsconfig.node.json | 10 + packages/hoppscotch-desktop/vite.config.ts | 80 + packages/hoppscotch-kernel/README.md | 103 + packages/hoppscotch-kernel/package.json | 62 + packages/hoppscotch-kernel/src/index.ts | 121 + .../src/io/impl/desktop/index.ts | 5 + .../src/io/impl/desktop/v/1.ts | 52 + .../src/io/impl/web/index.ts | 5 + .../hoppscotch-kernel/src/io/impl/web/v/1.ts | 83 + packages/hoppscotch-kernel/src/io/index.ts | 18 + packages/hoppscotch-kernel/src/io/v/1.ts | 70 + .../src/relay/impl/desktop/index.ts | 5 + .../src/relay/impl/desktop/v/1.ts | 195 + .../src/relay/impl/web/index.ts | 5 + .../src/relay/impl/web/v/1.ts | 270 + packages/hoppscotch-kernel/src/relay/index.ts | 11 + packages/hoppscotch-kernel/src/relay/v/1.ts | 594 ++ .../src/store/impl/desktop/index.ts | 5 + .../src/store/impl/desktop/v/1.ts | 314 + .../src/store/impl/web/index.ts | 5 + .../src/store/impl/web/v/1.ts | 259 + packages/hoppscotch-kernel/src/store/index.ts | 11 + packages/hoppscotch-kernel/src/store/v/1.ts | 118 + .../hoppscotch-kernel/src/type/versioning.ts | 10 + .../hoppscotch-kernel/src/util/capability.ts | 7 + packages/hoppscotch-kernel/tsconfig.base.json | 22 + packages/hoppscotch-kernel/tsconfig.decl.json | 11 + packages/hoppscotch-kernel/tsconfig.json | 7 + packages/hoppscotch-kernel/vite.config.d.ts | 2 + packages/hoppscotch-kernel/vite.config.ts | 36 + packages/hoppscotch-relay/.envrc | 3 - packages/hoppscotch-relay/devenv.nix | 54 - .../hoppscotch-selfhost-desktop/package.json | 2 +- .../src/platform/interceptors/native/index.ts | 2 - packages/hoppscotch-selfhost-web/package.json | 7 + .../src/components/Login.vue | 112 + packages/hoppscotch-selfhost-web/src/main.ts | 188 +- .../src/pages/device-login.vue | 142 + .../auth/{auth.api.ts => desktop/api.ts} | 2 +- .../src/platform/auth/desktop/index.ts | 513 ++ .../src/platform/auth/web/api.ts | 45 + .../auth/{auth.platform.ts => web/index.ts} | 38 +- .../{collections.api.ts => desktop/api.ts} | 114 +- .../{ => desktop}/gqlCollections.sync.ts | 12 +- .../src/platform/collections/desktop/index.ts | 1076 +++ .../{collections.sync.ts => desktop/sync.ts} | 8 +- .../src/platform/collections/web/api.ts | 348 + .../collections/web/gqlCollections.sync.ts | 331 + .../{collections.platform.ts => web/index.ts} | 10 +- .../src/platform/collections/web/sync.ts | 608 ++ .../{environments.api.ts => desktop/api.ts} | 2 +- .../index.ts} | 10 +- .../{environments.sync.ts => desktop/sync.ts} | 8 +- .../src/platform/environments/web/api.ts | 117 + .../src/platform/environments/web/index.ts | 213 + .../src/platform/environments/web/sync.ts | 131 + .../{history.api.ts => desktop/api.ts} | 2 +- .../{history.platform.ts => desktop/index.ts} | 10 +- .../{history.sync.ts => desktop/sync.ts} | 10 +- .../src/platform/history/web/api.ts | 127 + .../src/platform/history/web/index.ts | 338 + .../src/platform/history/web/sync.ts | 111 + .../src/platform/infra/infra.platform.ts | 2 +- .../{settings.api.ts => desktop/api.ts} | 2 +- .../index.ts} | 6 +- .../{settings.sync.ts => desktop/sync.ts} | 6 +- .../src/platform/settings/web/api.ts | 51 + .../src/platform/settings/web/index.ts | 88 + .../src/platform/settings/web/sync.ts | 30 + .../platform/tabState/tabState.platform.ts | 38 - .../hoppscotch-selfhost-web/tsconfig.json | 2 + .../hoppscotch-selfhost-web/vite.config.ts | 4 +- .../webapp-server/.gitignore | 21 + .../webapp-server/Cargo.lock | 1925 +++++ .../webapp-server/Cargo.toml | 26 + .../webapp-server/README.md | 33 + .../webapp-server/src/api/error.rs | 46 + .../webapp-server/src/api/handler.rs | 83 + .../webapp-server/src/api/mod.rs | 46 + .../webapp-server/src/api/model.rs | 46 + .../webapp-server/src/bundle/builder.rs | 109 + .../webapp-server/src/bundle/error.rs | 36 + .../webapp-server/src/bundle/manager.rs | 68 + .../webapp-server/src/bundle/mod.rs | 8 + .../webapp-server/src/bundle/model.rs | 58 + .../webapp-server/src/config.rs | 88 + .../webapp-server/src/error.rs | 125 + .../webapp-server/src/main.rs | 69 + .../webapp-server/src/model.rs | 42 + .../webapp-server/src/signing/error.rs | 51 + .../webapp-server/src/signing/key.rs | 37 + .../webapp-server/src/signing/mod.rs | 5 + pnpm-lock.yaml | 2113 +++-- prod.Dockerfile | 25 +- tailwind.config.ts | 1 + 465 files changed, 54061 insertions(+), 4097 deletions(-) create mode 100644 .envrc create mode 100644 devenv.lock create mode 100644 devenv.nix rename packages/hoppscotch-agent/devenv.yaml => devenv.yaml (100%) delete mode 100644 packages/hoppscotch-agent/.envrc create mode 100644 packages/hoppscotch-agent/src-tauri/src/command.rs create mode 100644 packages/hoppscotch-agent/src/pages/otp.vue create mode 100644 packages/hoppscotch-agent/src/pages/registrations.vue create mode 100644 packages/hoppscotch-common/src/components/app/KernelInterceptor.vue create mode 100644 packages/hoppscotch-common/src/components/instance/Switcher.vue create mode 100644 packages/hoppscotch-common/src/components/settings/AgentSubtitle.vue create mode 100644 packages/hoppscotch-common/src/components/settings/ExtensionSubtitle.vue create mode 100644 packages/hoppscotch-common/src/components/settings/Native.vue create mode 100644 packages/hoppscotch-common/src/composables/picker.ts create mode 100644 packages/hoppscotch-common/src/helpers/functional/domain-settings.ts create mode 100644 packages/hoppscotch-common/src/helpers/functional/filter-active.ts create mode 100644 packages/hoppscotch-common/src/helpers/functional/parse.ts create mode 100644 packages/hoppscotch-common/src/helpers/functional/preprocess.ts create mode 100644 packages/hoppscotch-common/src/helpers/kernel/__tests__/kernel.spec.ts create mode 100644 packages/hoppscotch-common/src/helpers/kernel/common/auth.ts create mode 100644 packages/hoppscotch-common/src/helpers/kernel/common/content.ts create mode 100644 packages/hoppscotch-common/src/helpers/kernel/common/index.ts create mode 100644 packages/hoppscotch-common/src/helpers/kernel/gql/request.ts create mode 100644 packages/hoppscotch-common/src/helpers/kernel/gql/response.ts create mode 100644 packages/hoppscotch-common/src/helpers/kernel/rest/index.ts create mode 100644 packages/hoppscotch-common/src/helpers/kernel/rest/request.ts create mode 100644 packages/hoppscotch-common/src/helpers/kernel/rest/response.ts create mode 100644 packages/hoppscotch-common/src/kernel/index.ts create mode 100644 packages/hoppscotch-common/src/kernel/io.ts create mode 100644 packages/hoppscotch-common/src/kernel/relay.ts create mode 100644 packages/hoppscotch-common/src/kernel/store.ts create mode 100644 packages/hoppscotch-common/src/modules/kernel-interceptors.ts create mode 100644 packages/hoppscotch-common/src/platform/kernel-interceptors.ts create mode 100644 packages/hoppscotch-common/src/platform/kernel-io.ts delete mode 100644 packages/hoppscotch-common/src/platform/std/interceptors/browser.ts create mode 100644 packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/index.ts create mode 100644 packages/hoppscotch-common/src/platform/std/kernel-interceptors/agent/store.ts create mode 100644 packages/hoppscotch-common/src/platform/std/kernel-interceptors/browser/index.ts create mode 100644 packages/hoppscotch-common/src/platform/std/kernel-interceptors/browser/store.ts create mode 100644 packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/index.ts create mode 100644 packages/hoppscotch-common/src/platform/std/kernel-interceptors/extension/store.ts create mode 100644 packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/index.ts create mode 100644 packages/hoppscotch-common/src/platform/std/kernel-interceptors/native/store.ts create mode 100644 packages/hoppscotch-common/src/platform/std/kernel-interceptors/proxy/index.ts create mode 100644 packages/hoppscotch-common/src/platform/std/kernel-interceptors/proxy/store.ts create mode 100644 packages/hoppscotch-common/src/platform/std/kernel-io.ts create mode 100644 packages/hoppscotch-common/src/services/__tests__/kernel-interceptor.service.spec.ts create mode 100644 packages/hoppscotch-common/src/services/initialization.service.ts delete mode 100644 packages/hoppscotch-common/src/services/inspection/inspectors/__tests__/header.inspector.spec.ts create mode 100644 packages/hoppscotch-common/src/services/inspection/inspectors/__tests__/request.inspector.spec.ts delete mode 100644 packages/hoppscotch-common/src/services/inspection/inspectors/authorization.inspector.ts delete mode 100644 packages/hoppscotch-common/src/services/inspection/inspectors/header.inspector.ts delete mode 100644 packages/hoppscotch-common/src/services/inspection/inspectors/interceptors.inspector.ts create mode 100644 packages/hoppscotch-common/src/services/inspection/inspectors/request.inspector.ts create mode 100644 packages/hoppscotch-common/src/services/instance-switcher.service.ts create mode 100644 packages/hoppscotch-common/src/services/kernel-interceptor.service.ts create mode 100644 packages/hoppscotch-common/src/services/spotlight/searchers/kernel-interceptor.searcher.ts create mode 100644 packages/hoppscotch-common/src/types/kernel.d.ts create mode 100644 packages/hoppscotch-desktop/.gitignore create mode 100644 packages/hoppscotch-desktop/README.md create mode 100644 packages/hoppscotch-desktop/crates/webapp-bundler/.gitignore create mode 100644 packages/hoppscotch-desktop/crates/webapp-bundler/Cargo.lock create mode 100644 packages/hoppscotch-desktop/crates/webapp-bundler/Cargo.toml create mode 100644 packages/hoppscotch-desktop/crates/webapp-bundler/src/main.rs create mode 100644 packages/hoppscotch-desktop/index.html create mode 100644 packages/hoppscotch-desktop/package.json create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/.envrc create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/.gitignore create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/Cargo.lock create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/Cargo.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/LICENSE.md create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/README.md rename packages/{hoppscotch-agent => hoppscotch-desktop/plugin-workspace/relay}/devenv.lock (84%) create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/devenv.nix rename packages/{hoppscotch-relay => hoppscotch-desktop/plugin-workspace/relay}/devenv.yaml (100%) create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/src/auth.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/src/content.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/src/error.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/src/header.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/src/interop.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/src/lib.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/src/relay.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/src/request.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/src/response.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/src/security.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/src/transfer.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/relay/src/util.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/.envrc create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/.gitignore create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/Cargo.lock create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/Cargo.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/LICENSE.md create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/README.md create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/build.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/devenv.lock rename packages/{hoppscotch-agent => hoppscotch-desktop/plugin-workspace/tauri-plugin-appload}/devenv.nix (63%) create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/devenv.yaml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/dist-js/index.cjs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/dist-js/index.d.ts create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/dist-js/index.d.ts.map create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/dist-js/index.js create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/.gitignore create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/.vscode/extensions.json create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/README.md create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/index.html create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/jsconfig.json create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/package.json create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/public/svelte.svg create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/public/tauri.svg create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/public/vite.svg create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/.gitignore create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/Cargo.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/build.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/capabilities/default.json create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/icons/128x128.png create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/icons/128x128@2x.png create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/icons/32x32.png create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/icons/icon.icns create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/icons/icon.ico create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/icons/icon.png create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/src/lib.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/src/main.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src-tauri/tauri.conf.json create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src/App.svelte create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src/lib/Greet.svelte create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src/main.js create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src/style.css create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/src/vite-env.d.ts create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app/vite.config.js create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/guest-js/index.ts create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/package.json create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/permissions/autogenerated/commands/clear.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/permissions/autogenerated/commands/download.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/permissions/autogenerated/commands/load.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/permissions/autogenerated/commands/ping.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/permissions/autogenerated/commands/remove.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/permissions/autogenerated/reference.md create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/permissions/default.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/permissions/schemas/schema.json create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/pnpm-lock.yaml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/rollup.config.js create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/api/client.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/api/error.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/api/mod.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/api/model.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/bundle/error.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/bundle/loader.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/bundle/mod.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/bundle/verified.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/cache/error.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/cache/manager.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/cache/mod.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/cache/policy.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/cache/store.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/commands.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/config/mod.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/config/model.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/desktop.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/envvar.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/error.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/global.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/kernel.js create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/lib.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/mobile.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/models.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/storage/error.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/storage/layout.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/storage/manager.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/storage/mod.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/storage/registry.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/ui/macos/mod.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/ui/macos/posit.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/ui/mod.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/ui/windows/mod.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/ui/windows/posit.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/uri/error.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/uri/handler.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/uri/mod.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/vendor/builder.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/vendor/config.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/vendor/error.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/vendor/mod.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/verification/bundle.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/verification/error.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/verification/file.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/verification/key.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/src/verification/mod.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/tsconfig.json create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/.envrc create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/.gitignore create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/Cargo.lock create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/Cargo.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/LICENSE.md create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/README.md create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/build.rs rename packages/{hoppscotch-relay => hoppscotch-desktop/plugin-workspace/tauri-plugin-relay}/devenv.lock (84%) create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/devenv.nix create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/devenv.yaml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/dist-js/index.cjs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/dist-js/index.d.ts create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/dist-js/index.d.ts.map create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/dist-js/index.js create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/guest-js/index.ts create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/package.json create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/permissions/autogenerated/commands/cancel.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/permissions/autogenerated/commands/execute.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/permissions/autogenerated/commands/run.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/permissions/autogenerated/commands/subscribe.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/permissions/autogenerated/reference.md create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/permissions/default.toml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/permissions/schemas/schema.json create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/pnpm-lock.yaml create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/rollup.config.js create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/src/commands.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/src/desktop.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/src/error.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/src/lib.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/src/mobile.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/src/models.rs create mode 100644 packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/tsconfig.json create mode 100644 packages/hoppscotch-desktop/postcss.config.js create mode 100644 packages/hoppscotch-desktop/public/favicon.ico create mode 100644 packages/hoppscotch-desktop/public/images/add_group.svg create mode 100644 packages/hoppscotch-desktop/public/images/pack.svg create mode 100644 packages/hoppscotch-desktop/public/images/youre_lost.svg create mode 100644 packages/hoppscotch-desktop/public/logo.svg create mode 100644 packages/hoppscotch-desktop/public/tauri.svg create mode 100644 packages/hoppscotch-desktop/public/vite.svg create mode 100644 packages/hoppscotch-desktop/src-tauri/.gitignore create mode 100644 packages/hoppscotch-desktop/src-tauri/Cargo.lock create mode 100644 packages/hoppscotch-desktop/src-tauri/Cargo.toml create mode 100644 packages/hoppscotch-desktop/src-tauri/build.rs create mode 100644 packages/hoppscotch-desktop/src-tauri/capabilities/default.json create mode 100644 packages/hoppscotch-desktop/src-tauri/capabilities/desktop.json create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/128x128.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/128x128@2x.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/32x32.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/Square107x107Logo.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/Square142x142Logo.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/Square150x150Logo.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/Square284x284Logo.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/Square30x30Logo.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/Square310x310Logo.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/Square44x44Logo.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/Square71x71Logo.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/Square89x89Logo.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/StoreLogo.png create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/icon.icns create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/icon.ico create mode 100644 packages/hoppscotch-desktop/src-tauri/icons/icon.png create mode 100644 packages/hoppscotch-desktop/src-tauri/src/lib.rs create mode 100644 packages/hoppscotch-desktop/src-tauri/src/main.rs create mode 100644 packages/hoppscotch-desktop/src-tauri/src/server.rs create mode 100644 packages/hoppscotch-desktop/src-tauri/src/updater.rs create mode 100644 packages/hoppscotch-desktop/src-tauri/tauri.conf.json create mode 100644 packages/hoppscotch-desktop/src/App.vue create mode 100644 packages/hoppscotch-desktop/src/assets/scss/styles.scss create mode 100644 packages/hoppscotch-desktop/src/assets/scss/tailwind.scss create mode 100644 packages/hoppscotch-desktop/src/assets/vue.svg create mode 100644 packages/hoppscotch-desktop/src/components.d.ts create mode 100644 packages/hoppscotch-desktop/src/components/layout/LayoutHeader.vue create mode 100644 packages/hoppscotch-desktop/src/components/layout/LayoutSidebar.vue create mode 100644 packages/hoppscotch-desktop/src/main.ts create mode 100644 packages/hoppscotch-desktop/src/router.ts create mode 100644 packages/hoppscotch-desktop/src/types/index.ts create mode 100644 packages/hoppscotch-desktop/src/views/Home.vue create mode 100644 packages/hoppscotch-desktop/src/vite-env.d.ts create mode 100644 packages/hoppscotch-desktop/tailwind.config.ts create mode 100644 packages/hoppscotch-desktop/tsconfig.json create mode 100644 packages/hoppscotch-desktop/tsconfig.node.json create mode 100644 packages/hoppscotch-desktop/vite.config.ts create mode 100644 packages/hoppscotch-kernel/README.md create mode 100644 packages/hoppscotch-kernel/package.json create mode 100644 packages/hoppscotch-kernel/src/index.ts create mode 100644 packages/hoppscotch-kernel/src/io/impl/desktop/index.ts create mode 100644 packages/hoppscotch-kernel/src/io/impl/desktop/v/1.ts create mode 100644 packages/hoppscotch-kernel/src/io/impl/web/index.ts create mode 100644 packages/hoppscotch-kernel/src/io/impl/web/v/1.ts create mode 100644 packages/hoppscotch-kernel/src/io/index.ts create mode 100644 packages/hoppscotch-kernel/src/io/v/1.ts create mode 100644 packages/hoppscotch-kernel/src/relay/impl/desktop/index.ts create mode 100644 packages/hoppscotch-kernel/src/relay/impl/desktop/v/1.ts create mode 100644 packages/hoppscotch-kernel/src/relay/impl/web/index.ts create mode 100644 packages/hoppscotch-kernel/src/relay/impl/web/v/1.ts create mode 100644 packages/hoppscotch-kernel/src/relay/index.ts create mode 100644 packages/hoppscotch-kernel/src/relay/v/1.ts create mode 100644 packages/hoppscotch-kernel/src/store/impl/desktop/index.ts create mode 100644 packages/hoppscotch-kernel/src/store/impl/desktop/v/1.ts create mode 100644 packages/hoppscotch-kernel/src/store/impl/web/index.ts create mode 100644 packages/hoppscotch-kernel/src/store/impl/web/v/1.ts create mode 100644 packages/hoppscotch-kernel/src/store/index.ts create mode 100644 packages/hoppscotch-kernel/src/store/v/1.ts create mode 100644 packages/hoppscotch-kernel/src/type/versioning.ts create mode 100644 packages/hoppscotch-kernel/src/util/capability.ts create mode 100644 packages/hoppscotch-kernel/tsconfig.base.json create mode 100644 packages/hoppscotch-kernel/tsconfig.decl.json create mode 100644 packages/hoppscotch-kernel/tsconfig.json create mode 100644 packages/hoppscotch-kernel/vite.config.d.ts create mode 100644 packages/hoppscotch-kernel/vite.config.ts delete mode 100644 packages/hoppscotch-relay/.envrc delete mode 100644 packages/hoppscotch-relay/devenv.nix create mode 100644 packages/hoppscotch-selfhost-web/src/components/Login.vue create mode 100644 packages/hoppscotch-selfhost-web/src/pages/device-login.vue rename packages/hoppscotch-selfhost-web/src/platform/auth/{auth.api.ts => desktop/api.ts} (97%) create mode 100644 packages/hoppscotch-selfhost-web/src/platform/auth/desktop/index.ts create mode 100644 packages/hoppscotch-selfhost-web/src/platform/auth/web/api.ts rename packages/hoppscotch-selfhost-web/src/platform/auth/{auth.platform.ts => web/index.ts} (92%) rename packages/hoppscotch-selfhost-web/src/platform/collections/{collections.api.ts => desktop/api.ts} (99%) rename packages/hoppscotch-selfhost-web/src/platform/collections/{ => desktop}/gqlCollections.sync.ts (96%) create mode 100644 packages/hoppscotch-selfhost-web/src/platform/collections/desktop/index.ts rename packages/hoppscotch-selfhost-web/src/platform/collections/{collections.sync.ts => desktop/sync.ts} (98%) create mode 100644 packages/hoppscotch-selfhost-web/src/platform/collections/web/api.ts create mode 100644 packages/hoppscotch-selfhost-web/src/platform/collections/web/gqlCollections.sync.ts rename packages/hoppscotch-selfhost-web/src/platform/collections/{collections.platform.ts => web/index.ts} (99%) create mode 100644 packages/hoppscotch-selfhost-web/src/platform/collections/web/sync.ts rename packages/hoppscotch-selfhost-web/src/platform/environments/{environments.api.ts => desktop/api.ts} (98%) rename packages/hoppscotch-selfhost-web/src/platform/environments/{environments.platform.ts => desktop/index.ts} (97%) rename packages/hoppscotch-selfhost-web/src/platform/environments/{environments.sync.ts => desktop/sync.ts} (94%) create mode 100644 packages/hoppscotch-selfhost-web/src/platform/environments/web/api.ts create mode 100644 packages/hoppscotch-selfhost-web/src/platform/environments/web/index.ts create mode 100644 packages/hoppscotch-selfhost-web/src/platform/environments/web/sync.ts rename packages/hoppscotch-selfhost-web/src/platform/history/{history.api.ts => desktop/api.ts} (98%) rename packages/hoppscotch-selfhost-web/src/platform/history/{history.platform.ts => desktop/index.ts} (97%) rename packages/hoppscotch-selfhost-web/src/platform/history/{history.sync.ts => desktop/sync.ts} (90%) create mode 100644 packages/hoppscotch-selfhost-web/src/platform/history/web/api.ts create mode 100644 packages/hoppscotch-selfhost-web/src/platform/history/web/index.ts create mode 100644 packages/hoppscotch-selfhost-web/src/platform/history/web/sync.ts rename packages/hoppscotch-selfhost-web/src/platform/settings/{settings.api.ts => desktop/api.ts} (97%) rename packages/hoppscotch-selfhost-web/src/platform/settings/{settings.platform.ts => desktop/index.ts} (96%) rename packages/hoppscotch-selfhost-web/src/platform/settings/{settings.sync.ts => desktop/sync.ts} (79%) create mode 100644 packages/hoppscotch-selfhost-web/src/platform/settings/web/api.ts create mode 100644 packages/hoppscotch-selfhost-web/src/platform/settings/web/index.ts create mode 100644 packages/hoppscotch-selfhost-web/src/platform/settings/web/sync.ts delete mode 100644 packages/hoppscotch-selfhost-web/src/platform/tabState/tabState.platform.ts create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/.gitignore create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/Cargo.lock create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/Cargo.toml create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/README.md create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/api/error.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/api/handler.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/api/mod.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/api/model.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/bundle/builder.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/bundle/error.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/bundle/manager.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/bundle/mod.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/bundle/model.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/config.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/error.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/main.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/model.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/signing/error.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/signing/key.rs create mode 100644 packages/hoppscotch-selfhost-web/webapp-server/src/signing/mod.rs diff --git a/.dockerignore b/.dockerignore index 93efa139..5b57b57f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,35 @@ +.devenv* +.direnv +.devcontainer +.git +.github +.husky +.vscode + +.envrc +devenv.yaml +devenv.nix +.prettierrc.js +.prettierignore +.editorconfig +.npmrc +.firebaserc + node_modules +**/node_modules **/*/node_modules + +**/dist +**/build +**/target + +**/__tests__ +**/*.test.* +**/coverage + +*.md +LICENSE +CODEOWNERS + +.DS_Store +*.log diff --git a/.env.example b/.env.example index fc8cb758..aba3decc 100644 --- a/.env.example +++ b/.env.example @@ -20,7 +20,14 @@ DATA_ENCRYPTION_KEY="data encryption key with 32 char" # Hoppscotch App Domain Config REDIRECT_URL="http://localhost:3000" -WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100" +# Whitelisted origins for the Hoppscotch App. +# This list controls which origins can interact with the app through cross-origin comms. +# - localhost ports (3170, 3000, 3100): app, backend, development servers and services +# - app://localhost_3200: Bundle server origin identifier +# NOTE: `3200` here refers to the bundle server (port 3200) that provides the bundles, +# NOT where the app runs. The app itself uses the `app://` protocol with dynamic +# bundle names like `app://{bundle-name}/` +WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100,app://localhost_3200,app://hoppscotch" VITE_ALLOWED_AUTH_PROVIDERS=GOOGLE,GITHUB,MICROSOFT,EMAIL # Google Auth Config diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..894571bf --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +source_url "https://raw.githubusercontent.com/cachix/devenv/82c0147677e510b247d8b9165c54f73d32dfd899/direnvrc" "sha256-7u4iDd1nZpxL4tCzmPG0dQgC5V+/44Ba+tHkPob1v2k=" + +use devenv diff --git a/.gitignore b/.gitignore index bdfa619c..68c41261 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,13 @@ tests/*/videos # GQL SDL generated for the frontends gql-gen/ + +# Devenv +.devenv* +devenv.local.nix + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/aio-subpath-access.Caddyfile b/aio-subpath-access.Caddyfile index 7318eecd..10b8bf55 100644 --- a/aio-subpath-access.Caddyfile +++ b/aio-subpath-access.Caddyfile @@ -21,6 +21,11 @@ reverse_proxy localhost:8080 } + # Handle requests under `/desktop-app-server*` path + handle_path /desktop-app-server* { + reverse_proxy localhost:3200 + } + # Catch-all route for unknown paths, serves `selfhost-web` SPA handle { root * /site/selfhost-web diff --git a/aio_run.mjs b/aio_run.mjs index 2690078f..3dbe7141 100644 --- a/aio_run.mjs +++ b/aio_run.mjs @@ -52,6 +52,7 @@ fs.rmSync("build.env") const caddyFileName = process.env.ENABLE_SUBPATH_BASED_ACCESS === 'true' ? 'aio-subpath-access.Caddyfile' : 'aio-multiport-setup.Caddyfile' const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", `/etc/caddy/${caddyFileName}`, "--adapter", "caddyfile"], "App/Admin Dashboard Caddy") const backendProcess = runChildProcessWithPrefix("node", ["/dist/backend/dist/main.js"], "Backend Server") +const webappProcess = runChildProcessWithPrefix("webapp-server", [], "Webapp Server") caddyProcess.on("exit", (code) => { console.log(`Exiting process because Caddy Server exited with code ${code}`) @@ -63,11 +64,17 @@ backendProcess.on("exit", (code) => { process.exit(code) }) +webappProcess.on("exit", (code) => { + console.log(`Exiting process because Webapp Server exited with code ${code}`) + process.exit(code) +}) + process.on('SIGINT', () => { console.log("SIGINT received, exiting...") caddyProcess.kill("SIGINT") backendProcess.kill("SIGINT") + webappProcess.kill("SIGINT") process.exit(0) }) diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 00000000..b22ef4ed --- /dev/null +++ b/devenv.lock @@ -0,0 +1,140 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1738772960, + "owner": "cachix", + "repo": "devenv", + "rev": "7f756cdf3fbb01cab243dcec4de0ca94e6aaa2af", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1738737274, + "owner": "nix-community", + "repo": "fenix", + "rev": "f82de9980822f3b1efcf54944939b1d514386827", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1737465171, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "9364dc02281ce2d37a1f55b6e51f7c0f65a75f17", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1738734093, + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5b2753b0356d1c951d7a3ef1d086ba5a71fff43c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "fenix": "fenix", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": [ + "git-hooks" + ] + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1738754241, + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "ca47cddc31ae76a05e8709ed4aec805c5ef741d3", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 00000000..2b27f253 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,189 @@ +{ pkgs, lib, config, inputs, ... }: + +let + rosettaPkgs = + if pkgs.stdenv.isDarwin && pkgs.stdenv.isAarch64 + then pkgs.pkgsx86_64Darwin + else pkgs; + + darwinPackages = with pkgs; [ + darwin.apple_sdk.frameworks.Security + darwin.apple_sdk.frameworks.CoreServices + darwin.apple_sdk.frameworks.CoreFoundation + darwin.apple_sdk.frameworks.Foundation + darwin.apple_sdk.frameworks.AppKit + darwin.apple_sdk.frameworks.WebKit + ]; + + linuxPackages = with pkgs; [ + libsoup_3 + webkitgtk_4_1 + librsvg + libappindicator + libayatana-appindicator + ]; + +in { + packages = with pkgs; [ + git + lima + colima + docker + jq + # NOTE: In case there's `Cannot find module: ... bcrypt ...` error, try `npm rebuild bcrypt` + # See: https://github.com/kelektiv/node.bcrypt.js/issues/800 + # See: https://github.com/kelektiv/node.bcrypt.js/issues/1055 + nodejs_20 + nodePackages.typescript-language-server + nodePackages."@volar/vue-language-server" + nodePackages.prisma + prisma-engines + cargo-edit + ] ++ lib.optionals pkgs.stdenv.isDarwin darwinPackages + ++ lib.optionals pkgs.stdenv.isLinux linuxPackages; + + env = { + APP_GREET = "Hoppscotch"; + DATABASE_URL = "postgresql://postgres:testpass@localhost:5432/hoppscotch?connect_timeout=300"; + DOCKER_BUILDKIT = "1"; + COMPOSE_DOCKER_CLI_BUILD = "1"; + } // lib.optionalAttrs pkgs.stdenv.isLinux { + # NOTE: Setting these `PRISMA_*` environment variable fixes + # "Error: Failed to fetch sha256 checksum at https://binaries.prisma.sh/all_commits//linux-nixos/libquery_engine.so.node.gz.sha256 - 404 Not Found" + # See: https://github.com/prisma/prisma/discussions/3120 + PRISMA_QUERY_ENGINE_LIBRARY = "${pkgs.prisma-engines}/lib/libquery_engine.node"; + PRISMA_QUERY_ENGINE_BINARY = "${pkgs.prisma-engines}/bin/query-engine"; + PRISMA_SCHEMA_ENGINE_BINARY = "${pkgs.prisma-engines}/bin/schema-engine"; + + LD_LIBRARY_PATH = lib.makeLibraryPath [ + pkgs.libappindicator + pkgs.libayatana-appindicator + ]; + } // lib.optionalAttrs pkgs.stdenv.isDarwin { + # Place to put macOS-specific environment variables + }; + + scripts = { + hello.exec = "echo hello from $APP_GREET"; + e.exec = "emacs"; + lima-setup.exec = "limactl start template://docker"; + lima-clean.exec = "limactl rm -f $(limactl ls -q)"; + colima-start.exec = "colima start --cpu 4 --memory 50"; + + docker-prune.exec = '' + echo "Cleaning up unused Docker resources..." + docker system prune -f + ''; + + docker-logs.exec = "docker logs -f hoppscotch-aio"; + + docker-status.exec = '' + echo "Container Status:" + docker ps -a --filter "name=hoppscotch-aio" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + ''; + + db-reset.exec = '' + echo "Resetting database..." + psql -U postgres -d hoppscotch -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;' + echo "Database reset complete." + ''; + + docker-clean-all.exec = '' + echo "Starting complete Docker cleanup..." + + echo "1/6: Stopping all running containers..." + CONTAINERS=$(docker ps -q) + if [ -n "$CONTAINERS" ]; then + docker stop $CONTAINERS + echo "✓ Containers stopped" + else + echo "• No running containers found" + fi + + echo "2/6: Removing all containers..." + CONTAINERS=$(docker ps -aq) + if [ -n "$CONTAINERS" ]; then + docker rm $CONTAINERS + echo "✓ Containers removed" + else + echo "• No containers to remove" + fi + + echo "3/6: Removing all images..." + IMAGES=$(docker images -q) + if [ -n "$IMAGES" ]; then + docker rmi --force $IMAGES + echo "✓ Images removed" + else + echo "• No images to remove" + fi + + echo "4/6: Removing all volumes..." + VOLUMES=$(docker volume ls -q) + if [ -n "$VOLUMES" ]; then + docker volume rm $VOLUMES + echo "✓ Volumes removed" + else + echo "• No volumes to remove" + fi + + echo "5/6: Removing custom networks..." + NETWORKS=$(docker network ls --filter type=custom -q) + if [ -n "$NETWORKS" ]; then + docker network rm $NETWORKS + echo "✓ Networks removed" + else + echo "• No custom networks to remove" + fi + + echo "6/6: Running system prune..." + docker system prune --all --force --volumes + echo "✓ System pruned" + + echo "Done!" + ''; + }; + + enterShell = '' + git --version + echo "Hoppscotch development environment ready!" + ${lib.optionalString pkgs.stdenv.isDarwin '' + # Place to put macOS-specific shell initialization + ''} + ${lib.optionalString pkgs.stdenv.isLinux '' + # Place to put Linux-specific shell initialization + ''} + ''; + + enterTest = '' + echo "Running tests" + ''; + + dotenv.enable = true; + + languages = { + typescript = { + enable = true; + }; + javascript = { + package = pkgs.nodejs_20; + enable = true; + npm.enable = true; + pnpm.enable = true; + }; + rust = { + enable = true; + channel = "nightly"; + components = [ + "rustc" + "cargo" + "clippy" + "rustfmt" + "rust-analyzer" + "llvm-tools-preview" + "rust-src" + "rustc-codegen-cranelift-preview" + ]; + }; + }; +} diff --git a/packages/hoppscotch-agent/devenv.yaml b/devenv.yaml similarity index 100% rename from packages/hoppscotch-agent/devenv.yaml rename to devenv.yaml diff --git a/docker-compose.yml b/docker-compose.yml index ee5bd337..65bdced2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,36 @@ # has a container with a Postgres instance running. # You can tweak around this file to match your instances +# PROFILES EXPLANATION: +# +# We use Docker Compose profiles to manage different deployment scenarios and avoid port conflicts. +# +# These are all the available profiles: +# - default: All-in-one service + database + auto-migration (recommended for most users) +# - default-no-db: All-in-one service without database (for users with external DB) +# - backend: The backend service only +# - app: The main Hoppscotch application only +# - admin: The self-host admin dashboard only +# - webapp: The static web app server only +# - database: Just the PostgreSQL database +# - just-backend: All services except webapp for local development +# - deprecated: All deprecated services (not recommended) + +# USAGE: +# +# To run the default setup: docker compose --profile default up +# To run without database: docker compose --profile default-no-db up +# To run specific components: docker compose --profile backend up +# To run all except webapp: docker compose --profile just-backend up +# To run deprecated services: docker compose --profile deprecated up + +# NOTE: The default and default-no-db profiles should not be mixed with individual service +# profiles as they would conflict on ports. + services: # This service runs the backend app in the port 3170 hoppscotch-backend: + profiles: ["backend", "just-backend"] container_name: hoppscotch-backend build: dockerfile: prod.Dockerfile @@ -32,6 +59,7 @@ services: # NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for # the SH admin dashboard server at packages/hoppscotch-selfhost-web/Caddyfile hoppscotch-app: + profiles: ["app"] container_name: hoppscotch-app build: dockerfile: prod.Dockerfile @@ -49,6 +77,7 @@ services: # NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for # the SH admin dashboard server at packages/hoppscotch-sh-admin/Caddyfile hoppscotch-sh-admin: + profiles: ["admin"] container_name: hoppscotch-sh-admin build: dockerfile: prod.Dockerfile @@ -62,8 +91,22 @@ services: - "3280:80" - "3100:3100" - # The service that spins up all 3 services at once in one container + # The static server for serving web content to desktop shell, hosted at port 3200 + hoppscotch-webapp-server: + profiles: ["webapp"] + container_name: hoppscotch-webapp-server + env_file: + - ./.env + build: + dockerfile: prod.Dockerfile + context: . + target: webapp_server + ports: + - "3200:3200" + + # The service that spins up all services at once in one container hoppscotch-aio: + profiles: ["default", "default-no-db"] container_name: hoppscotch-aio restart: unless-stopped build: @@ -79,12 +122,14 @@ services: - "3000:3000" - "3100:3100" - "3170:3170" + - "3200:3200" - "3080:80" # The preset DB service, you can delete/comment the below lines if # you are using an external postgres instance # This will be exposed at port 5432 hoppscotch-db: + profiles: ["default", "database", "just-backend"] image: postgres:15 ports: - "5432:5432" @@ -105,8 +150,24 @@ services: timeout: 5s retries: 10 - # All the services listed below are deprececated + # Auto-migration service - handles database migrations automatically + hoppscotch-migrate: + profiles: ["default", "just-backend"] + build: + dockerfile: prod.Dockerfile + context: . + target: backend + env_file: + - ./.env + depends_on: + hoppscotch-db: + condition: service_healthy + command: sh -c "pnpx prisma migrate deploy" + + # All the services listed below are deprecated + # These services are kept for backward compatibility but should not be used for new deployments hoppscotch-old-backend: + profiles: ["deprecated"] container_name: hoppscotch-old-backend build: dockerfile: packages/hoppscotch-backend/Dockerfile @@ -130,6 +191,7 @@ services: - "3170:3000" hoppscotch-old-app: + profiles: ["deprecated"] container_name: hoppscotch-old-app build: dockerfile: packages/hoppscotch-selfhost-web/Dockerfile @@ -142,6 +204,7 @@ services: - "3000:8080" hoppscotch-old-sh-admin: + profiles: ["deprecated"] container_name: hoppscotch-old-sh-admin build: dockerfile: packages/hoppscotch-sh-admin/Dockerfile @@ -152,3 +215,29 @@ services: - hoppscotch-old-backend ports: - "3100:8080" + +# DEPLOYMENT SCENARIOS: +# 1. Default deployment (recommended): +# docker compose --profile default up +# This will start: AIO + database + auto-migration +# +# 2. Default deployment without database: +# docker compose --profile default-no-db up +# This will start: AIO only (use when you have an external database) +# +# 3. Individual service deployment: +# docker compose --profile backend up # Just the backend +# docker compose --profile app up # Just the app +# docker compose --profile admin up # Just the admin dashboard +# docker compose --profile webapp up # Just the static web server +# docker compose --profile database up # Just the database +# +# 4. Development deployment: +# docker compose --profile just-backend up # All services except webapp +# +# 5. Deprecated services: +# docker compose --profile deprecated up +# This will start all deprecated services (not recommended for new deployments) +# +# Remember: The default and default-no-db profiles should not be mixed with individual service +# profiles as they would conflict on ports. diff --git a/packages/codemirror-lang-graphql/tsconfig.json b/packages/codemirror-lang-graphql/tsconfig.json index b258d4ce..7c5d26ee 100644 --- a/packages/codemirror-lang-graphql/tsconfig.json +++ b/packages/codemirror-lang-graphql/tsconfig.json @@ -7,6 +7,7 @@ "declaration": true, "declarationDir": "./dist", "moduleResolution": "node", + "skipLibCheck": true, "allowJs": true }, "include": ["src/*"] diff --git a/packages/hoppscotch-agent/.envrc b/packages/hoppscotch-agent/.envrc deleted file mode 100644 index 5bf8fc15..00000000 --- a/packages/hoppscotch-agent/.envrc +++ /dev/null @@ -1,3 +0,0 @@ -source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0=" - -use devenv \ No newline at end of file diff --git a/packages/hoppscotch-agent/package.json b/packages/hoppscotch-agent/package.json index 19b6f64e..429849d5 100644 --- a/packages/hoppscotch-agent/package.json +++ b/packages/hoppscotch-agent/package.json @@ -16,6 +16,7 @@ "@vueuse/core": "^11.1.0", "axios": "^1.7.7", "fp-ts": "^2.16.9", + "lodash-es": "4.17.21", "vue": "3.3.9" }, "devDependencies": { @@ -29,6 +30,7 @@ "typescript": "^5.6.3", "unplugin-icons": "^0.19.3", "vite": "^5.4.8", + "@types/lodash-es": "4.17.12", "vue-tsc": "^2.1.6" } } diff --git a/packages/hoppscotch-agent/src-tauri/Cargo.lock b/packages/hoppscotch-agent/src-tauri/Cargo.lock index 4893f4e8..e4f125bd 100644 --- a/packages/hoppscotch-agent/src-tauri/Cargo.lock +++ b/packages/hoppscotch-agent/src-tauri/Cargo.lock @@ -93,9 +93,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -108,49 +108,49 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.91" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" dependencies = [ "derive_arbitrary", ] @@ -163,9 +163,9 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "ashpd" -version = "0.9.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d43c03d9e36dd40cab48435be0b09646da362c278223ca535493877b2c1dee9" +checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3" dependencies = [ "enumflags2", "futures-channel", @@ -179,17 +179,7 @@ dependencies = [ "wayland-backend", "wayland-client", "wayland-protocols", - "zbus", -] - -[[package]] -name = "assert-json-diff" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" -dependencies = [ - "serde", - "serde_json", + "zbus 5.2.0", ] [[package]] @@ -218,9 +208,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" dependencies = [ "flate2", "futures-core", @@ -255,9 +245,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if", @@ -310,7 +300,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -345,14 +335,14 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] name = "atk" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" dependencies = [ "atk-sys", "glib", @@ -361,9 +351,9 @@ dependencies = [ [[package]] name = "atk-sys" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" dependencies = [ "glib-sys", "gobject-sys", @@ -384,7 +374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" dependencies = [ "dirs 4.0.0", - "thiserror", + "thiserror 1.0.69", "winreg 0.10.1", ] @@ -396,9 +386,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", @@ -409,7 +399,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "itoa 1.0.11", + "itoa 1.0.14", "matchit", "memchr", "mime", @@ -420,7 +410,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper", "tokio", "tower", "tower-layer", @@ -443,7 +433,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.1", + "sync_wrapper", "tower-layer", "tower-service", "tracing", @@ -451,25 +441,26 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.9.4" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73c3220b188aea709cf1b6c5f9b01c3bd936bb08bd2b5184a12b35ac8131b1f9" +checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" dependencies = [ "axum", "axum-core", "bytes", + "fastrand", "futures-util", "headers", "http", "http-body", "http-body-util", "mime", + "multer", "pin-project-lite", "serde", "tower", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -586,9 +577,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" [[package]] name = "byteorder" @@ -604,9 +595,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" dependencies = [ "serde", ] @@ -622,7 +613,7 @@ dependencies = [ "glib", "libc", "once_cell", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -647,9 +638,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" dependencies = [ "serde", ] @@ -665,7 +656,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -675,14 +666,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719" dependencies = [ "serde", - "toml 0.8.2", + "toml 0.8.19", ] [[package]] name = "cc" -version = "1.1.31" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" dependencies = [ "shlex", ] @@ -728,9 +719,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -813,19 +804,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" - -[[package]] -name = "colored" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" -dependencies = [ - "lazy_static", - "windows-sys 0.48.0", -] +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "combine" @@ -865,12 +846,13 @@ dependencies = [ [[package]] name = "cookie_store" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" dependencies = [ "cookie", - "idna 0.5.0", + "document-features", + "idna", "log", "publicsuffix", "serde", @@ -956,9 +938,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -974,18 +956,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" @@ -1022,17 +1004,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] name = "ctor" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1092,7 +1074,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1116,7 +1098,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1127,7 +1109,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1161,26 +1143,15 @@ dependencies = [ "serde", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "derive_arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1193,7 +1164,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1282,7 +1253,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1291,7 +1262,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.8.5", + "libloading 0.8.6", ] [[package]] @@ -1314,7 +1285,16 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", ] [[package]] @@ -1367,14 +1347,14 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "embed-resource" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e24052d7be71f0efb50c201557f6fe7d237cfd5a64fd5bcd7fd8fe32dbbffa" +checksum = "b68b6f9f63a0b6a38bc447d4ce84e2b388f3ec95c99c641c8ff0dd3ef89a6379" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.8.2", + "toml 0.8.19", "vswhom", "winreg 0.52.0", ] @@ -1387,9 +1367,9 @@ checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -1418,7 +1398,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1462,12 +1442,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1483,9 +1463,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ "event-listener", "pin-project-lite", @@ -1493,15 +1473,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] @@ -1536,23 +1516,14 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", ] -[[package]] -name = "fluent-uri" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1586,7 +1557,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1654,9 +1625,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ "fastrand", "futures-core", @@ -1673,7 +1644,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1716,9 +1687,9 @@ dependencies = [ [[package]] name = "gdk" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -1757,9 +1728,9 @@ dependencies = [ [[package]] name = "gdk-sys" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1774,9 +1745,9 @@ dependencies = [ [[package]] name = "gdkwayland-sys" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90fbf5c033c65d93792192a49a8efb5bb1e640c419682a58bb96f5ae77f3d4a" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" dependencies = [ "gdk-sys", "glib-sys", @@ -1788,9 +1759,9 @@ dependencies = [ [[package]] name = "gdkx11" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2ea8a4909d530f79921290389cbd7c34cb9d623bfe970eaae65ca5f9cd9cce" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" dependencies = [ "gdk", "gdkx11-sys", @@ -1802,9 +1773,9 @@ dependencies = [ [[package]] name = "gdkx11-sys" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee8f00f4ee46cad2939b8990f5c70c94ff882c3028f3cc5abf950fa4ab53043" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" dependencies = [ "gdk-sys", "glib-sys", @@ -1841,8 +1812,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1877,7 +1850,7 @@ dependencies = [ "once_cell", "pin-project-lite", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1913,7 +1886,7 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1923,11 +1896,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ "heck 0.4.1", - "proc-macro-crate 2.0.2", + "proc-macro-crate 2.0.0", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1959,9 +1932,9 @@ dependencies = [ [[package]] name = "gtk" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" dependencies = [ "atk", "cairo-rs", @@ -1980,9 +1953,9 @@ dependencies = [ [[package]] name = "gtk-sys" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" dependencies = [ "atk-sys", "cairo-sys-rs", @@ -1998,22 +1971,22 @@ dependencies = [ [[package]] name = "gtk3-macros" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] name = "h2" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", @@ -2021,7 +1994,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.6.0", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -2042,9 +2015,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "headers" @@ -2082,12 +2055,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hermit-abi" version = "0.4.0" @@ -2102,16 +2069,16 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "hoppscotch-agent" -version = "0.1.3" +version = "0.1.4" dependencies = [ "aes-gcm", "axum", @@ -2119,15 +2086,13 @@ dependencies = [ "base16", "chrono", "dashmap", - "env_logger", - "hoppscotch-relay", "lazy_static", - "log", - "mockito", "native-dialog", "rand 0.8.5", + "relay", "serde", "serde_json", + "sha2", "tauri", "tauri-build", "tauri-plugin-autostart", @@ -2138,32 +2103,18 @@ dependencies = [ "tauri-plugin-store", "tauri-plugin-updater", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-util", "tower-http", + "tracing", + "tracing-appender", + "tracing-subscriber", "uuid", "winreg 0.52.0", "x25519-dalek", ] -[[package]] -name = "hoppscotch-relay" -version = "0.1.1" -dependencies = [ - "curl", - "env_logger", - "http", - "log", - "openssl", - "openssl-sys", - "serde", - "serde_json", - "thiserror", - "tokio-util", - "url-escape", -] - [[package]] name = "html5ever" version = "0.26.0" @@ -2180,13 +2131,13 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", - "itoa 1.0.11", + "itoa 1.0.14", ] [[package]] @@ -2212,6 +2163,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-serde" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f056c8559e3757392c8d091e796416e4649d8e49e88b8d76df6c002f05027fd" +dependencies = [ + "http", + "serde", +] + [[package]] name = "httparse" version = "1.9.5" @@ -2232,9 +2193,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", @@ -2244,7 +2205,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.11", + "itoa 1.0.14", "pin-project-lite", "smallvec", "tokio", @@ -2253,9 +2214,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "f6884a48c6826ec44f524c7456b163cebe9e55a18d7b5e307cb4f100371cc767" dependencies = [ "futures-util", "http", @@ -2271,9 +2232,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", @@ -2321,6 +2282,124 @@ dependencies = [ "png", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2329,29 +2408,30 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.3.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "idna" -version = "0.5.0" +name = "idna_adapter" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", ] [[package]] name = "image" -version = "0.25.4" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -2372,12 +2452,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.2", "serde", ] @@ -2456,9 +2536,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "javascriptcore-rs" @@ -2494,7 +2574,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -2507,46 +2587,24 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] -[[package]] -name = "json-patch" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc" -dependencies = [ - "jsonptr 0.4.7", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "json-patch" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" dependencies = [ - "jsonptr 0.6.3", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "jsonptr" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627" -dependencies = [ - "fluent-uri", + "jsonptr", "serde", "serde_json", + "thiserror 1.0.69", ] [[package]] @@ -2615,9 +2673,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.161" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" @@ -2631,12 +2689,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -2668,6 +2726,18 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.12" @@ -2713,6 +2783,15 @@ dependencies = [ "tendril", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matches" version = "0.1.10" @@ -2754,15 +2833,15 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "minisign-verify" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a05b5d0594e0cb1ad8cee3373018d2b84e25905dc75b2468114cc9a8e86cfc20" +checksum = "6367d84fb54d4242af283086402907277715b8fe46976963af5ebf173f8efba3" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", "simd-adler32", @@ -2770,45 +2849,20 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] -[[package]] -name = "mockito" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b34bd91b9e5c5b06338d392463e1318d683cf82ec3d3af4014609be6e2108d" -dependencies = [ - "assert-json-diff", - "bytes", - "colored", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "log", - "rand 0.8.5", - "regex", - "serde_json", - "serde_urlencoded", - "similar", - "tokio", -] - [[package]] name = "muda" -version = "0.15.1" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8123dfd4996055ac9b15a60ad263b44b01e539007523ad7a4a533a3d93b0591" +checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" dependencies = [ "crossbeam-channel", "dpi", @@ -2820,10 +2874,27 @@ dependencies = [ "once_cell", "png", "serde", - "thiserror", + "thiserror 1.0.69", "windows-sys 0.59.0", ] +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + [[package]] name = "native-dialog" version = "0.7.0" @@ -2840,7 +2911,7 @@ dependencies = [ "objc_id", "once_cell", "raw-window-handle 0.5.2", - "thiserror", + "thiserror 1.0.69", "versions", "wfd", "which", @@ -2859,7 +2930,7 @@ dependencies = [ "ndk-sys", "num_enum", "raw-window-handle 0.6.2", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2885,12 +2956,13 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nix" -version = "0.27.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.6.0", "cfg-if", + "cfg_aliases", "libc", "memoffset", ] @@ -2911,6 +2983,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2941,10 +3023,10 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -3218,9 +3300,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "open" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" +checksum = "3ecd52f0b8d15c40ce4820aa251ed5de032e5d91fab27f7db2f40d42a8bdf69c" dependencies = [ "is-wsl", "libc", @@ -3250,7 +3332,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -3261,9 +3343,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.4.0+3.4.0" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] @@ -3307,6 +3389,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "pango" version = "0.18.3" @@ -3363,9 +3451,9 @@ dependencies = [ [[package]] name = "pathdiff" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "percent-encoding" @@ -3477,7 +3565,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -3509,9 +3597,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -3543,7 +3631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.6.0", + "indexmap 2.7.0", "quick-xml 0.32.0", "serde", "time", @@ -3551,9 +3639,9 @@ dependencies = [ [[package]] name = "png" -version = "0.17.14" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -3564,13 +3652,13 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.4.0", + "hermit-abi", "pin-project-lite", "rustix", "tracing", @@ -3622,12 +3710,20 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime", - "toml_edit 0.20.2", + "toml_edit 0.20.7", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.22", ] [[package]] @@ -3662,9 +3758,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -3677,11 +3773,11 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "publicsuffix" -version = "2.2.3" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" dependencies = [ - "idna 0.3.0", + "idna", "psl-types", ] @@ -3705,9 +3801,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" dependencies = [ "bytes", "pin-project-lite", @@ -3716,34 +3812,38 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.8", "tokio", "tracing", ] [[package]] name = "quinn-proto" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", + "getrandom 0.2.15", "rand 0.8.5", "ring", "rustc-hash", "rustls", + "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.8", "tinyvec", "tracing", + "web-time", ] [[package]] name = "quinn-udp" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" dependencies = [ + "cfg_aliases", "libc", "once_cell", "socket2", @@ -3855,9 +3955,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] @@ -3870,43 +3970,86 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relay" +version = "0.1.1" +source = "git+https://github.com/CuriousCorrelation/relay.git#0cab8a64b4c9148c930590f2ad5ad3cc1c6c00ae" +dependencies = [ + "bytes", + "curl", + "dashmap", + "env_logger", + "http", + "http-serde", + "infer", + "lazy_static", + "log", + "mime", + "openssl", + "openssl-sys", + "serde", + "serde_json", + "strum", + "thiserror 1.0.69", + "time", + "tokio-util", + "tracing", + "url-escape", + "urlencoding", +] + [[package]] name = "reqwest" -version = "0.12.8" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "async-compression", "base64 0.22.1", @@ -3937,7 +4080,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper", "system-configuration", "tokio", "tokio-rustls", @@ -3954,9 +4097,9 @@ dependencies = [ [[package]] name = "rfd" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8af382a047821a08aa6bfc09ab0d80ff48d45d8726f7cd8e44891f7cb4a4278e" +checksum = "46f6f80a9b882647d9014673ca9925d30ffc9750f2eed2b4490e189eaebd01e8" dependencies = [ "ashpd", "block2", @@ -3998,9 +4141,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustc_version" @@ -4013,22 +4156,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.15" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "once_cell", "ring", @@ -4049,9 +4192,12 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -4109,7 +4255,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -4146,18 +4292,18 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.213" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] @@ -4175,13 +4321,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.213" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -4192,16 +4338,16 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ - "itoa 1.0.11", + "itoa 1.0.14", "memchr", "ryu", "serde", @@ -4213,7 +4359,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" dependencies = [ - "itoa 1.0.11", + "itoa 1.0.14", "serde", ] @@ -4225,7 +4371,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -4244,7 +4390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.11", + "itoa 1.0.14", "ryu", "serde", ] @@ -4259,7 +4405,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.6.0", + "indexmap 2.7.0", "serde", "serde_derive", "serde_json", @@ -4276,7 +4422,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -4333,6 +4479,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shared_child" version = "1.0.1" @@ -4364,12 +4519,6 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" -[[package]] -name = "similar" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" - [[package]] name = "siphasher" version = "0.3.11" @@ -4393,9 +4542,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -4499,6 +4648,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.90", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4529,9 +4700,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.85" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -4540,19 +4711,24 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -4583,15 +4759,15 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.2", + "toml 0.8.19", "version-compare", ] [[package]] name = "tao" -version = "0.30.3" +version = "0.30.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0dbbebe82d02044dfa481adca1550d6dd7bd16e086bc34fa0fbecceb5a63751" +checksum = "6682a07cf5bab0b8a2bd20d0a542917ab928b5edb75ebd4eda6b05cbaab872da" dependencies = [ "bitflags 2.6.0", "cocoa 0.26.0", @@ -4634,14 +4810,14 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] name = "tar" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" +checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" dependencies = [ "filetime", "libc", @@ -4656,9 +4832,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.0.6" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3889b392db6d32a105d3757230ea0220090b8f94c90d3e60b6c5eb91178ab1b" +checksum = "e545de0a2dfe296fa67db208266cd397c5a55ae782da77973ef4c4fac90e9f2c" dependencies = [ "anyhow", "bytes", @@ -4694,7 +4870,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror", + "thiserror 2.0.8", "tokio", "tray-icon", "url", @@ -4707,36 +4883,36 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f96827ccfb1aa40d55d0ded79562d18ba18566657a553f992a982d755148376" +checksum = "7bd2a4bcfaf5fb9f4be72520eefcb61ae565038f8ccba2a497d8c28f463b8c01" dependencies = [ "anyhow", "cargo_toml", "dirs 5.0.1", "glob", "heck 0.5.0", - "json-patch 3.0.1", + "json-patch", "schemars", "semver", "serde", "serde_json", "tauri-utils", "tauri-winres", - "toml 0.8.2", + "toml 0.8.19", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947f16f47becd9e9cd39b74ee337fd1981574d78819be18e4384d85e5a0b82f" +checksum = "bf79faeecf301d3e969b1fae977039edb77a4c1f25cc0a961be298b54bff97cf" dependencies = [ "base64 0.22.1", "brotli", "ico", - "json-patch 2.0.0", + "json-patch", "plist", "png", "proc-macro2", @@ -4745,9 +4921,9 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.85", + "syn 2.0.90", "tauri-utils", - "thiserror", + "thiserror 2.0.8", "time", "url", "uuid", @@ -4756,23 +4932,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd1c8d4a66799d3438747c3a79705cd665a95d6f24cb5f315413ff7a981fe2a" +checksum = "c52027c8c5afb83166dacddc092ee8fff50772f9646d461d8c33ee887e447a03" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa4e6c94cb1d635f65a770c69e23de1bc054b0e4c554fa037a7cc7676333d39" +checksum = "e753f2a30933a9bbf0a202fa47d7cc4a3401f06e8d6dcc53b79aa62954828c79" dependencies = [ "anyhow", "glob", @@ -4781,30 +4957,29 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.8.2", + "toml 0.8.19", "walkdir", ] [[package]] name = "tauri-plugin-autostart" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bba6bb936e0fd0a58ed958b49e2e423dd40949c9d9425cc991be996959e3838e" +checksum = "a9c13f843e5e5df3eed270fc42b02923cc1a6b5c7e56b0f3ac1d858ab2c8b5fb" dependencies = [ "auto-launch", - "log", "serde", "serde_json", "tauri", "tauri-plugin", - "thiserror", + "thiserror 2.0.8", ] [[package]] name = "tauri-plugin-dialog" -version = "2.0.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4307310e1d2c09ab110235834722e7c2b85099b683e1eb7342ab351b0be5ada3" +checksum = "8b59fd750551b1066744ab956a1cd6b1ea3e1b3763b0b9153ac27a044d596426" dependencies = [ "log", "raw-window-handle 0.6.2", @@ -4814,15 +4989,15 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror", + "thiserror 2.0.8", "url", ] [[package]] name = "tauri-plugin-fs" -version = "2.0.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba7d46e86db8c830d143ef90ab5a453328365b0cc834c24edea4267b16aba0" +checksum = "a1a1edf18000f02903a7c2e5997fb89aca455ecbc0acc15c6535afbb883be223" dependencies = [ "anyhow", "dunce", @@ -4834,16 +5009,18 @@ dependencies = [ "serde_repr", "tauri", "tauri-plugin", - "thiserror", + "tauri-utils", + "thiserror 2.0.8", + "toml 0.8.19", "url", "uuid", ] [[package]] name = "tauri-plugin-http" -version = "2.0.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c752aee1b00ec3c4d4f440095995d9bd2c640b478f2067d1fba388900b82eb96" +checksum = "e62a9bde54d6a0218b63f5a248f02056ad4316ba6ad81dfb9e4f73715df5deb1" dependencies = [ "data-url", "http", @@ -4855,7 +5032,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror", + "thiserror 2.0.8", "tokio", "url", "urlpattern", @@ -4863,9 +5040,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.0.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad7880c5586b6b2104be451e3d7fc0f3800c84bda69e9ba81c828f87cb34267" +checksum = "bb2c50a63e60fb8925956cc5b7569f4b750ac197a4d39f13b8dd46ea8e2bad79" dependencies = [ "encoding_rs", "log", @@ -4878,46 +5055,46 @@ dependencies = [ "shared_child", "tauri", "tauri-plugin", - "thiserror", + "thiserror 2.0.8", "tokio", ] [[package]] name = "tauri-plugin-single-instance" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a25ac834491d089699a2bc9266a662faf373c9f779f05a2235bc6e4d9e61769a" +checksum = "0f36019ee9832dc99e4450bb55a21cfad8633b19c2c18bd17c7741939b070ede" dependencies = [ - "log", "serde", "serde_json", "tauri", - "thiserror", + "thiserror 2.0.8", + "tracing", "windows-sys 0.59.0", - "zbus", + "zbus 4.4.0", ] [[package]] name = "tauri-plugin-store" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9a580be53f04bb62422d239aa798e88522877f58a0d4a0e745f030055a51bb4" +checksum = "1c0c08fae6995909f5e9a0da6038273b750221319f2c0f3b526d6de1cde21505" dependencies = [ "dunce", - "log", "serde", "serde_json", "tauri", "tauri-plugin", - "thiserror", + "thiserror 2.0.8", "tokio", + "tracing", ] [[package]] name = "tauri-plugin-updater" -version = "2.0.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd3d2fe0f02bf52eebb5a9d23b987fffac6684646ab6fd683d706dafb18da87" +checksum = "b7351014c140906bcfff59d96e04b1170c8f602557f40eb37f7de356d4e7067b" dependencies = [ "base64 0.22.1", "dirs 5.0.1", @@ -4935,7 +5112,7 @@ dependencies = [ "tauri", "tauri-plugin", "tempfile", - "thiserror", + "thiserror 2.0.8", "time", "tokio", "url", @@ -4945,9 +5122,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ef7363e7229ac8d04e8a5d405670dbd43dde8fc4bc3bc56105c35452d03784" +checksum = "cce18d43f80d4aba3aa8a0c953bbe835f3d0f2370aca75e8dbb14bd4bab27958" dependencies = [ "dpi", "gtk", @@ -4957,16 +5134,16 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "thiserror", + "thiserror 2.0.8", "url", "windows", ] [[package]] name = "tauri-runtime-wry" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62fa2068e8498ad007b54d5773d03d57c3ff6dd96f8c8ce58beff44d0d5e0d30" +checksum = "9f442a38863e10129ffe2cec7bd09c2dcf8a098a3a27801a476a304d5bb991d2" dependencies = [ "gtk", "http", @@ -4990,9 +5167,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc65d6f5c54e56b66258948a6d9e47a82ea41f4b5a7612bfbdd1634c2913ed0" +checksum = "9271a88f99b4adea0dc71d0baca4505475a0bbd139fb135f62958721aaa8fe54" dependencies = [ "brotli", "cargo_metadata", @@ -5000,8 +5177,9 @@ dependencies = [ "dunce", "glob", "html5ever", + "http", "infer", - "json-patch 2.0.0", + "json-patch", "kuchikiki", "log", "memchr", @@ -5016,8 +5194,8 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror", - "toml 0.8.2", + "thiserror 2.0.8", + "toml 0.8.19", "url", "urlpattern", "uuid", @@ -5036,9 +5214,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", @@ -5066,32 +5244,62 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a" +dependencies = [ + "thiserror-impl 2.0.8", ] [[package]] name = "thiserror-impl" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", ] [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", - "itoa 1.0.11", + "itoa 1.0.14", "num-conv", "powerfmt", "serde", @@ -5107,14 +5315,24 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -5132,9 +5350,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", @@ -5157,25 +5375,24 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ "rustls", - "rustls-pki-types", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -5198,21 +5415,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.2" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.20.2", + "toml_edit 0.22.22", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] @@ -5223,36 +5440,47 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.0", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.20.2" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.0", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.7.0", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.20", ] [[package]] name = "tower" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 0.1.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -5261,9 +5489,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ "bitflags 2.6.0", "bytes", @@ -5287,9 +5515,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -5298,30 +5526,86 @@ dependencies = [ ] [[package]] -name = "tracing-attributes" -version = "0.1.27" +name = "tracing-appender" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] name = "tray-icon" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c92af36a182b46206723bdf8a7942e20838cde1cf062e5b97854d57eb01763b" +checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b" dependencies = [ "core-graphics 0.24.0", "crossbeam-channel", @@ -5334,7 +5618,7 @@ dependencies = [ "once_cell", "png", "serde", - "thiserror", + "thiserror 1.0.69", "windows-sys 0.59.0", ] @@ -5408,26 +5692,11 @@ dependencies = [ "unic-common", ] -[[package]] -name = "unicode-bidi" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" - [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" - -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-segmentation" @@ -5453,12 +5722,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna", "percent-encoding", "serde", ] @@ -5472,6 +5741,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" @@ -5490,6 +5765,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -5507,6 +5794,12 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -5588,9 +5881,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -5599,36 +5892,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.45" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5636,28 +5929,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wasm-streams" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -5728,9 +6021,19 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -5782,9 +6085,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.6" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" dependencies = [ "rustls-pki-types", ] @@ -5811,7 +6114,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -5820,7 +6123,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886" dependencies = [ - "thiserror", + "thiserror 1.0.69", "windows", "windows-core 0.58.0", ] @@ -5869,7 +6172,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -5932,7 +6235,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -5943,7 +6246,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -6208,6 +6511,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" @@ -6228,13 +6540,26 @@ dependencies = [ ] [[package]] -name = "wry" -version = "0.46.3" +name = "write16" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd5cdf57c66813d97601181349c63b96994b3074fc3d7a31a8cce96e968e3bbd" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wry" +version = "0.47.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ce51277d65170f6379d8cda935c80e3c2d1f0ff712a123c8bddb11b31a4b73" dependencies = [ "base64 0.22.1", "block2", + "cookie", "crossbeam-channel", "dpi", "dunce", @@ -6258,7 +6583,8 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror", + "thiserror 1.0.69", + "url", "webkit2gtk", "webkit2gtk-sys", "webview2-com", @@ -6323,10 +6649,34 @@ dependencies = [ ] [[package]] -name = "zbus" -version = "4.0.1" +name = "yoke" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b8e3d6ae3342792a6cc2340e4394334c7402f3d793b390d2c5494a4032b3030" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" dependencies = [ "async-broadcast", "async-executor", @@ -6338,7 +6688,6 @@ dependencies = [ "async-task", "async-trait", "blocking", - "derivative", "enumflags2", "event-listener", "futures-core", @@ -6352,28 +6701,71 @@ dependencies = [ "serde_repr", "sha1", "static_assertions", - "tokio", "tracing", "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb67eadba43784b6fb14857eba0d8fc518686d3ee537066eb6086dc318e2c8a1" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-util", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.6.20", + "xdg-home", + "zbus_macros 5.2.0", + "zbus_names 4.1.0", + "zvariant 5.1.0", ] [[package]] name = "zbus_macros" -version = "4.0.1" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a3e850ff1e7217a3b7a07eba90d37fe9bb9e89a310f718afcde5885ca9b6d7" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "regex", - "syn 1.0.109", - "zvariant_utils", + "syn 2.0.90", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d49ebc960ceb660f2abe40a5904da975de6986f2af0d7884b39eec6528c57" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.90", + "zbus_names 4.1.0", + "zvariant 5.1.0", + "zvariant_utils 3.0.2", ] [[package]] @@ -6384,7 +6776,19 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.6.20", + "zvariant 5.1.0", ] [[package]] @@ -6405,7 +6809,28 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", ] [[package]] @@ -6425,58 +6850,122 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", ] [[package]] name = "zip" -version = "2.2.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" +checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" dependencies = [ "arbitrary", "crc32fast", "crossbeam-utils", "displaydoc", - "indexmap 2.6.0", + "indexmap 2.7.0", "memchr", - "thiserror", + "thiserror 2.0.8", ] [[package]] name = "zvariant" -version = "4.0.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e09e8be97d44eeab994d752f341e67b3b0d80512a8b315a0671d47232ef1b65" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" dependencies = [ "endi", "enumflags2", "serde", "static_assertions", "url", - "zvariant_derive", + "winnow 0.6.20", + "zvariant_derive 5.1.0", + "zvariant_utils 3.0.2", ] [[package]] name = "zvariant_derive" -version = "4.0.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a5857e2856435331636a9fbb415b09243df4521a267c5bedcd5289b4d5799e" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 1.0.109", - "zvariant_utils", + "syn 2.0.90", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.90", + "zvariant_utils 3.0.2", ] [[package]] name = "zvariant_utils" -version = "1.1.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.90", +] + +[[package]] +name = "zvariant_utils" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.90", + "winnow 0.6.20", ] diff --git a/packages/hoppscotch-agent/src-tauri/Cargo.toml b/packages/hoppscotch-agent/src-tauri/Cargo.toml index 79b8140a..66fec325 100644 --- a/packages/hoppscotch-agent/src-tauri/Cargo.toml +++ b/packages/hoppscotch-agent/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hoppscotch-agent" -version = "0.1.3" +version = "0.1.4" description = "A cross-platform HTTP request agent for Hoppscotch for advanced request handling including custom headers, certificates, proxies, and local system integration." authors = ["AndrewBastin", "CuriousCorrelation"] edition = "2021" @@ -29,9 +29,10 @@ tokio-util = "0.7.12" uuid = { version = "1.11.0", features = [ "v4", "fast-rng" ] } chrono = { version = "0.4", features = ["serde"] } rand = "0.8.5" -log = "0.4.22" -env_logger = "0.11.5" -hoppscotch-relay = { path = "../../hoppscotch-relay" } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "fmt", "std", "time"] } +tracing-appender = "0.2.3" +relay = { git = "https://github.com/CuriousCorrelation/relay.git" } thiserror = "1.0.64" tauri-plugin-store = "2.1.0" x25519-dalek = { version = "2.0.1", features = ["getrandom"] } @@ -43,14 +44,12 @@ lazy_static = "1.5.0" tauri-plugin-single-instance = "2.0.1" tauri-plugin-http = { version = "2.0.1", features = ["gzip"] } native-dialog = "0.7.0" +sha2 = "0.10.8" [target.'cfg(windows)'.dependencies] tempfile = { version = "3.13.0" } winreg = { version = "0.52.0" } -[dev-dependencies] -mockito = "1.5.0" - [features] default = ["tauri-plugin-autostart"] portable = [] diff --git a/packages/hoppscotch-agent/src-tauri/capabilities/default.json b/packages/hoppscotch-agent/src-tauri/capabilities/default.json index fe0f3775..70c49c1c 100644 --- a/packages/hoppscotch-agent/src-tauri/capabilities/default.json +++ b/packages/hoppscotch-agent/src-tauri/capabilities/default.json @@ -18,6 +18,7 @@ "core:default", "shell:allow-open", "core:window:allow-close", + "core:window:allow-hide", "core:window:allow-set-focus", "core:window:allow-set-always-on-top" ] diff --git a/packages/hoppscotch-agent/src-tauri/src/command.rs b/packages/hoppscotch-agent/src-tauri/src/command.rs new file mode 100644 index 00000000..40de8c5b --- /dev/null +++ b/packages/hoppscotch-agent/src-tauri/src/command.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; + +use crate::{ + model::{MaskedRegistration, RegistrationsList}, + state::AppState, + util::generate_auth_key_hash, +}; + +#[tauri::command] +#[tracing::instrument(skip(state))] +pub async fn get_otp(state: tauri::State<'_, Arc>) -> Result, ()> { + tracing::debug!("Retrieving current OTP"); + let otp = state.active_registration_code.read().await.clone(); + if otp.is_some() { + tracing::debug!("OTP found"); + } else { + tracing::debug!("No active OTP"); + } + Ok(otp) +} + +#[tauri::command] +#[tracing::instrument(skip(state))] +pub fn list_registrations(state: tauri::State<'_, Arc>) -> Result { + tracing::debug!("Retrieving registrations list"); + + let masked_registrations = state + .get_registrations() + .iter() + .map(|entry| MaskedRegistration { + registered_at: entry.value().registered_at, + auth_key_hash: generate_auth_key_hash(entry.key()), + }) + .collect(); + + Ok(RegistrationsList { + registrations: masked_registrations, + total: state.get_registrations().len(), + }) +} diff --git a/packages/hoppscotch-agent/src-tauri/src/controller.rs b/packages/hoppscotch-agent/src-tauri/src/controller.rs index 00e0006f..42f26217 100644 --- a/packages/hoppscotch-agent/src-tauri/src/controller.rs +++ b/packages/hoppscotch-agent/src-tauri/src/controller.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use axum::{ body::Bytes, extract::{Path, State}, @@ -8,46 +10,57 @@ use axum_extra::{ headers::{authorization::Bearer, Authorization}, TypedHeader, }; -use hoppscotch_relay::{RequestWithMetadata, ResponseWithMetadata}; -use std::sync::Arc; +use chrono::Utc; +use rand::Rng; +use serde_json::json; use tauri::{AppHandle, Emitter}; +use uuid::Uuid; use x25519_dalek::{EphemeralSecret, PublicKey}; use crate::{ error::{AgentError, AgentResult}, - model::{AuthKeyResponse, ConfirmedRegistrationRequest, HandshakeResponse}, - state::{AppState, Registration}, - util::EncryptedJson, + global::NONCE, + model::{ + AuthKeyResponse, ConfirmedRegistrationRequest, HandshakeResponse, LogEntry, LogLevel, + MaskedRegistration, Registration, + }, + state::AppState, + util::{generate_auth_key_hash, EncryptedJson}, }; -use chrono::Utc; -use rand::Rng; -use serde_json::json; -use uuid::Uuid; +#[tracing::instrument] fn generate_otp() -> String { let otp: u32 = rand::thread_rng().gen_range(0..1_000_000); - - format!("{:06}", otp) + let formatted = format!("{:06}", otp); + tracing::debug!("Generated OTP: {}", formatted); + formatted } +#[tracing::instrument(skip(app_handle))] pub async fn handshake( State((_, app_handle)): State<(Arc, AppHandle)>, ) -> AgentResult> { - Ok(Json(HandshakeResponse { + tracing::info!("Processing handshake request"); + let response = HandshakeResponse { status: "success".to_string(), __hoppscotch__agent__: true, agent_version: app_handle.package_info().version.to_string(), - })) + }; + tracing::info!("Handshake successful"); + Ok(Json(response)) } +#[tracing::instrument(skip(state, app_handle))] pub async fn receive_registration( State((state, app_handle)): State<(Arc, AppHandle)>, ) -> AgentResult> { let otp = generate_otp(); + tracing::info!("Generated new registration OTP"); let mut active_registration_code = state.active_registration_code.write().await; if !active_registration_code.is_none() { + tracing::warn!("Registration attempt while another registration is active"); return Ok(Json( json!({ "message": "There is already an existing registration happening" }), )); @@ -55,32 +68,73 @@ pub async fn receive_registration( *active_registration_code = Some(otp.clone()); - app_handle - .emit("registration_received", otp) - .map_err(|_| AgentError::InternalServerError)?; - - Ok(Json( - json!({ "message": "Registration received and stored" }), - )) + match app_handle.emit("registration-received", otp) { + Ok(_) => { + tracing::info!("Registration event emitted successfully"); + Ok(Json( + json!({ "message": "Registration received and stored" }), + )) + } + Err(e) => { + tracing::error!("Failed to emit registration event: {}", e); + Err(AgentError::InternalServerError) + } + } } +#[tracing::instrument(skip(state, _app_handle))] +pub async fn registration( + State((state, _app_handle)): State<(Arc, AppHandle)>, + TypedHeader(auth_header): TypedHeader>, +) -> AgentResult> { + let token = auth_header.token(); + + if !state.validate_access(token) { + tracing::warn!("Unauthorized attempt to list registrations"); + return Err(AgentError::Unauthorized); + } + + let registration = state + .get_registration(token) + .ok_or(AgentError::Unauthorized)?; + + let key_b16 = registration.shared_secret_b16; + + let registration = MaskedRegistration { + registered_at: registration.registered_at, + auth_key_hash: generate_auth_key_hash(token), + }; + + tracing::info!("Successfully retrieved registrations list"); + Ok(EncryptedJson { + key_b16, + data: registration, + }) +} + +#[tracing::instrument(skip(state, app_handle), fields(auth_key))] pub async fn verify_registration( State((state, app_handle)): State<(Arc, AppHandle)>, Json(confirmed_registration): Json, ) -> AgentResult> { - state + tracing::info!("Verifying registration request"); + + if !state .validate_registration(&confirmed_registration.registration) .await - .then_some(()) - .ok_or(AgentError::InvalidRegistration)?; + { + tracing::warn!("Invalid registration attempt"); + return Err(AgentError::InvalidRegistration); + } let auth_key = Uuid::new_v4().to_string(); let created_at = Utc::now(); - let auth_key_copy = auth_key.clone(); + tracing::Span::current().record("auth_key", &auth_key.as_str()); - let agent_secret_key = EphemeralSecret::random(); - let agent_public_key = PublicKey::from(&agent_secret_key); + let auth_key_copy = auth_key.clone(); + let secret_key = EphemeralSecret::random(); + let public_key = PublicKey::from(&secret_key); let their_public_key = { let public_key_slice: &[u8; 32] = @@ -92,9 +146,9 @@ pub async fn verify_registration( PublicKey::from(public_key_slice.to_owned()) }; - let shared_secret = agent_secret_key.diffie_hellman(&their_public_key); + let shared_secret = secret_key.diffie_hellman(&their_public_key); - let _ = state.update_registrations(app_handle.clone(), |regs| { + if let Err(e) = state.update_registrations(app_handle.clone(), |regs| { regs.insert( auth_key_copy, Registration { @@ -102,82 +156,102 @@ pub async fn verify_registration( shared_secret_b16: base16::encode_lower(shared_secret.as_bytes()), }, ); - })?; + }) { + tracing::error!("Failed to update registrations: {:?}", e); + return Err(e); + } let auth_payload = json!({ "auth_key": auth_key, "created_at": created_at }); - app_handle - .emit("authenticated", &auth_payload) - .map_err(|_| AgentError::InternalServerError)?; + if let Err(e) = app_handle.emit("authenticated", &auth_payload) { + tracing::error!("Failed to emit authenticated event: {:?}", e); + return Err(AgentError::InternalServerError); + } + let _ = state.clear_active_registration().await; + + tracing::info!("Registration verified successfully"); Ok(Json(AuthKeyResponse { auth_key, created_at, - agent_public_key_b16: base16::encode_lower(agent_public_key.as_bytes()), + agent_public_key_b16: base16::encode_lower(public_key.as_bytes()), })) } -pub async fn run_request( - State((state, _app_handle)): State<(Arc, T)>, +#[tracing::instrument(skip(state, app_handle), fields(auth_key = %auth_key))] +pub async fn delete_registration( + State((state, app_handle)): State<(Arc, AppHandle)>, + TypedHeader(auth_header): TypedHeader>, + Path(auth_key): Path, +) -> AgentResult> { + if !state.validate_access(auth_header.token()) { + tracing::warn!("Unauthorized deletion attempt"); + return Err(AgentError::Unauthorized); + } + + let _removed = state.update_registrations(app_handle.clone(), |regs| { + regs.remove(&auth_key); + })?; + + tracing::info!("Registration deleted successfully"); + let message = format!("{} registration deleted successfully", auth_key); + Ok(Json(json!({ "message": message }))) +} + +#[tracing::instrument(skip(state, body, _app_handle), fields(req_id))] +pub async fn execute( + State((state, _app_handle)): State<(Arc, AppHandle)>, TypedHeader(auth_header): TypedHeader>, headers: HeaderMap, body: Bytes, -) -> AgentResult> { - let nonce = headers - .get("X-Hopp-Nonce") - .ok_or(AgentError::Unauthorized)? - .to_str() - .map_err(|_| AgentError::Unauthorized)?; - - let req: RequestWithMetadata = state - .validate_access_and_get_data(auth_header.token(), nonce, &body) - .ok_or(AgentError::Unauthorized)?; - - let req_id = req.req_id; - - let reg_info = state - .get_registration_info(auth_header.token()) - .ok_or(AgentError::Unauthorized)?; - - let cancel_token = tokio_util::sync::CancellationToken::new(); - state.add_cancellation_token(req.req_id, cancel_token.clone()); - - let cancel_token_clone = cancel_token.clone(); - // Execute the HTTP request in a blocking thread pool and handles cancellation. - // - // It: - // 1. Uses `spawn_blocking` to run the sync `run_request_task` - // without blocking the main Tokio runtime. - // 2. Uses `select!` to concurrently wait for either - // a. the task to complete, - // b. or a cancellation signal. - // - // Why spawn_blocking? - // - `run_request_task` uses synchronous curl operations which would block - // the async runtime if not run in a separate thread. - // - `spawn_blocking` moves this operation to a thread pool designed for - // blocking tasks, so other async operations to continue unblocked. - let result = tokio::select! { - res = tokio::task::spawn_blocking(move || hoppscotch_relay::run_request_task(&req, cancel_token_clone)) => { - match res { - Ok(task_result) => Ok(task_result?), - Err(_) => Err(AgentError::InternalServerError), +) -> AgentResult> { + let nonce = match headers.get(NONCE) { + Some(n) => match n.to_str() { + Ok(n) => n, + Err(_) => { + tracing::warn!("Invalid nonce header"); + return Err(AgentError::Unauthorized); } }, - _ = cancel_token.cancelled() => { - Err(AgentError::RequestCancelled) + None => { + tracing::warn!("Missing nonce header"); + return Err(AgentError::Unauthorized); } }; - state.remove_cancellation_token(req_id); + let request = match state.validate_access_and_get_data::( + auth_header.token(), + nonce, + &body, + ) { + Some(r) => r, + None => { + tracing::warn!("Invalid access or data"); + return Err(AgentError::Unauthorized); + } + }; - result.map(|val| EncryptedJson { - key_b16: reg_info.shared_secret_b16, - data: val, - }) + let request_id = request.id; + + tracing::Span::current().record("request_id", &request_id); + + let reg_info = match state.get_registration(auth_header.token()) { + Some(r) => r, + None => { + tracing::warn!("Registration info not found"); + return Err(AgentError::Unauthorized); + } + }; + + Ok(relay::execute(request) + .await + .map(|response| EncryptedJson { + key_b16: reg_info.shared_secret_b16, + data: response, + })?) } /// Provides a way for registered clients to check if their @@ -187,34 +261,139 @@ pub async fn run_request( /// registration, the client also needs the shared secret to verify /// if the read fails, or the auth_key didn't validate and this route returns /// undefined, we can count on the registration not being valid anymore. +#[tracing::instrument(skip(state, _app_handle))] pub async fn registered_handshake( - State((state, _)): State<(Arc, AppHandle)>, + State((state, _app_handle)): State<(Arc, AppHandle)>, TypedHeader(auth_header): TypedHeader>, ) -> AgentResult> { - let reg_info = state.get_registration_info(auth_header.token()); + let reg_info = state.get_registration(auth_header.token()); match reg_info { - Some(reg) => Ok(EncryptedJson { - key_b16: reg.shared_secret_b16, - data: json!(true), - }), - None => Err(AgentError::Unauthorized), + Some(reg) => { + tracing::info!("Handshake successful"); + Ok(EncryptedJson { + key_b16: reg.shared_secret_b16, + data: json!(true), + }) + } + None => { + tracing::warn!("Unauthorized handshake attempt"); + Err(AgentError::Unauthorized) + } } } -pub async fn cancel_request( - State((state, _app_handle)): State<(Arc, T)>, +#[tracing::instrument(skip(state, _app_handle), fields(request_id = %request_id))] +pub async fn cancel( + State((state, _app_handle)): State<(Arc, AppHandle)>, TypedHeader(auth_header): TypedHeader>, - Path(req_id): Path, + Path(request_id): Path, ) -> AgentResult> { if !state.validate_access(auth_header.token()) { + tracing::warn!("Unauthorized cancellation attempt"); return Err(AgentError::Unauthorized); } - if let Some((_, token)) = state.remove_cancellation_token(req_id) { - token.cancel(); + if let Ok(()) = relay::cancel(request_id.try_into().unwrap()).await { + tracing::info!("Request cancelled successfully"); Ok(Json(json!({"message": "Request cancelled successfully"}))) } else { + tracing::warn!("Request not found"); Err(AgentError::RequestNotFound) } } + +#[tracing::instrument(skip_all)] +pub async fn log_sink( + State((state, _app_handle)): State<(Arc, AppHandle)>, + TypedHeader(auth_header): TypedHeader>, + headers: HeaderMap, + body: Bytes, +) -> AgentResult> { + if !state.validate_access(auth_header.token()) { + tracing::warn!("Unauthorized log sink access attempt"); + return Err(AgentError::Unauthorized); + } + + let nonce = match headers.get(NONCE) { + Some(n) => match n.to_str() { + Ok(n) => n, + Err(_) => { + tracing::warn!("Invalid nonce header"); + return Err(AgentError::Unauthorized); + } + }, + None => { + tracing::warn!("Missing nonce header"); + return Err(AgentError::Unauthorized); + } + }; + + let log_entry: LogEntry = + match state.validate_access_and_get_data(auth_header.token(), nonce, &body) { + Some(entry) => entry, + None => { + tracing::warn!("Failed to decrypt or parse log entry"); + return Err(AgentError::BadRequest("Invalid log entry format".into())); + } + }; + + let metadata_str = log_entry + .metadata + .map(|m| m.to_string()) + .unwrap_or_default(); + + let correlation = log_entry.correlation_id.unwrap_or_default(); + + match log_entry.level { + LogLevel::Debug => { + tracing::debug!( + timestamp = %log_entry.timestamp, + context = %log_entry.context, + source = %log_entry.source, + metadata = %metadata_str, + correlation_id = %correlation, + "{}", + log_entry.message + ); + } + LogLevel::Info => { + tracing::info!( + timestamp = %log_entry.timestamp, + context = %log_entry.context, + source = %log_entry.source, + metadata = %metadata_str, + correlation_id = %correlation, + "{}", + log_entry.message + ); + } + LogLevel::Warn => { + tracing::warn!( + timestamp = %log_entry.timestamp, + context = %log_entry.context, + source = %log_entry.source, + metadata = %metadata_str, + correlation_id = %correlation, + "{}", + log_entry.message + ); + } + LogLevel::Error => { + tracing::error!( + timestamp = %log_entry.timestamp, + context = %log_entry.context, + source = %log_entry.source, + metadata = %metadata_str, + correlation_id = %correlation, + "{}", + log_entry.message + ); + } + } + + Ok(Json(json!({ + "status": "success", + "message": "Log entry processed" + }))) +} diff --git a/packages/hoppscotch-agent/src-tauri/src/dialog.rs b/packages/hoppscotch-agent/src-tauri/src/dialog.rs index 5b8a19ec..e4967b43 100644 --- a/packages/hoppscotch-agent/src-tauri/src/dialog.rs +++ b/packages/hoppscotch-agent/src-tauri/src/dialog.rs @@ -10,13 +10,13 @@ pub fn panic(msg: &str) { .show_alert() .unwrap_or_default(); - log::error!("{}: {}", FATAL_ERROR, msg); + tracing::error!("{}: {}", FATAL_ERROR, msg); panic!("{}: {}", FATAL_ERROR, msg); } pub fn info(msg: &str) { - log::info!("{}", msg); + tracing::info!("{}", msg); MessageDialog::new() .set_type(MessageType::Info) @@ -27,7 +27,7 @@ pub fn info(msg: &str) { } pub fn warn(msg: &str) { - log::warn!("{}", msg); + tracing::warn!("{}", msg); MessageDialog::new() .set_type(MessageType::Warning) @@ -38,7 +38,7 @@ pub fn warn(msg: &str) { } pub fn error(msg: &str) { - log::error!("{}", msg); + tracing::error!("{}", msg); MessageDialog::new() .set_type(MessageType::Error) diff --git a/packages/hoppscotch-agent/src-tauri/src/error.rs b/packages/hoppscotch-agent/src-tauri/src/error.rs index 697b3dd3..638a29f3 100644 --- a/packages/hoppscotch-agent/src-tauri/src/error.rs +++ b/packages/hoppscotch-agent/src-tauri/src/error.rs @@ -8,6 +8,10 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum AgentError { + #[error("FATAL: No `main` window found")] + NoMainWindow, + #[error("Tauri error: {0}")] + Tauri(#[from] tauri::Error), #[error("Invalid Registration")] InvalidRegistration, #[error("Invalid Client Public Key")] @@ -45,7 +49,13 @@ pub enum AgentError { #[error("Store error: {0}")] TauriPluginStore(#[from] tauri_plugin_store::Error), #[error("Relay error: {0}")] - Relay(#[from] hoppscotch_relay::RelayError), + Relay(#[from] relay::error::RelayError), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Log init error: {0}")] + LogInit(#[from] tracing_appender::rolling::InitError), + #[error("Log init global error: {0}")] + LogInitGlobal(#[from] tracing::subscriber::SetGlobalDefaultError), } impl IntoResponse for AgentError { @@ -55,7 +65,9 @@ impl IntoResponse for AgentError { AgentError::InvalidClientPublicKey => (StatusCode::BAD_REQUEST, self.to_string()), AgentError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()), AgentError::RequestNotFound => (StatusCode::NOT_FOUND, self.to_string()), - AgentError::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), + AgentError::InternalServerError => { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()) + } AgentError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), AgentError::ClientCertError => (StatusCode::BAD_REQUEST, self.to_string()), AgentError::RootCertError => (StatusCode::BAD_REQUEST, self.to_string()), diff --git a/packages/hoppscotch-agent/src-tauri/src/global.rs b/packages/hoppscotch-agent/src-tauri/src/global.rs index feaf7fe8..002729db 100644 --- a/packages/hoppscotch-agent/src-tauri/src/global.rs +++ b/packages/hoppscotch-agent/src-tauri/src/global.rs @@ -1,2 +1,3 @@ pub const AGENT_STORE: &str = "app_data.bin"; pub const REGISTRATIONS: &str = "registrations"; +pub const NONCE: &str = "X-Hopp-Nonce"; diff --git a/packages/hoppscotch-agent/src-tauri/src/lib.rs b/packages/hoppscotch-agent/src-tauri/src/lib.rs index b8312eb0..e7ead68f 100644 --- a/packages/hoppscotch-agent/src-tauri/src/lib.rs +++ b/packages/hoppscotch-agent/src-tauri/src/lib.rs @@ -1,3 +1,4 @@ +pub mod command; pub mod controller; pub mod dialog; pub mod error; @@ -11,59 +12,105 @@ pub mod updater; pub mod util; pub mod webview; -use log::{error, info}; use std::sync::Arc; -use tauri::{Emitter, Listener, Manager, WebviewWindowBuilder}; +use tauri::{AppHandle, Emitter, Listener, Manager, WebviewWindowBuilder}; use tauri_plugin_updater::UpdaterExt; use tokio_util::sync::CancellationToken; +use tracing_subscriber::{fmt::format::JsonFields, EnvFilter}; -use model::Payload; +use error::{AgentError, AgentResult}; +use model::{LogGuard, Payload}; use state::AppState; -#[tauri::command] -async fn get_otp(state: tauri::State<'_, Arc>) -> Result, ()> { - Ok(state.active_registration_code.read().await.clone()) +#[tracing::instrument(skip(app_handle))] +fn create_main_window(app_handle: &AppHandle) -> AgentResult<()> { + tracing::info!("Creating main application window"); + + let main = &app_handle + .config() + .app + .windows + .first() + .ok_or(AgentError::NoMainWindow)?; + + tracing::debug!("Building webview window from config"); + let window = WebviewWindowBuilder::from_config(app_handle, main)?.build()?; + + window.hide()?; + + tracing::info!("Main window created successfully"); + Ok(()) +} + +#[tracing::instrument(skip(app_handle))] +pub fn show_main_window(app_handle: &AppHandle) -> AgentResult<()> { + tracing::debug!("Attempting to show main window"); + if let Some(window) = app_handle.get_webview_window("main") { + window.show()?; + window.set_focus()?; + tracing::info!("Main window shown and focused"); + } + Ok(()) +} + +#[tracing::instrument(skip(app_handle))] +pub fn hide_main_window(app_handle: &AppHandle) -> AgentResult<()> { + tracing::debug!("Attempting to hide main window"); + if let Some(window) = app_handle.get_webview_window("main") { + window.hide()?; + tracing::info!("Main window hidden"); + } + Ok(()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - env_logger::init(); + tracing::info!("Initializing Hoppscotch Agent"); // The installer takes care of installing `WebView`, // this check is only required for portable variant. #[cfg(all(feature = "portable", windows))] - webview::init_webview(); + { + tracing::debug!("Checking WebView initialization for portable Windows variant"); + webview::init_webview(); + } let cancellation_token = CancellationToken::new(); let server_cancellation_token = cancellation_token.clone(); - tauri::Builder::default() + tracing::debug!("Building Tauri application"); + let builder = tauri::Builder::default() // NOTE: Currently, plugins run in the order they were added in to the builder, // so `tauri_plugin_single_instance` needs to be registered first. // See: https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/single-instance .plugin(tauri_plugin_single_instance::init(|app, args, cwd| { - info!("{}, {args:?}, {cwd}", app.package_info().name); + tracing::info!( + app_name = %app.package_info().name, + "Single instance handler triggered" + ); - app.emit("single-instance", Payload::new(args, cwd)) - .unwrap(); + if let Err(e) = app.emit("single-instance", Payload::new(args, cwd)) { + tracing::error!(error = %e, "Failed to emit single-instance event"); + } // Application is already running, bring it to foreground. - if let Some(window) = app.get_webview_window("main") { - let _ = window.show(); - let _ = window.set_focus(); - } else { - error!("Failed to get `main` window"); + if let Err(e) = show_main_window(&app) { + tracing::error!(error = %e, "Failed to show window"); } })) .plugin(tauri_plugin_store::Builder::new().build()) .setup(move |app| { - let app_handle = app.app_handle(); + // let _ = setup_logging(&app.handle())?; + + tracing::info!("Setting up application"); + let app_handle = app.handle(); #[cfg(all(desktop, not(feature = "portable")))] { use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::ManagerExt; + tracing::debug!("Configuring autostart for desktop variant"); let _ = app.handle().plugin(tauri_plugin_autostart::init( MacosLauncher::LaunchAgent, None, @@ -71,30 +118,29 @@ pub fn run() { let autostart_manager = app.autolaunch(); - println!( - "autostart enabled: {}", - autostart_manager.is_enabled().unwrap() + tracing::info!( + enabled = autostart_manager.is_enabled().unwrap_or(false), + "Checking autostart status" ); - if !autostart_manager.is_enabled().unwrap() { - let _ = autostart_manager.enable(); - println!( - "autostart updated: {}", - autostart_manager.is_enabled().unwrap() - ); + if !autostart_manager.is_enabled().unwrap_or(false) { + if let Err(e) = autostart_manager.enable() { + tracing::error!(error = %e, "Failed to enable autostart"); + } else { + tracing::info!("Autostart enabled successfully"); + } } }; #[cfg(desktop)] { + tracing::debug!("Initializing desktop-specific features"); let _ = app .handle() .plugin(tauri_plugin_updater::Builder::new().build()); - let _ = app.handle().plugin(tauri_plugin_dialog::init()); let updater = app.updater_builder().build().unwrap(); - let app_handle_ref = app_handle.clone(); tauri::async_runtime::spawn_blocking(|| { @@ -104,20 +150,24 @@ pub fn run() { }); }; - let app_state = Arc::new(AppState::new(app_handle.clone())?); + // Create and hide the main window during setup. + create_main_window(&app_handle)?; + tracing::debug!("Initializing application state"); + let app_state = Arc::new(AppState::new(app_handle.clone())?); app.manage(app_state.clone()); let server_cancellation_token = server_cancellation_token.clone(); - let server_app_handle = app_handle.clone(); + tracing::debug!("Spawning server process"); tauri::async_runtime::spawn(async move { server::run_server(app_state, server_cancellation_token, server_app_handle).await; }); #[cfg(all(desktop))] { + tracing::debug!("Creating system tray"); let handle = app.handle(); tray::create_tray(handle)?; } @@ -125,54 +175,116 @@ pub fn run() { // Blocks the app from populating the macOS dock #[cfg(target_os = "macos")] { + tracing::debug!("Setting macOS activation policy"); app_handle .set_activation_policy(tauri::ActivationPolicy::Accessory) .unwrap(); }; let app_handle_ref = app_handle.clone(); - - app_handle.listen("registration_received", move |_| { - WebviewWindowBuilder::from_config( - &app_handle_ref, - &app_handle_ref.config().app.windows[0], - ) - .unwrap() - .build() - .unwrap() - .show() - .unwrap(); + app_handle.listen("registration-received", move |_| { + tracing::info!("Registration received event triggered"); + if let Err(e) = show_main_window(&app_handle_ref) { + tracing::error!(error = %e, "Failed to show window"); + } }); + tracing::info!("Application setup completed successfully"); Ok(()) }) .manage(cancellation_token) .on_window_event(|window, event| { match &event { - tauri::WindowEvent::CloseRequested { .. } => { + tauri::WindowEvent::CloseRequested { api, .. } => { + tracing::info!("Window close requested"); + api.prevent_close(); + + if let Err(e) = window.hide() { + tracing::error!(error = %e, "Failed to hide window"); + } + let app_state = window.state::>(); - let mut current_code = app_state.active_registration_code.blocking_write(); - if current_code.is_some() { + tracing::debug!("Clearing active registration code"); *current_code = None; } + + if let Err(e) = window.emit("window-hidden", ()) { + tracing::error!(error = %e, "Failed to emit window-hidden event"); + } + } + _ => { + tracing::debug!(event = ?event, "Window event received"); } - _ => {} }; }) - .invoke_handler(tauri::generate_handler![get_otp]) + .invoke_handler(tauri::generate_handler![ + command::get_otp, + command::list_registrations + ]); + + tracing::info!("Building Tauri application with context"); + let app = builder .build(tauri::generate_context!()) - .expect("error while building tauri application") - .run(|app_handle, event| match event { - tauri::RunEvent::ExitRequested { api, code, .. } => { - if code.is_none() || matches!(code, Some(0)) { - api.prevent_exit() - } else if code.is_some() { - let state = app_handle.state::(); - state.cancel(); - } + .expect("error while building tauri application"); + + tracing::info!("Running application"); + app.run(|app_handle, event| match event { + tauri::RunEvent::ExitRequested { api, code, .. } => { + if code.is_none() || matches!(code, Some(0)) { + tracing::info!("Exit requested, preventing immediate exit"); + api.prevent_exit(); + } else if code.is_some() { + tracing::info!("Exit with non-zero code requested, initiating shutdown"); + let state = app_handle.state::(); + state.cancel(); } - _ => {} - }); + } + _ => {} + }); +} + +#[tracing::instrument(skip(app_handle))] +pub fn setup_logging(app_handle: &AppHandle) -> AgentResult<()> { + tracing::info!("Setting up logging system"); + + let app_data_dir = app_handle.path().app_data_dir()?; + tracing::debug!(path = ?app_data_dir, "Creating app data directory"); + std::fs::create_dir_all(&app_data_dir)?; + + tracing::debug!("Configuring file appender"); + let file_appender = tracing_appender::rolling::RollingFileAppender::builder() + .rotation(tracing_appender::rolling::Rotation::DAILY) + .filename_prefix("hoppscotch-agent") + .filename_suffix("log") + .max_log_files(1) + .build(&app_data_dir)?; + + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + + tracing::debug!("Building subscriber with JSON formatting"); + let subscriber = tracing_subscriber::fmt() + .fmt_fields(JsonFields::new()) + .with_target(false) + .with_writer(non_blocking) + .with_ansi(false) + .with_timer(tracing_subscriber::fmt::time::UtcTime::rfc_3339()) + .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| { + if cfg!(debug_assertions) { + "debug" + } else { + "info" + } + .into() + })) + .with_filter_reloading() + .finish(); + + tracing::subscriber::set_global_default(subscriber)?; + + app_handle.manage(LogGuard(_guard)); + + tracing::info!("Logging system initialized successfully"); + Ok(()) } diff --git a/packages/hoppscotch-agent/src-tauri/src/main.rs b/packages/hoppscotch-agent/src-tauri/src/main.rs index bf757fbd..8276f5b5 100644 --- a/packages/hoppscotch-agent/src-tauri/src/main.rs +++ b/packages/hoppscotch-agent/src-tauri/src/main.rs @@ -1,6 +1,18 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + fn main() { + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()), + ) + .with(tracing_subscriber::fmt::layer().without_time()) + .init(); + + tracing::info!("Starting Hoppscotch Agent..."); + hoppscotch_agent_lib::run() } diff --git a/packages/hoppscotch-agent/src-tauri/src/model.rs b/packages/hoppscotch-agent/src-tauri/src/model.rs index 68589fe9..718422e9 100644 --- a/packages/hoppscotch-agent/src-tauri/src/model.rs +++ b/packages/hoppscotch-agent/src-tauri/src/model.rs @@ -1,5 +1,42 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sha2::{Digest, Sha256}; + +/// Describes one registered app instance +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Registration { + pub registered_at: DateTime, + + /// base16 (lowercase) encoded shared secret that the client + /// and agent established during registration that is used + /// to encrypt traffic between them + pub shared_secret_b16: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct MaskedRegistration { + pub registered_at: DateTime, + pub auth_key_hash: String, +} + +impl From<(&String, &Registration)> for MaskedRegistration { + fn from((key, registration): (&String, &Registration)) -> Self { + let hash = Sha256::digest(key.as_bytes()); + let short_hash = base16::encode_lower(&hash[..3]); + + Self { + registered_at: registration.registered_at, + auth_key_hash: short_hash, + } + } +} + +#[derive(Debug, Serialize)] +pub struct RegistrationsList { + pub registrations: Vec, + pub total: usize, +} /// Single instance payload. #[derive(Clone, Serialize)] @@ -45,3 +82,29 @@ pub struct AuthKeyResponse { /// and client after registration pub agent_public_key_b16: String, } + +/// A logger guard, managed by tauri runtime to make sure +/// logger doesn't get cleaned up or dropped during app's run time. +pub struct LogGuard(pub tracing_appender::non_blocking::WorkerGuard); + +#[derive(Debug, Deserialize)] +pub struct LogEntry { + pub timestamp: String, + pub level: LogLevel, + pub context: String, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + pub source: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub correlation_id: Option, +} + +#[derive(Debug, Deserialize, Clone, Copy)] +#[serde(rename_all = "UPPERCASE")] +pub enum LogLevel { + Debug, + Info, + Warn, + Error, +} diff --git a/packages/hoppscotch-agent/src-tauri/src/route.rs b/packages/hoppscotch-agent/src-tauri/src/route.rs index 29066812..e6362565 100644 --- a/packages/hoppscotch-agent/src-tauri/src/route.rs +++ b/packages/hoppscotch-agent/src-tauri/src/route.rs @@ -1,5 +1,5 @@ use axum::{ - routing::{get, post}, + routing::{delete, get, post}, Router, }; use std::sync::Arc; @@ -22,7 +22,13 @@ pub fn route(state: Arc, app_handle: AppHandle) -> Router { "/registered-handshake", get(controller::registered_handshake), ) - .route("/request", post(controller::run_request)) - .route("/cancel-request/:req_id", post(controller::cancel_request)) + .route("/registration", get(controller::registration)) + .route( + "/registrations/:auth_key", + delete(controller::delete_registration), + ) + .route("/execute", post(controller::execute)) + .route("/cancel/:req_id", post(controller::cancel)) + .route("/log-sink", post(controller::log_sink)) .with_state((state, app_handle)) } diff --git a/packages/hoppscotch-agent/src-tauri/src/server.rs b/packages/hoppscotch-agent/src-tauri/src/server.rs index 1e9210fa..33818477 100644 --- a/packages/hoppscotch-agent/src-tauri/src/server.rs +++ b/packages/hoppscotch-agent/src-tauri/src/server.rs @@ -6,11 +6,13 @@ use tower_http::cors::CorsLayer; use crate::route; use crate::state::AppState; +#[tracing::instrument(skip(state, cancellation_token, app_handle))] pub async fn run_server( state: Arc, cancellation_token: CancellationToken, app_handle: tauri::AppHandle, ) { + tracing::info!("Initializing server"); let cors = CorsLayer::permissive(); let app = Router::new() @@ -18,17 +20,31 @@ pub async fn run_server( .layer(cors); let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 9119)); + tracing::info!(address = %addr, "Starting server"); - println!("Server running on http://{}", addr); + match tokio::net::TcpListener::bind(&addr).await { + Ok(listener) => { + tracing::info!(address = %addr, "Server bound successfully"); - let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + if let Err(e) = axum::serve(listener, app.into_make_service()) + .with_graceful_shutdown(async move { + cancellation_token.cancelled().await; + tracing::info!("Graceful shutdown initiated"); + }) + .await + { + tracing::error!(error = %e, "Server error occurred"); + return; + } - axum::serve(listener, app.into_make_service()) - .with_graceful_shutdown(async move { - cancellation_token.cancelled().await; - }) - .await - .unwrap(); - - println!("Server shut down"); + tracing::info!("Server shut down successfully"); + } + Err(e) => { + tracing::error!( + error = %e, + address = %addr, + "Failed to bind server to address" + ); + } + } } diff --git a/packages/hoppscotch-agent/src-tauri/src/state.rs b/packages/hoppscotch-agent/src-tauri/src/state.rs index c8e8c928..4bd211fb 100644 --- a/packages/hoppscotch-agent/src-tauri/src/state.rs +++ b/packages/hoppscotch-agent/src-tauri/src/state.rs @@ -1,8 +1,7 @@ use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit}; use axum::body::Bytes; -use chrono::{DateTime, Utc}; use dashmap::DashMap; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde::de::DeserializeOwned; use tauri_plugin_store::StoreExt; use tokio::sync::RwLock; use tokio_util::sync::CancellationToken; @@ -10,20 +9,10 @@ use tokio_util::sync::CancellationToken; use crate::{ error::{AgentError, AgentResult}, global::{AGENT_STORE, REGISTRATIONS}, + model::Registration, }; -/// Describes one registered app instance -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Registration { - pub registered_at: DateTime, - - /// base16 (lowercase) encoded shared secret that the client - /// and agent established during registration that is used - /// to encrypt traffic between them - pub shared_secret_b16: String, -} - -#[derive(Default)] +#[derive(Debug, Default)] pub struct AppState { /// The active registration code that is being registered. pub active_registration_code: RwLock>, @@ -37,19 +26,36 @@ pub struct AppState { } impl AppState { + #[tracing::instrument(skip(app_handle))] pub fn new(app_handle: tauri::AppHandle) -> AgentResult { - let store = app_handle.store(AGENT_STORE)?; + tracing::info!("Initializing application state"); + let store = match app_handle.store(AGENT_STORE) { + Ok(store) => store, + Err(e) => { + tracing::error!("Failed to access app store: {}", e); + return Err(e.into()); + } + }; // Try loading and parsing registrations from the store, if that failed, // load the default list let registrations = store .get(REGISTRATIONS) .and_then(|val| serde_json::from_value(val.clone()).ok()) - .unwrap_or_else(|| DashMap::new()); + .unwrap_or_else(|| { + tracing::debug!("No existing registrations found, initializing empty map"); + DashMap::new() + }); // Try to save the latest registrations list let _ = store.set(REGISTRATIONS, serde_json::to_value(®istrations)?); - let _ = store.save(); + + if let Err(e) = store.save() { + tracing::error!("Failed to persist store changes: {}", e); + return Err(e.into()); + } + + tracing::info!("Application state initialized successfully"); Ok(Self { active_registration_code: RwLock::new(None), @@ -62,7 +68,9 @@ impl AppState { /// NOTE: Although DashMap API allows you to update the list from an immutable /// reference, you shouldn't do it for registrations as `update_registrations` /// performs save operation that needs to be done and should be used instead + #[tracing::instrument] pub fn get_registrations(&self) -> &DashMap { + tracing::debug!("Retrieving registrations list"); &self.registrations } @@ -71,60 +79,117 @@ impl AppState { /// This function bypasses `store.reload()` to avoid issues from stale or inconsistent /// data on disk. By relying solely on the in-memory `self.registrations`, /// we make sure that updates are applied based on the most recent changes in memory. + #[tracing::instrument(skip(self, app_handle, update_func))] pub fn update_registrations( &self, app_handle: tauri::AppHandle, update_func: impl FnOnce(&DashMap), ) -> Result<(), AgentError> { + tracing::info!("Updating registrations"); update_func(&self.registrations); - let store = app_handle.store(AGENT_STORE)?; + let store = match app_handle.store(AGENT_STORE) { + Ok(store) => store, + Err(e) => { + tracing::error!("Failed to access app store: {}", e); + return Err(e.into()); + } + }; if store.has(REGISTRATIONS) { + tracing::debug!("Clearing existing registrations from store"); // We've confirmed `REGISTRATIONS` exists in the store - store - .delete(REGISTRATIONS) - .then_some(()) - .ok_or(AgentError::RegistrationClearError)?; + if !store.delete(REGISTRATIONS) { + tracing::error!("Failed to clear existing registrations"); + return Err(AgentError::RegistrationClearError); + } } else { - log::debug!("`REGISTRATIONS` key not found in store; continuing with update."); + tracing::debug!("`REGISTRATIONS` key not found in store; continuing with update."); } // Since we've established `self.registrations` as the source of truth, // we avoid reloading the store from disk and instead choose to override it. - - store.set( - REGISTRATIONS, - serde_json::to_value(self.registrations.clone())?, - ); + match serde_json::to_value(self.registrations.clone()) { + Ok(value) => { + let _ = store.set(REGISTRATIONS, value); + } + Err(e) => { + tracing::error!("Failed to serialize registrations: {}", e); + return Err(e.into()); + } + } // Explicitly save the changes - store.save()?; + if let Err(e) = store.save() { + tracing::error!("Failed to persist store changes: {}", e); + return Err(e.into()); + } + tracing::info!("Registrations updated successfully"); Ok(()) } /// Clear all the registrations + #[tracing::instrument(skip(self, app_handle))] pub fn clear_registrations(&self, app_handle: tauri::AppHandle) -> Result<(), AgentError> { - Ok(self.update_registrations(app_handle, |registrations| registrations.clear())?) + tracing::info!("Clearing all registrations"); + self.update_registrations(app_handle, |registrations| registrations.clear())?; + tracing::info!("All registrations cleared successfully"); + Ok(()) } + #[tracing::instrument(skip(self))] + pub async fn clear_active_registration(&self) { + tracing::debug!("Clearing active registration code"); + let mut active_registration_code = self.active_registration_code.write().await; + *active_registration_code = None; + tracing::debug!("Active registration code cleared"); + } + + #[tracing::instrument(skip(self))] pub async fn validate_registration(&self, registration: &str) -> bool { - self.active_registration_code.read().await.as_deref() == Some(registration) + tracing::debug!("Validating registration code"); + let is_valid = self.active_registration_code.read().await.as_deref() == Some(registration); + if is_valid { + tracing::info!("Registration code validated successfully"); + } else { + tracing::warn!("Invalid registration code provided"); + } + is_valid } + #[tracing::instrument(skip(self))] pub fn remove_cancellation_token(&self, req_id: usize) -> Option<(usize, CancellationToken)> { - self.cancellation_tokens.remove(&req_id) + tracing::debug!(req_id, "Removing cancellation token"); + let result = self.cancellation_tokens.remove(&req_id); + if result.is_some() { + tracing::info!(req_id, "Cancellation token removed successfully"); + } else { + tracing::debug!(req_id, "No cancellation token found to remove"); + } + result } - pub fn add_cancellation_token(&self, req_id: usize, cancellation_tokens: CancellationToken) { - self.cancellation_tokens.insert(req_id, cancellation_tokens); + #[tracing::instrument(skip(self))] + pub fn add_cancellation_token(&self, req_id: usize, cancellation_token: CancellationToken) { + tracing::debug!(req_id, "Adding new cancellation token"); + self.cancellation_tokens.insert(req_id, cancellation_token); + tracing::debug!(req_id, "Cancellation token added successfully"); } + #[tracing::instrument(skip(self))] pub fn validate_access(&self, auth_key: &str) -> bool { - self.registrations.get(auth_key).is_some() + tracing::debug!(auth_key, "Validating access"); + let is_valid = self.registrations.get(auth_key).is_some(); + if is_valid { + tracing::info!(auth_key, "Access validated successfully"); + } else { + tracing::warn!(auth_key, "Invalid access attempt"); + } + is_valid } + #[tracing::instrument(skip(self, data))] pub fn validate_access_and_get_data( &self, auth_key: &str, @@ -134,28 +199,79 @@ impl AppState { where T: DeserializeOwned, { - if let Some(registration) = self.registrations.get(auth_key) { - let key: [u8; 32] = base16::decode(®istration.shared_secret_b16).ok()?[0..32] - .try_into() - .ok()?; + tracing::debug!( + auth_key, + nonce_len = nonce.len(), + "Validating access and decrypting data" + ); - let nonce: [u8; 12] = base16::decode(nonce).ok()?[0..12].try_into().ok()?; + let registration = match self.registrations.get(auth_key) { + Some(reg) => reg, + None => { + tracing::warn!(auth_key, "Registration not found"); + return None; + } + }; - let cipher = Aes256Gcm::new(&key.into()); + let key: [u8; 32] = match base16::decode(®istration.shared_secret_b16).ok()?[0..32] + .try_into() + .ok() + { + Some(k) => k, + None => { + tracing::error!(auth_key, "Failed to decode shared secret"); + return None; + } + }; - let data = data.iter().cloned().collect::>(); + let nonce: [u8; 12] = match base16::decode(nonce).ok()?[0..12].try_into().ok() { + Some(n) => n, + None => { + tracing::error!(auth_key, "Failed to decode nonce"); + return None; + } + }; - let plain_data = cipher.decrypt(&nonce.into(), data.as_slice()).ok()?; + let cipher = Aes256Gcm::new(&key.into()); + let data = data.iter().cloned().collect::>(); - serde_json::from_reader(plain_data.as_slice()).ok() - } else { - None + let plain_data = match cipher.decrypt(&nonce.into(), data.as_slice()) { + Ok(d) => d, + Err(e) => { + tracing::error!(auth_key, error = ?e, "Decryption failed"); + return None; + } + }; + + match serde_json::from_reader(plain_data.as_slice()) { + Ok(result) => { + tracing::info!(auth_key, "Data successfully decrypted and parsed"); + Some(result) + } + Err(e) => { + tracing::error!(auth_key, error = ?e, "Failed to parse decrypted data"); + None + } } } - pub fn get_registration_info(&self, auth_key: &str) -> Option { - self.registrations + #[tracing::instrument(skip(self))] + pub fn get_registration(&self, auth_key: &str) -> Option { + tracing::debug!(auth_key, "Retrieving registration tracing::info"); + let result = self + .registrations .get(auth_key) - .map(|reference| reference.value().clone()) + .map(|reference| reference.value().clone()); + + if result.is_some() { + tracing::info!( + auth_key, + "Registration tracing::info retrieved successfully" + ); + } else { + tracing::debug!(auth_key, "No registration tracing::info found"); + } + + result } } diff --git a/packages/hoppscotch-agent/src-tauri/src/tray.rs b/packages/hoppscotch-agent/src-tauri/src/tray.rs index 1b1f62e8..afd08441 100644 --- a/packages/hoppscotch-agent/src-tauri/src/tray.rs +++ b/packages/hoppscotch-agent/src-tauri/src/tray.rs @@ -1,11 +1,11 @@ -use crate::state::AppState; +use crate::{show_main_window, state::AppState}; use lazy_static::lazy_static; use std::sync::Arc; use tauri::{ image::Image, menu::{MenuBuilder, MenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, - AppHandle, Manager, + AppHandle, Emitter, Manager, }; const TRAY_ICON_DATA: &'static [u8] = include_bytes!("../icons/tray_icon.png"); @@ -23,6 +23,13 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { true, None::<&str>, )?; + let show_registrations = MenuItem::with_id( + app, + "show_registrations", + "Show Registrations", + true, + None::<&str>, + )?; let pkg_info = app.package_info(); let app_name = pkg_info.name.clone(); @@ -42,6 +49,8 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { .item(&app_version_item) .separator() .item(&clear_registrations) + .item(&show_registrations) + .separator() .item(&quit_i) .build()?; @@ -57,8 +66,9 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { .menu_on_left_click(true) .on_menu_event(move |app, event| match event.id.as_ref() { "quit" => { - log::info!("Exiting the agent..."); - app.exit(-1); + tracing::info!("Exiting the agent..."); + // Exit with a specific code to allow actual exit. + app.exit(1); } "clear_registrations" => { let app_state = app.state::>(); @@ -67,8 +77,16 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { .clear_registrations(app.clone()) .expect("Invariant violation: Failed to clear registrations"); } + "show_registrations" => { + app.emit("show-registrations", ()).unwrap_or_else(|e| { + tracing::error!("Failed to emit show-registrations event: {}", e); + }); + if let Err(e) = show_main_window(&app) { + tracing::error!("Failed to show window: {}", e); + } + } _ => { - log::warn!("Unhandled menu event: {:?}", event.id); + tracing::warn!("Unhandled menu event: {:?}", event.id); } }) .on_tray_icon_event(|tray, event| { @@ -79,9 +97,8 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { } = event { let app = tray.app_handle(); - if let Some(window) = app.get_webview_window("main") { - let _ = window.show(); - let _ = window.set_focus(); + if let Err(e) = show_main_window(&app) { + tracing::error!("Failed to show window from tray: {}", e); } } }) diff --git a/packages/hoppscotch-agent/src-tauri/src/util.rs b/packages/hoppscotch-agent/src-tauri/src/util.rs index e383f0b9..56b3fb02 100644 --- a/packages/hoppscotch-agent/src-tauri/src/util.rs +++ b/packages/hoppscotch-agent/src-tauri/src/util.rs @@ -7,6 +7,14 @@ use axum::{ }; use rand::rngs::OsRng; use serde::Serialize; +use sha2::{Digest, Sha256}; + +use crate::global::NONCE; + +pub fn generate_auth_key_hash(auth_key: &str) -> String { + let hash = Sha256::digest(auth_key.as_bytes()); + base16::encode_lower(&hash[..3]) +} pub fn open_link(link: &str) -> Option<()> { let null = Stdio::null(); @@ -79,7 +87,7 @@ where let response_headers = response.headers_mut(); response_headers.insert("Content-Type", "application/octet-stream".parse().unwrap()); - response_headers.insert("X-Hopp-Nonce", nonce_b16.parse().unwrap()); + response_headers.insert(NONCE, nonce_b16.parse().unwrap()); response } diff --git a/packages/hoppscotch-agent/src-tauri/src/webview/mod.rs b/packages/hoppscotch-agent/src-tauri/src/webview/mod.rs index f66279b4..5758b96c 100644 --- a/packages/hoppscotch-agent/src-tauri/src/webview/mod.rs +++ b/packages/hoppscotch-agent/src-tauri/src/webview/mod.rs @@ -182,7 +182,7 @@ pub fn init_webview() { ) .not() { - log::warn!("Declined to setup WebView."); + tracing::warn!("Declined to setup WebView."); std::process::exit(1); } @@ -196,7 +196,7 @@ pub fn init_webview() { )); if let Err(e) = open_install_website() { - log::warn!("Failed to launch WebView website:\n{}", e); + tracing::warn!("Failed to launch WebView website:\n{}", e); } std::process::exit(1); diff --git a/packages/hoppscotch-agent/src/App.vue b/packages/hoppscotch-agent/src/App.vue index f58644c2..640a291d 100644 --- a/packages/hoppscotch-agent/src/App.vue +++ b/packages/hoppscotch-agent/src/App.vue @@ -1,71 +1,179 @@ - diff --git a/packages/hoppscotch-agent/src/pages/otp.vue b/packages/hoppscotch-agent/src/pages/otp.vue new file mode 100644 index 00000000..2265faf7 --- /dev/null +++ b/packages/hoppscotch-agent/src/pages/otp.vue @@ -0,0 +1,80 @@ + + + diff --git a/packages/hoppscotch-agent/src/pages/registrations.vue b/packages/hoppscotch-agent/src/pages/registrations.vue new file mode 100644 index 00000000..b6936594 --- /dev/null +++ b/packages/hoppscotch-agent/src/pages/registrations.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/hoppscotch-backend/src/app.module.ts b/packages/hoppscotch-backend/src/app.module.ts index f86db38c..f61bd187 100644 --- a/packages/hoppscotch-backend/src/app.module.ts +++ b/packages/hoppscotch-backend/src/app.module.ts @@ -1,4 +1,4 @@ -import { ForbiddenException, HttpException, Module } from '@nestjs/common'; +import { HttpException, Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { UserModule } from './user/user.module'; @@ -8,7 +8,7 @@ import { UserSettingsModule } from './user-settings/user-settings.module'; import { UserEnvironmentsModule } from './user-environment/user-environments.module'; import { UserRequestModule } from './user-request/user-request.module'; import { UserHistoryModule } from './user-history/user-history.module'; -import { subscriptionContextCookieParser } from './auth/helper'; +import { subscriptionContextCookieParser, extractAccessTokenFromAuthRecords } from './auth/helper'; import { TeamModule } from './team/team.module'; import { TeamEnvironmentsModule } from './team-environments/team-environments.module'; import { TeamCollectionModule } from './team-collection/team-collection.module'; @@ -52,20 +52,29 @@ import { InfraTokenModule } from './infra-token/infra-token.module'; subscriptions: { 'subscriptions-transport-ws': { path: '/graphql', - onConnect: (_, websocket) => { + onConnect: (connectionParams, websocket) => { + const websocketHeaders = websocket?.upgradeReq?.headers; + try { - const cookies = subscriptionContextCookieParser( - websocket.upgradeReq.headers.cookie, - ); - return { - headers: { ...websocket?.upgradeReq?.headers, cookies }, - }; - } catch (error) { - throw new HttpException(COOKIES_NOT_FOUND, 400, { - cause: new Error(COOKIES_NOT_FOUND), - }); + const accessToken = extractAccessTokenFromAuthRecords(connectionParams); + const authorization = `Bearer ${accessToken}` + + return { headers: { ...websocketHeaders, authorization } }; + } catch (authError) { + const cookiesFromHeader = websocketHeaders?.cookie; + const cookies = cookiesFromHeader + ? subscriptionContextCookieParser(cookiesFromHeader) + : null; + + if (!cookies) { + throw new HttpException(COOKIES_NOT_FOUND, 400, { + cause: new Error(COOKIES_NOT_FOUND), + }); + } + + return { headers: { ...websocketHeaders, cookies } }; } - }, + } }, }, context: ({ req, res, connection }) => ({ diff --git a/packages/hoppscotch-backend/src/auth/auth.controller.ts b/packages/hoppscotch-backend/src/auth/auth.controller.ts index 9d652db4..dd7833ba 100644 --- a/packages/hoppscotch-backend/src/auth/auth.controller.ts +++ b/packages/hoppscotch-backend/src/auth/auth.controller.ts @@ -193,4 +193,24 @@ export class AuthController { if (E.isLeft(userInfo)) throwHTTPErr(userInfo.left); return userInfo.right; } + + @Get('desktop') + @UseGuards(JwtAuthGuard) + @UseInterceptors(UserLastLoginInterceptor) + async desktopAuthCallback( + @GqlUser() user: AuthUser, + @Query('redirect_uri') redirectUri: string, + ) { + if (!redirectUri || !redirectUri.startsWith('http://localhost')) { + throwHTTPErr({ + message: 'Invalid desktop callback URL', + statusCode: 400 + }); + } + + const tokens = await this.authService.generateAuthTokens(user.uid); + if (E.isLeft(tokens)) throwHTTPErr(tokens.left); + + return tokens.right; + } } diff --git a/packages/hoppscotch-backend/src/auth/helper.ts b/packages/hoppscotch-backend/src/auth/helper.ts index 9c5c051d..c3316910 100644 --- a/packages/hoppscotch-backend/src/auth/helper.ts +++ b/packages/hoppscotch-backend/src/auth/helper.ts @@ -3,9 +3,10 @@ import { DateTime } from 'luxon'; import { AuthTokens } from 'src/types/AuthTokens'; import { Response } from 'express'; import * as cookie from 'cookie'; -import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors'; +import { AUTH_HEADER_NOT_FOUND, AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND, INVALID_AUTH_HEADER } from 'src/errors'; import { throwErr } from 'src/utils'; import { ConfigService } from '@nestjs/config'; +import { IncomingHttpHeaders } from 'http'; enum AuthTokenType { ACCESS_TOKEN = 'access_token', @@ -117,11 +118,102 @@ export function authProviderCheck( const envVariables = VITE_ALLOWED_AUTH_PROVIDERS ? VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) => - provider.trim().toUpperCase(), - ) + provider.trim().toUpperCase(), + ) : []; if (!envVariables.includes(provider.toUpperCase())) return false; return true; } + +/** + * Extract cookie as key-value pairs from headers of a request + * @param headers HTTP request headers containing auth tokens + * @returns Cookie's key-value pairs + */ +export const extractCookieAsKeyValuesFromHeaders = (headers: IncomingHttpHeaders) => { + const cookieHeader = headers['cookie'] || headers['Cookie'] || headers['COOKIE']; + + if (!cookieHeader) { + throw new HttpException(COOKIES_NOT_FOUND, 400, { + cause: new Error(COOKIES_NOT_FOUND), + }); + } + + const cookieStr = Array.isArray(cookieHeader) ? cookieHeader[0] : cookieHeader; + + const kv = cookieStr.split(';') + .map(cookie => cookie.trim()) + .reduce((acc, curr) => { + const [key, value] = curr.split('='); + acc[key] = value; + return acc; + }, {} as Record); + + return kv; +}; + +/** + * Extract auth tokens from cookie headers of a request + * @param headers HTTP request headers containing auth tokens + * @returns AuthTokens for JWT strategy to use + */ +export const extractAuthTokensFromCookieHeaders = (headers: IncomingHttpHeaders) => { + const cookieKV = extractCookieAsKeyValuesFromHeaders(headers); + + if (!cookieKV[AuthTokenType.ACCESS_TOKEN] || !cookieKV[AuthTokenType.REFRESH_TOKEN]) { + throw new HttpException(COOKIES_NOT_FOUND, 400, { + cause: new Error(COOKIES_NOT_FOUND), + }); + } + + return { + access_token: cookieKV[AuthTokenType.ACCESS_TOKEN], + refresh_token: cookieKV[AuthTokenType.REFRESH_TOKEN], + }; +}; + +/** + * Extract access tokens from cookie headers of a request + * @param headers HTTP request headers containing access tokens + * @returns AccessTokens for JWT strategy to use + */ +export const extractAccessTokensFromCookieHeaders = (headers: IncomingHttpHeaders) => { + const cookieKV = extractCookieAsKeyValuesFromHeaders(headers); + + if (!cookieKV[AuthTokenType.ACCESS_TOKEN]) { + throw new HttpException(COOKIES_NOT_FOUND, 400, { + cause: new Error(COOKIES_NOT_FOUND), + }); + } + + return { + access_token: cookieKV[AuthTokenType.ACCESS_TOKEN], + }; +}; + +/** + * Extract access token from authorization header + * @param headers HTTP request headers containing bearer token + * @returns AccessTokens for JWT strategy + */ +export const extractAccessTokenFromAuthRecords = (headers: IncomingHttpHeaders) => { + const authHeader = headers['authorization'] || headers['Authorization']; + if (!authHeader) { + throw new HttpException(AUTH_HEADER_NOT_FOUND, 400, { + cause: new Error(AUTH_HEADER_NOT_FOUND), + }); + } + + const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader; + const [bearer, access_token] = headerValue.split(' '); + + if (bearer !== 'Bearer' || !access_token) { + throw new HttpException(INVALID_AUTH_HEADER, 400, { + cause: new Error(INVALID_AUTH_HEADER), + }); + } + + return access_token; +}; diff --git a/packages/hoppscotch-backend/src/auth/strategies/jwt.strategy.ts b/packages/hoppscotch-backend/src/auth/strategies/jwt.strategy.ts index d113a47c..2b7a7d32 100644 --- a/packages/hoppscotch-backend/src/auth/strategies/jwt.strategy.ts +++ b/packages/hoppscotch-backend/src/auth/strategies/jwt.strategy.ts @@ -1,21 +1,66 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; -import { - Injectable, - ForbiddenException, - UnauthorizedException, -} from '@nestjs/common'; +import { Injectable, ForbiddenException, UnauthorizedException } from '@nestjs/common'; import { AccessTokenPayload } from 'src/types/AuthTokens'; import { UserService } from 'src/user/user.service'; -import { AuthService } from '../auth.service'; +import { ConfigService } from '@nestjs/config'; import { Request } from 'express'; import * as O from 'fp-ts/Option'; -import { - COOKIES_NOT_FOUND, - INVALID_ACCESS_TOKEN, - USER_NOT_FOUND, -} from 'src/errors'; -import { ConfigService } from '@nestjs/config'; +import * as E from 'fp-ts/Either'; +import { pipe } from 'fp-ts/function'; +import { COOKIES_NOT_FOUND, INVALID_ACCESS_TOKEN, USER_NOT_FOUND } from 'src/errors'; + +/** + * Extracts an access token from a cookie in the request. + * + * @param request - Express Request object + * @returns Option containing the token if found + */ +const extractFromCookie = (request: Request): O.Option => + pipe( + O.fromNullable(request.cookies), + O.chain(cookies => O.fromNullable(cookies['access_token'])) + ); + +/** + * Extracts an access token from the Authorization header. + * Expects the header to be in the format: 'Bearer '. + * + * @param request - Express Request object + * @returns Option containing the token if found + */ +const extractFromAuthHeaders = (request: Request): O.Option => + pipe( + // First try headers.authorization, then fall back to root level authorization + // see `gql-auth.guard` for more info. + O.fromNullable( + request?.headers?.authorization || + (request && 'authorization' in request ? request['authorization'] : undefined) + ), + O.chain(auth => + typeof auth === 'string' && auth.startsWith('Bearer ') + ? O.some(auth.slice(7)) + : O.none + ) + ); + +/** + * Combines cookie and header token extraction strategies. + * Attempts to extract from cookie first, then falls back to Authorization header. + * + * @param request - Express Request object + * @returns Either containing the token or an error + */ +const extractToken = (request: Request): E.Either => + pipe( + extractFromCookie(request), + O.alt(() => extractFromAuthHeaders(request)), + // Neither `Authorization` header nor `Cookie` were found with the request, + // `COOKIES_NOT_FOUND` for backwards compatibility. + E.fromOption(() => { + return new ForbiddenException(COOKIES_NOT_FOUND); + }) + ); @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { @@ -25,13 +70,14 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { ) { super({ jwtFromRequest: ExtractJwt.fromExtractors([ - (request: Request) => { - const ATCookie = request.cookies['access_token']; - if (!ATCookie) { - throw new ForbiddenException(COOKIES_NOT_FOUND); - } - return ATCookie; - }, + (request: Request) => + pipe( + extractToken(request), + E.fold( + error => { throw error; }, + token => { return token } + ) + ), ]), secretOrKey: configService.get('JWT_SECRET'), }); diff --git a/packages/hoppscotch-backend/src/auth/strategies/rt-jwt.strategy.ts b/packages/hoppscotch-backend/src/auth/strategies/rt-jwt.strategy.ts index 8fdf107a..6f42321f 100644 --- a/packages/hoppscotch-backend/src/auth/strategies/rt-jwt.strategy.ts +++ b/packages/hoppscotch-backend/src/auth/strategies/rt-jwt.strategy.ts @@ -27,6 +27,7 @@ export class RTJwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { (request: Request) => { const RTCookie = request.cookies['refresh_token']; if (!RTCookie) { + console.error("`refresh_token` not found") throw new ForbiddenException(COOKIES_NOT_FOUND); } return RTCookie; diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index 5279d2bf..0a52d247 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -593,6 +593,18 @@ export const TOKEN_EXPIRED = 'auth/token_expired' as const; */ export const MAGIC_LINK_EXPIRED = 'auth/magic_link_expired' as const; +/** + * Auth header was NOT found in the auth request + * (AuthService) + */ +export const AUTH_HEADER_NOT_FOUND = 'auth/auth_header_not_found' as const; + +/** + * Auth header was found but the format was invalid + * (AuthService) + */ +export const INVALID_AUTH_HEADER = 'auth/invalid_auth_header' as const; + /** * No cookies were found in the auth request * (AuthService) diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index 50545c8d..6a18bcf1 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -9,8 +9,11 @@ "select_workspace": "Select a workspace", "clear": "Clear", "clear_all": "Clear all", + "clear_cache": "Clear Cache", "clear_history": "Clear all History", + "clear_unpinned": "Clear Unpinned", "close": "Close", + "confirm": "Confirm", "connect": "Connect", "connecting": "Connecting", "copy": "Copy", @@ -18,6 +21,7 @@ "delete": "Delete", "disconnect": "Disconnect", "dismiss": "Dismiss", + "done": "Done", "dont_save": "Don't save", "download_file": "Download file", "download_test_report": "Download test report", @@ -42,6 +46,7 @@ "properties": "Properties", "register": "Register", "remove": "Remove", + "remove_instance": "Remove instance", "rename": "Rename", "restore": "Restore", "retry": "Retry", @@ -96,7 +101,6 @@ "enter_otp_instruction": "Please enter the verification code generated by Hoppscotch Agent and complete the registration", "otp_label": "Verification Code", "processing": "Processing your request...", - "not_running": "The Hoppscotch Agent is not running. Please start the agent and click 'Retry'.", "not_running_title": "Agent not detected", "registration_title": "Agent registration", "verify_ssl_certs": "Verify SSL Certificates", @@ -255,8 +259,7 @@ "nonce_count": "Nonce Count", "client_nonce": "Client Nonce", "opaque": "Opaque", - "disable_retry": "Disable Retrying Request", - "inspector_warning": "Agent interceptor is recommended when using Digest Authorization." + "disable_retry": "Disable Retrying Request" } }, "collection": { @@ -416,6 +419,43 @@ "details": "Details" }, "error": { + "network": { + "heading": "Network Error", + "description": "Network connection failed. {message}: {cause}" + }, + "timeout": { + "heading": "Timeout Error", + "description": "Request timed out during {phase}. {message}" + }, + "certificate": { + "heading": "Certificate Error", + "description": "Invalid certificate. {message}: {cause}" + }, + "auth": { + "heading": "Authentication Error", + "description": "Access denied. {message}: {cause}" + }, + "proxy": { + "heading": "Proxy Error", + "description": "Proxy connection failed. {message}: {cause}" + }, + "parse": { + "heading": "Parse Error", + "description": "Failed to parse response. {message}: {cause}" + }, + "version": { + "heading": "Version Error", + "description": "Incompatible versions. {message}: {cause}" + }, + "abort": { + "heading": "Request Aborted", + "description": "Operation cancelled. {message}: {cause}" + }, + "unknown": { + "heading": "Unknown Error", + "description": "An unknown error occurred.", + "cause": "Unknown cause" + }, "authproviders_load_error": "Unable to load auth providers", "browser_support_sse": "This browser doesn't seems to have Server Sent Events support.", "check_console_details": "Check console log for details.", @@ -536,7 +576,8 @@ "collection": "Collapse Collection Panel", "more": "Hide more", "preview": "Hide Preview", - "sidebar": "Collapse sidebar" + "sidebar": "Collapse sidebar", + "password": "Hide Password" }, "import": { "collections": "Import collections", @@ -591,6 +632,17 @@ "import_summary_test_scripts_title": "Test scripts", "import_summary_not_supported_by_hoppscotch_import": "We do not support importing {featureLabel} from this source right now." }, + "instances": { + "switch": "Switch Hoppscotch Instance", + "enter_server_url": "Connect to a self-hosted instance", + "already_connected": "You are already connected to this instance", + "recent_connections": "Recent Connections", + "add_instance": "Add an instance", + "add_new": "Add a new instance", + "confirm_remove": "Confirm Removal", + "remove_warning": "Are you sure you want to remove this instance?", + "clear_cached_bundles": "Clear cached bundles" + }, "inspections": { "description": "Inspect possible errors", "environment": { @@ -614,10 +666,36 @@ "extension_not_installed": "Extension not installed.", "extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.", "extention_enable_action": "Enable Browser Extension", - "extention_not_enabled": "Extension not enabled." + "extention_not_enabled": "Extension not enabled.", + "localaccess_unsupported": "Current interceptor does not support local access, please consider using Agent, Extension interceptors or the Desktop App" }, - "requestBody": { - "active_interceptor_doesnt_support_binary_body": "Sending binary data via the current interceptor is not supported yet." + "auth": { + "digest": "Agent interceptor or the Hoppscotch Desktop app are recommended when using Digest Authorization." + }, + "body": { + "binary": "Sending binary data via the current interceptor is not supported yet." + } + }, + "interceptor": { + "native": { + "name": "Native", + "settings_title": "Native" + }, + "agent": { + "name": "Agent", + "settings_title": "Agent" + }, + "proxy": { + "name": "Proxy", + "settings_title": "Proxy" + }, + "browser": { + "name": "Browser", + "settings_title": "Browser" + }, + "extension": { + "name": "Extension", + "settings_title": "Extension" } }, "layout": { @@ -797,6 +875,13 @@ "account_email_description": "Your primary email address.", "account_name_description": "This is your display name.", "additional": "Additional Settings", + "agent_not_running": "Hoppscotch Agent not detected - click `Retry` to check again.", + "agent_not_running_short": "Check Agent's status.", + "agent_running": "Hoppscotch Agent is live.", + "agent_running_short": "Hoppscotch Agent is live.", + "agent_reset_registration": "Reset Registration", + "agent_registered": "Agent Registered", + "agent_registration_successful": "Agent Registered Successfully", "auto_encode_mode": "Auto", "auto_encode_mode_tooltip": "Encode the parameters in the request only if some special characters are present", "background": "Background", @@ -807,6 +892,7 @@ "delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.", "disable_encode_mode_tooltip": "Never encode the parameters in the request", "enable_encode_mode_tooltip": "Always encode the parameters in the request", + "enter_otp": "Enter Agent's code", "expand_navigation": "Expand navigation", "experiments": "Experiments", "experiments_notice": "This is a collection of experiments we're working on that might turn out to be useful, fun, both, or neither. They're not final and may not be stable, so if something overly weird happens, don't panic. Just turn the dang thing off. Jokes aside, ", @@ -819,6 +905,8 @@ "general_description": " General settings used in the application", "interceptor": "Interceptor", "interceptor_description": "Middleware between application and APIs.", + "kernel_interceptor": "Interceptor", + "kernel_interceptor_description": "Middleware between application and APIs.", "language": "Language", "light_mode": "Light", "official_proxy_hosting": "Official Proxy is hosted by Hoppscotch.", @@ -858,7 +946,33 @@ "use_experimental_url_bar": "Use experimental URL bar with environment highlighting", "user": "User", "verified_email": "Verified email", - "verify_email": "Verify email" + "verify_email": "Verify email", + "validate_certificates": "Validate SSL/TLS Certificates", + "verify_host": "Verify Host", + "verify_peer": "Verify Peer", + "client_certificates": "Client Certificates", + "certificate_settings": "Certificate Settings", + "certificate": "Certificate", + "key": "Private Key", + "pfx_or_p12": "PFX/PKCS#12", + "password": "Password", + "select_file": "Select File", + "domain": "Domain", + "add_certificate": "Add Certificate", + "add_cert_file": "Add Certificate File", + "add_key_file": "Add Key File", + "add_pfx_file": "Add PFX File", + "global_defaults": "Global Defaults", + "add_domain_override": "Add Domain Override", + "domain_override": "Domain Override", + "manage_domains_overrides": "Manage Domains Overrides", + "add_domain": "Add Domain", + "remove_domain": "Remove Domain", + "ca_certificate": "CA Certificate", + "ca_certificates": "CA Certificates", + "ca_certificates_support": "Hoppscotch supports .crt, .cer or .pem files containing one or more certificates.", + "proxy_capabilities": "Hoppscotch Agent and Desktop App supports HTTP/HTTPS/SOCKS proxies with NTLM and Basic Auth support.", + "proxy_auth": "You can also include username and password in the URL." }, "shared_requests": { "button": "Button", @@ -967,7 +1081,8 @@ "code": "Show code", "collection": "Expand Collection Panel", "more": "Show more", - "sidebar": "Expand sidebar" + "sidebar": "Expand sidebar", + "password": "Show Password" }, "socketio": { "communication": "Communication", diff --git a/packages/hoppscotch-common/package.json b/packages/hoppscotch-common/package.json index 50709480..b9e7fe50 100644 --- a/packages/hoppscotch-common/package.json +++ b/packages/hoppscotch-common/package.json @@ -34,16 +34,19 @@ "@codemirror/search": "6.5.6", "@codemirror/state": "6.4.1", "@codemirror/view": "6.25.1", + "@hoppscotch/plugin-appload": "github:CuriousCorrelation/tauri-plugin-appload", "@hoppscotch/codemirror-lang-graphql": "workspace:^", "@hoppscotch/data": "workspace:^", "@hoppscotch/httpsnippet": "3.0.7", "@hoppscotch/js-sandbox": "workspace:^", + "@hoppscotch/kernel": "workspace:^", "@hoppscotch/ui": "0.2.2", "@hoppscotch/vue-toasted": "0.1.0", "@lezer/highlight": "1.2.0", "@noble/curves": "1.6.0", "@scure/base": "1.1.9", "@shopify/lang-jsonc": "1.0.0", + "@tauri-apps/plugin-store": "2.2.0", "@types/markdown-it": "14.1.2", "@unhead/vue": "1.11.10", "@urql/core": "5.0.6", @@ -90,6 +93,7 @@ "splitpanes": "3.1.5", "stream-browserify": "3.0.0", "subscriptions-transport-ws": "0.11.0", + "superjson": "2.2.2", "tern": "0.24.3", "timers": "0.1.1", "tippy.js": "6.3.7", diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts index 64eb0910..68a94d16 100644 --- a/packages/hoppscotch-common/src/components.d.ts +++ b/packages/hoppscotch-common/src/components.d.ts @@ -24,6 +24,7 @@ declare module 'vue' { AppHeader: typeof import('./components/app/Header.vue')['default'] AppInspection: typeof import('./components/app/Inspection.vue')['default'] AppInterceptor: typeof import('./components/app/Interceptor.vue')['default'] + AppKernelInterceptor: typeof import('./components/app/KernelInterceptor.vue')['default'] AppLogo: typeof import('./components/app/Logo.vue')['default'] AppMarkdown: typeof import('./components/app/Markdown.vue')['default'] AppOptions: typeof import('./components/app/Options.vue')['default'] @@ -230,6 +231,7 @@ declare module 'vue' { ImportExportImportExportStepsImportSummary: typeof import('./components/importExport/ImportExportSteps/ImportSummary.vue')['default'] ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default'] ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default'] + InstanceSwitcher: typeof import('./components/instance/Switcher.vue')['default'] InterceptorsAgentModalNativeCACertificates: typeof import('./components/interceptors/agent/ModalNativeCACertificates.vue')['default'] InterceptorsAgentModalNativeClientCertificates: typeof import('./components/interceptors/agent/ModalNativeClientCertificates.vue')['default'] InterceptorsAgentModalNativeClientCertsAdd: typeof import('./components/interceptors/agent/ModalNativeClientCertsAdd.vue')['default'] @@ -255,7 +257,10 @@ declare module 'vue' { RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default'] RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default'] SettingsAgent: typeof import('./components/settings/Agent.vue')['default'] + SettingsAgentSubtitle: typeof import('./components/settings/AgentSubtitle.vue')['default'] SettingsExtension: typeof import('./components/settings/Extension.vue')['default'] + SettingsExtensionSubtitle: typeof import('./components/settings/ExtensionSubtitle.vue')['default'] + SettingsNative: typeof import('./components/settings/Native.vue')['default'] SettingsProxy: typeof import('./components/settings/Proxy.vue')['default'] Share: typeof import('./components/share/index.vue')['default'] ShareCreateModal: typeof import('./components/share/CreateModal.vue')['default'] diff --git a/packages/hoppscotch-common/src/components/app/ActionHandler.vue b/packages/hoppscotch-common/src/components/app/ActionHandler.vue index 9253b0ed..2bcac28f 100644 --- a/packages/hoppscotch-common/src/components/app/ActionHandler.vue +++ b/packages/hoppscotch-common/src/components/app/ActionHandler.vue @@ -2,6 +2,10 @@ + { @@ -30,6 +35,10 @@ defineActionHandler("modals.login.toggle", () => { showLogin.value = !showLogin.value }) +defineActionHandler("modals.instance-switcher.toggle", () => { + showInstanceSwitcher.value = !showInstanceSwitcher.value +}) + defineActionHandler("response.schema.toggle", () => { isDrawerOpen.value = !isDrawerOpen.value }) diff --git a/packages/hoppscotch-common/src/components/app/Footer.vue b/packages/hoppscotch-common/src/components/app/Footer.vue index 9cea74eb..6676e5d8 100644 --- a/packages/hoppscotch-common/src/components/app/Footer.vue +++ b/packages/hoppscotch-common/src/components/app/Footer.vue @@ -17,7 +17,7 @@ :icon="IconShieldCheck" />
+ +
+ + {{ instanceDisplayName }} + + +
+ +
-
+
-
+
{{ currentUser.email }} - {{ currentUser.email }} -

diff --git a/packages/hoppscotch-common/src/components/app/PaneLayout.vue b/packages/hoppscotch-common/src/components/app/PaneLayout.vue index 6ca2c7a5..8e4a1cfd 100644 --- a/packages/hoppscotch-common/src/components/app/PaneLayout.vue +++ b/packages/hoppscotch-common/src/components/app/PaneLayout.vue @@ -54,7 +54,7 @@ import "splitpanes/dist/splitpanes.css" import { useSetting } from "@composables/settings" import { breakpointsTailwind, useBreakpoints } from "@vueuse/core" import { useService } from "dioc/vue" -import { computed, ref, useSlots } from "vue" +import { computed, onMounted, ref, useSlots } from "vue" import { PersistenceService } from "~/services/persistence" const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT") @@ -104,23 +104,26 @@ if (!COLUMN_LAYOUT.value) { PANE_MAIN_BOTTOM_SIZE.value = 50 } -function setPaneEvent(event: PaneEvent[], type: "vertical" | "horizontal") { +async function setPaneEvent( + event: PaneEvent[], + type: "vertical" | "horizontal" +) { if (!props.layoutId) return const storageKey = `${props.layoutId}-pane-config-${type}` - persistenceService.setLocalConfig(storageKey, JSON.stringify(event)) + await persistenceService.setLocalConfig(storageKey, JSON.stringify(event)) } -function populatePaneEvent() { +async function populatePaneEvent() { if (!props.layoutId) return - const verticalPaneData = getPaneData("vertical") + const verticalPaneData = await getPaneData("vertical") if (verticalPaneData) { const [mainPane, sidebarPane] = verticalPaneData PANE_MAIN_SIZE.value = mainPane?.size PANE_SIDEBAR_SIZE.value = sidebarPane?.size } - const horizontalPaneData = getPaneData("horizontal") + const horizontalPaneData = await getPaneData("horizontal") if (horizontalPaneData) { const [mainTopPane, mainBottomPane] = horizontalPaneData PANE_MAIN_TOP_SIZE.value = mainTopPane?.size @@ -128,12 +131,16 @@ function populatePaneEvent() { } } -function getPaneData(type: "vertical" | "horizontal"): PaneEvent[] | null { +async function getPaneData( + type: "vertical" | "horizontal" +): Promise { const storageKey = `${props.layoutId}-pane-config-${type}` - const paneEvent = persistenceService.getLocalConfig(storageKey) + const paneEvent = await persistenceService.getLocalConfig(storageKey) if (!paneEvent) return null return JSON.parse(paneEvent) } -populatePaneEvent() +onMounted(async () => { + await populatePaneEvent() +}) diff --git a/packages/hoppscotch-common/src/components/app/WhatsNewDialog.vue b/packages/hoppscotch-common/src/components/app/WhatsNewDialog.vue index 9df856ea..44fb5dce 100644 --- a/packages/hoppscotch-common/src/components/app/WhatsNewDialog.vue +++ b/packages/hoppscotch-common/src/components/app/WhatsNewDialog.vue @@ -48,7 +48,7 @@ defineEmits<{ }>() const openWhatsNew = () => { - if (props.notesUrl) platform.io.openExternalLink(props.notesUrl) + if (props.notesUrl) platform.kernelIO.openExternalLink(props.notesUrl) } diff --git a/packages/hoppscotch-common/src/components/app/spotlight/index.vue b/packages/hoppscotch-common/src/components/app/spotlight/index.vue index 7da96ea7..e731b29c 100644 --- a/packages/hoppscotch-common/src/components/app/spotlight/index.vue +++ b/packages/hoppscotch-common/src/components/app/spotlight/index.vue @@ -104,7 +104,9 @@ import { } from "~/services/spotlight/searchers/environment.searcher" import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher" import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher" -import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher" +// NOTE: Old interceptors +// import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher" +import { KernelInterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/kernel-interceptor.searcher" import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher" import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher" import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher" @@ -144,7 +146,9 @@ useService(EnvironmentsSpotlightSearcherService) useService(SwitchEnvSpotlightSearcherService) useService(WorkspaceSpotlightSearcherService) useService(SwitchWorkspaceSpotlightSearcherService) -useService(InterceptorSpotlightSearcherService) +// NOTE: Old interceptors +// useService(InterceptorSpotlightSearcherService) +useService(KernelInterceptorSpotlightSearcherService) useService(TeamsSpotlightSearcherService) platform.spotlight?.additionalSearchers?.forEach((searcher) => diff --git a/packages/hoppscotch-common/src/components/collections/ImportExport.vue b/packages/hoppscotch-common/src/components/collections/ImportExport.vue index 279d1889..ced13a9b 100644 --- a/packages/hoppscotch-common/src/components/collections/ImportExport.vue +++ b/packages/hoppscotch-common/src/components/collections/ImportExport.vue @@ -625,7 +625,7 @@ const HoppGistCollectionsExporter: ImporterOrExporter = { platform: "rest", }) - platform.io.openExternalLink(res.right) + platform.kernelIO.openExternalLink(res.right) } else { toast.error(collectionJSON.left) } diff --git a/packages/hoppscotch-common/src/components/collections/Properties.vue b/packages/hoppscotch-common/src/components/collections/Properties.vue index c79a4140..ad2def9f 100644 --- a/packages/hoppscotch-common/src/components/collections/Properties.vue +++ b/packages/hoppscotch-common/src/components/collections/Properties.vue @@ -199,7 +199,7 @@ const activeTabIsDetails = computed(() => activeTab.value === "details") watch( editableCollection, - (updatedEditableCollection) => { + async (updatedEditableCollection) => { if (props.show && props.editingProperties) { const unsavedCollectionProperties: EditingProperties = { collection: updatedEditableCollection, @@ -207,7 +207,7 @@ watch( path: props.editingProperties.path, inheritedProperties: props.editingProperties.inheritedProperties, } - persistenceService.setLocalConfig( + await persistenceService.setLocalConfig( "unsaved_collection_properties", JSON.stringify(unsavedCollectionProperties) ) @@ -222,7 +222,7 @@ const activeTab = useVModel(props, "modelValue", emit) watch( () => props.show, - (show) => { + async (show) => { // `Details` tab doesn't exist for personal workspace, hence switching to the `Headers` tab // The modal can appear empty while switching from a team workspace with `Details` as the active tab if (activeTab.value === "details" && !props.showDetails) { @@ -245,12 +245,14 @@ watch( }, } - persistenceService.removeLocalConfig("unsaved_collection_properties") + await persistenceService.removeLocalConfig( + "unsaved_collection_properties" + ) } } ) -const saveEditedCollection = () => { +const saveEditedCollection = async () => { if (!props.editingProperties) return const finalCollection = clone(editableCollection.value) const collection = { @@ -262,11 +264,11 @@ const saveEditedCollection = () => { isRootCollection: props.editingProperties.isRootCollection, } emit("set-collection-properties", collection as EditingProperties) - persistenceService.removeLocalConfig("unsaved_collection_properties") + await persistenceService.removeLocalConfig("unsaved_collection_properties") } -const hideModal = () => { - persistenceService.removeLocalConfig("unsaved_collection_properties") +const hideModal = async () => { + await persistenceService.removeLocalConfig("unsaved_collection_properties") emit("hide-modal") } diff --git a/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue b/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue index b0759eab..2934197d 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue @@ -208,7 +208,7 @@ const GqlCollectionsGistExporter: ImporterOrExporter = { exporter: "gist", }) - platform.io.openExternalLink(res.right) + platform.kernelIO.openExternalLink(res.right) } isGqlCollectionGistExportInProgress.value = false diff --git a/packages/hoppscotch-common/src/components/collections/graphql/index.vue b/packages/hoppscotch-common/src/components/collections/graphql/index.vue index 406cade5..bb5917df 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/index.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/index.vue @@ -243,9 +243,9 @@ const persistenceService = useService(PersistenceService) const collectionPropertiesModalActiveTab = ref("headers") -onMounted(() => { +onMounted(async () => { const localOAuthTempConfig = - persistenceService.getLocalConfig("oauth_temp_config") + await persistenceService.getLocalConfig("oauth_temp_config") if (!localOAuthTempConfig) { return @@ -260,9 +260,8 @@ onMounted(() => { if (context?.type === "collection-properties") { // load the unsaved editing properties - const unsavedCollectionPropertiesString = persistenceService.getLocalConfig( - "unsaved_collection_properties" - ) + const unsavedCollectionPropertiesString = + await persistenceService.getLocalConfig("unsaved_collection_properties") if (unsavedCollectionPropertiesString) { const unsavedCollectionProperties: EditingProperties = JSON.parse( @@ -284,7 +283,7 @@ onMounted(() => { editingProperties.value = unsavedCollectionProperties } - persistenceService.removeLocalConfig("oauth_temp_config") + await persistenceService.removeLocalConfig("oauth_temp_config") collectionPropertiesModalActiveTab.value = "authorization" showModalEditProperties.value = true } diff --git a/packages/hoppscotch-common/src/components/collections/index.vue b/packages/hoppscotch-common/src/components/collections/index.vue index f0656c38..89d6da94 100644 --- a/packages/hoppscotch-common/src/components/collections/index.vue +++ b/packages/hoppscotch-common/src/components/collections/index.vue @@ -440,9 +440,9 @@ const persistenceService = useService(PersistenceService) const collectionPropertiesModalActiveTab = ref("headers") -onMounted(() => { +onMounted(async () => { const localOAuthTempConfig = - persistenceService.getLocalConfig("oauth_temp_config") + await persistenceService.getLocalConfig("oauth_temp_config") if (!localOAuthTempConfig) { return @@ -457,9 +457,8 @@ onMounted(() => { if (context?.type === "collection-properties") { // load the unsaved editing properties - const unsavedCollectionPropertiesString = persistenceService.getLocalConfig( - "unsaved_collection_properties" - ) + const unsavedCollectionPropertiesString = + await persistenceService.getLocalConfig("unsaved_collection_properties") if (unsavedCollectionPropertiesString) { const unsavedCollectionProperties: EditingProperties = JSON.parse( @@ -481,7 +480,7 @@ onMounted(() => { editingProperties.value = unsavedCollectionProperties } - persistenceService.removeLocalConfig("oauth_temp_config") + await persistenceService.removeLocalConfig("oauth_temp_config") collectionPropertiesModalActiveTab.value = "authorization" showModalEditProperties.value = true } @@ -2642,7 +2641,7 @@ const initializeDownloadCollection = async ( collectionJSON: string, name: string | null ) => { - const result = await platform.io.saveFileWithDialog({ + const result = await platform.kernelIO.saveFileWithDialog({ data: collectionJSON, contentType: "application/json", suggestedFilename: `${name ?? "collection"}.json`, diff --git a/packages/hoppscotch-common/src/components/cookies/AllModal.vue b/packages/hoppscotch-common/src/components/cookies/AllModal.vue index 56539493..6d6618f4 100644 --- a/packages/hoppscotch-common/src/components/cookies/AllModal.vue +++ b/packages/hoppscotch-common/src/components/cookies/AllModal.vue @@ -149,10 +149,10 @@ import IconTrash2 from "~icons/lucide/trash-2" import IconPlus from "~icons/lucide/plus" import { cloneDeep } from "lodash-es" import { ref, watch, computed } from "vue" -import { InterceptorService } from "~/services/interceptor.service" import { EditCookieConfig } from "./EditCookie.vue" import { useColorMode } from "@composables/theming" import { useToast } from "@composables/toast" +import { KernelInterceptorService } from "~/services/kernel-interceptor.service" const props = defineProps<{ show: boolean @@ -168,17 +168,16 @@ const toast = useToast() const newDomainText = ref("") -const interceptorService = useService(InterceptorService) +const interceptorService = useService(KernelInterceptorService) const cookieJarService = useService(CookieJarService) const workingCookieJar = ref(cloneDeep(cookieJarService.cookieJar.value)) const currentInterceptorSupportsCookies = computed(() => { - const currentInterceptor = interceptorService.currentInterceptor.value + const capabilities = interceptorService.current.value?.capabilities + const supportsCookies = capabilities["advanced"].has("cookies") - if (!currentInterceptor) return true - - return currentInterceptor.supportsCookies ?? false + return supportsCookies ?? false }) function addNewDomain() { diff --git a/packages/hoppscotch-common/src/components/environments/ImportExport.vue b/packages/hoppscotch-common/src/components/environments/ImportExport.vue index 337c2aac..12210863 100644 --- a/packages/hoppscotch-common/src/components/environments/ImportExport.vue +++ b/packages/hoppscotch-common/src/components/environments/ImportExport.vue @@ -328,7 +328,7 @@ const HoppEnvironmentsGistExporter: ImporterOrExporter = { platform: "rest", }) - platform.io.openExternalLink(res.right) + platform.kernelIO.openExternalLink(res.right) } isEnvironmentGistExportInProgress.value = false diff --git a/packages/hoppscotch-common/src/components/firebase/Login.vue b/packages/hoppscotch-common/src/components/firebase/Login.vue index a0bef338..c3a4bd12 100644 --- a/packages/hoppscotch-common/src/components/firebase/Login.vue +++ b/packages/hoppscotch-common/src/components/firebase/Login.vue @@ -316,9 +316,9 @@ const signInWithEmail = async () => { await platform.auth .signInWithEmail(form.email) - .then(() => { + .then(async () => { mode.value = "email-sent" - persistenceService.setLocalConfig("emailForSignIn", form.email) + await persistenceService.setLocalConfig("emailForSignIn", form.email) }) .catch((e) => { console.error(e) diff --git a/packages/hoppscotch-common/src/components/graphql/Request.vue b/packages/hoppscotch-common/src/components/graphql/Request.vue index e9632b74..ce06d5cf 100644 --- a/packages/hoppscotch-common/src/components/graphql/Request.vue +++ b/packages/hoppscotch-common/src/components/graphql/Request.vue @@ -68,7 +68,7 @@ import { computed, ref, watch } from "vue" import { connection } from "~/helpers/graphql/connection" import { connect } from "~/helpers/graphql/connection" import { disconnect } from "~/helpers/graphql/connection" -import { InterceptorService } from "~/services/interceptor.service" +import { KernelInterceptorService } from "~/services/kernel-interceptor.service" import { useService } from "dioc/vue" import { defineActionHandler } from "~/helpers/actions" import { GQLTabService } from "~/services/tab/graphql" @@ -77,7 +77,7 @@ import { HoppGQLAuth, HoppGQLRequest } from "@hoppscotch/data" const t = useI18n() const tabs = useService(GQLTabService) -const interceptorService = useService(InterceptorService) +const interceptorService = useService(KernelInterceptorService) const connectionSwitchModal = ref(false) @@ -120,7 +120,7 @@ const gqlConnect = () => { platform.analytics?.logEvent({ type: "HOPP_REQUEST_RUN", platform: "graphql-schema", - strategy: interceptorService.currentInterceptorID.value!, + strategy: interceptorService.current.value!.id, }) } diff --git a/packages/hoppscotch-common/src/components/graphql/RequestOptions.vue b/packages/hoppscotch-common/src/components/graphql/RequestOptions.vue index 1f0ab9c4..111a2f2a 100644 --- a/packages/hoppscotch-common/src/components/graphql/RequestOptions.vue +++ b/packages/hoppscotch-common/src/components/graphql/RequestOptions.vue @@ -74,7 +74,7 @@ import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties" import { completePageProgress, startPageProgress } from "~/modules/loadingbar" import { editGraphqlRequest } from "~/newstore/collections" import { platform } from "~/platform" -import { InterceptorService } from "~/services/interceptor.service" +import { KernelInterceptorService } from "~/services/kernel-interceptor.service" import { GQLTabService } from "~/services/tab/graphql" const VALID_GQL_OPERATIONS = [ @@ -86,7 +86,7 @@ const VALID_GQL_OPERATIONS = [ export type GQLOptionTabs = (typeof VALID_GQL_OPERATIONS)[number] -const interceptorService = useService(InterceptorService) +const interceptorService = useService(KernelInterceptorService) const t = useI18n() const toast = useToast() @@ -174,7 +174,7 @@ const runQuery = async ( platform.analytics?.logEvent({ type: "HOPP_REQUEST_RUN", platform: "graphql-query", - strategy: interceptorService.currentInterceptorID.value!, + strategy: interceptorService.current.value!.id, }) } diff --git a/packages/hoppscotch-common/src/components/graphql/Sidebar.vue b/packages/hoppscotch-common/src/components/graphql/Sidebar.vue index e31e0632..a1f13610 100644 --- a/packages/hoppscotch-common/src/components/graphql/Sidebar.vue +++ b/packages/hoppscotch-common/src/components/graphql/Sidebar.vue @@ -147,7 +147,7 @@ const downloadSchema = async () => { URL.revokeObjectURL(url) - const result = await platform.io.saveFileWithDialog({ + const result = await platform.kernelIO.saveFileWithDialog({ data: dataToWrite, contentType: "application/graphql", suggestedFilename: filename, diff --git a/packages/hoppscotch-common/src/components/http/Request.vue b/packages/hoppscotch-common/src/components/http/Request.vue index e84360fe..2ee8951f 100644 --- a/packages/hoppscotch-common/src/components/http/Request.vue +++ b/packages/hoppscotch-common/src/components/http/Request.vue @@ -261,15 +261,15 @@ import { platform } from "~/platform" import { HoppRESTRequest } from "@hoppscotch/data" import { useService } from "dioc/vue" import { InspectionService } from "~/services/inspection" -import { InterceptorService } from "~/services/interceptor.service" import { HoppTab } from "~/services/tab" import { HoppRequestDocument } from "~/helpers/rest/document" import { RESTTabService } from "~/services/tab/rest" import { getMethodLabelColor } from "~/helpers/rest/labelColoring" import { WorkspaceService } from "~/services/workspace.service" +import { KernelInterceptorService } from "~/services/kernel-interceptor.service" const t = useI18n() -const interceptorService = useService(InterceptorService) +const interceptorService = useService(KernelInterceptorService) const methods = [ "GET", @@ -348,7 +348,7 @@ const newSendRequest = async () => { platform.analytics?.logEvent({ type: "HOPP_REQUEST_RUN", platform: "rest", - strategy: interceptorService.currentInterceptorID.value!, + strategy: interceptorService.current.value!.id, workspaceType: workspaceService.currentWorkspace.value.type, }) diff --git a/packages/hoppscotch-common/src/components/http/ResponseMeta.vue b/packages/hoppscotch-common/src/components/http/ResponseMeta.vue index dcfbe094..8bb9b848 100644 --- a/packages/hoppscotch-common/src/components/http/ResponseMeta.vue +++ b/packages/hoppscotch-common/src/components/http/ResponseMeta.vue @@ -29,6 +29,26 @@ v-if="response.type === 'extension_error'" class="flex-1" /> + + + { : { type: "request-tab", metadata: {} }, grant_type: auth.value.grantTypeInfo.grantType, } - persistenceService.setLocalConfig( + await persistenceService.setLocalConfig( "oauth_temp_config", JSON.stringify(authConfig) ) diff --git a/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/UrlImport.vue b/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/UrlImport.vue index e91211de..cc9a5a33 100644 --- a/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/UrlImport.vue +++ b/packages/hoppscotch-common/src/components/importExport/ImportExportSteps/UrlImport.vue @@ -45,11 +45,13 @@ import { computed, ref, watch } from "vue" import { useI18n } from "@composables/i18n" import { useToast } from "~/composables/toast" -import { InterceptorService } from "~/services/interceptor.service" +import { KernelInterceptorService } from "~/services/kernel-interceptor.service" import { useService } from "dioc/vue" import * as E from "fp-ts/Either" +import * as O from "fp-ts/Option" +import { parseBodyAsJSON } from "~/helpers/functional/json" -const interceptorService = useService(InterceptorService) +const interceptorService = useService(KernelInterceptorService) const t = useI18n() @@ -83,33 +85,26 @@ const disableImportCTA = computed(() => !hasURL.value || props.loading) const urlFetchLogic = props.fetchLogic ?? async function (url: string) { - const res = await interceptorService.runRequest({ + const { response } = interceptorService.execute({ + id: Date.now(), url: url, - transitional: { - forcedJSONParsing: false, - silentJSONParsing: false, - clarifyTimeoutError: true, - }, + method: "GET", + version: "HTTP/1.1", }) - const response = await res.response + const res = await response - if (E.isLeft(response)) { + if (E.isLeft(res)) { return E.left("REQUEST_FAILED") } - // convert ArrayBuffer to string - if (!(response.right.data instanceof ArrayBuffer)) { - return E.left("REQUEST_FAILED") + const responsePayload = parseBodyAsJSON(res.right.body) + + if (O.isSome(responsePayload)) { + return E.right(responsePayload) } - try { - return E.right( - InterceptorService.convertArrayBufferToString(response.right.data) - ) - } catch (e) { - return E.left("REQUEST_FAILED") - } + return E.left("REQUEST_FAILED") } async function fetchUrlData() { diff --git a/packages/hoppscotch-common/src/components/instance/Switcher.vue b/packages/hoppscotch-common/src/components/instance/Switcher.vue new file mode 100644 index 00000000..9fe359f7 --- /dev/null +++ b/packages/hoppscotch-common/src/components/instance/Switcher.vue @@ -0,0 +1,380 @@ + + + diff --git a/packages/hoppscotch-common/src/components/lenses/renderers/HTMLLensRenderer.vue b/packages/hoppscotch-common/src/components/lenses/renderers/HTMLLensRenderer.vue index 2d54445c..9abc0bbd 100644 --- a/packages/hoppscotch-common/src/components/lenses/renderers/HTMLLensRenderer.vue +++ b/packages/hoppscotch-common/src/components/lenses/renderers/HTMLLensRenderer.vue @@ -137,15 +137,15 @@ const { downloadIcon, downloadResponse } = useDownloadResponse( }) ) const defaultPreview = - persistenceService.getLocalConfig("lens_html_preview") === "true" + (await persistenceService.getLocalConfig("lens_html_preview")) === "true" const { previewFrame, previewEnabled, togglePreview } = usePreview( defaultPreview, responseBodyText ) -const doTogglePreview = () => { - persistenceService.setLocalConfig( +const doTogglePreview = async () => { + await persistenceService.setLocalConfig( "lens_html_preview", previewEnabled.value ? "false" : "true" ) diff --git a/packages/hoppscotch-common/src/components/settings/Agent.vue b/packages/hoppscotch-common/src/components/settings/Agent.vue index c4d8bbee..886404de 100644 --- a/packages/hoppscotch-common/src/components/settings/Agent.vue +++ b/packages/hoppscotch-common/src/components/settings/Agent.vue @@ -1,106 +1,661 @@ diff --git a/packages/hoppscotch-common/src/components/settings/AgentSubtitle.vue b/packages/hoppscotch-common/src/components/settings/AgentSubtitle.vue new file mode 100644 index 00000000..5b52abb1 --- /dev/null +++ b/packages/hoppscotch-common/src/components/settings/AgentSubtitle.vue @@ -0,0 +1,179 @@ + + + diff --git a/packages/hoppscotch-common/src/components/settings/Extension.vue b/packages/hoppscotch-common/src/components/settings/Extension.vue index b136854e..742eea26 100644 --- a/packages/hoppscotch-common/src/components/settings/Extension.vue +++ b/packages/hoppscotch-common/src/components/settings/Extension.vue @@ -12,29 +12,25 @@ {{ t("settings.extension_ver_not_reported") }}
-
- - - - - - +
+ +
diff --git a/packages/hoppscotch-common/src/components/settings/ExtensionSubtitle.vue b/packages/hoppscotch-common/src/components/settings/ExtensionSubtitle.vue new file mode 100644 index 00000000..ac211eb8 --- /dev/null +++ b/packages/hoppscotch-common/src/components/settings/ExtensionSubtitle.vue @@ -0,0 +1,64 @@ + + + diff --git a/packages/hoppscotch-common/src/components/settings/Native.vue b/packages/hoppscotch-common/src/components/settings/Native.vue new file mode 100644 index 00000000..2c266d4f --- /dev/null +++ b/packages/hoppscotch-common/src/components/settings/Native.vue @@ -0,0 +1,642 @@ + + + diff --git a/packages/hoppscotch-common/src/components/settings/Proxy.vue b/packages/hoppscotch-common/src/components/settings/Proxy.vue index a0de5a4c..6c136ea5 100644 --- a/packages/hoppscotch-common/src/components/settings/Proxy.vue +++ b/packages/hoppscotch-common/src/components/settings/Proxy.vue @@ -10,13 +10,14 @@
diff --git a/packages/hoppscotch-common/src/composables/lens-actions.ts b/packages/hoppscotch-common/src/composables/lens-actions.ts index 5d56dcac..6af3d9e0 100644 --- a/packages/hoppscotch-common/src/composables/lens-actions.ts +++ b/packages/hoppscotch-common/src/composables/lens-actions.ts @@ -68,7 +68,7 @@ export function useDownloadResponse( const dataToWrite = responseBody.value // TODO: Look at the mime type and determine extension ? - const result = await platform.io.saveFileWithDialog({ + const result = await platform.kernelIO.saveFileWithDialog({ data: dataToWrite, contentType: contentType, suggestedFilename: filename, diff --git a/packages/hoppscotch-common/src/composables/picker.ts b/packages/hoppscotch-common/src/composables/picker.ts new file mode 100644 index 00000000..690fb7da --- /dev/null +++ b/packages/hoppscotch-common/src/composables/picker.ts @@ -0,0 +1,138 @@ +import { ref, reactive, computed } from "vue" +import { useFileDialog } from "@vueuse/core" + +export interface CertFiles { + pem_cert: File | null + pem_key: File | null + pfx: File | null + ca: File[] +} + +export interface CertPickerOptions { + onPEMCertChange?: (file: File | null) => void + onPEMKeyChange?: (file: File | null) => void + onPFXChange?: (file: File | null) => void + onCACertAdd?: (file: File) => void + onCACertRemove?: (index: number) => void +} + +export function useCertificatePicker(options: CertPickerOptions = {}) { + const certFiles = reactive({ + pem_cert: null, + pem_key: null, + pfx: null, + ca: [], + }) + + const certType = ref<"pem" | "pfx">("pem") + const pfxPassword = ref("") + + const pemCertPicker = useFileDialog({ + accept: ".pem,.crt", + reset: true, + multiple: false, + }) + + const pemKeyPicker = useFileDialog({ + accept: ".pem,.key", + reset: true, + multiple: false, + }) + + const pfxPicker = useFileDialog({ + accept: ".pfx,.p12", + reset: true, + multiple: false, + }) + + const caCertPicker = useFileDialog({ + accept: ".pem,.crt", + reset: true, + multiple: false, + }) + + function pickPEMCertificate() { + pemCertPicker.onChange((files) => { + const selectedFile = files?.item(0) + if (selectedFile) { + certFiles.pem_cert = selectedFile + certFiles.pfx = null + options.onPEMCertChange?.(selectedFile) + } + pemCertPicker.reset() + }) + pemCertPicker.open() + } + + function pickPEMKey() { + pemKeyPicker.onChange((files) => { + const selectedFile = files?.item(0) + if (selectedFile) { + certFiles.pem_key = selectedFile + certFiles.pfx = null + options.onPEMKeyChange?.(selectedFile) + } + pemKeyPicker.reset() + }) + pemKeyPicker.open() + } + + function pickPFXCertificate() { + pfxPicker.onChange((files) => { + const selectedFile = files?.item(0) + if (selectedFile) { + certFiles.pfx = selectedFile + certFiles.pem_cert = null + certFiles.pem_key = null + options.onPFXChange?.(selectedFile) + } + pfxPicker.reset() + }) + pfxPicker.open() + } + + function pickCACertificate() { + caCertPicker.onChange((files) => { + const selectedFile = files?.item(0) + if (selectedFile) { + certFiles.ca.push(selectedFile) + options.onCACertAdd?.(selectedFile) + } + caCertPicker.reset() + }) + caCertPicker.open() + } + + function removeCACertificate(index: number) { + certFiles.ca.splice(index, 1) + options.onCACertRemove?.(index) + } + + function reset() { + certFiles.pem_cert = null + certFiles.pem_key = null + certFiles.pfx = null + certFiles.ca = [] + pfxPassword.value = "" + certType.value = "pem" + } + + const isValidCertConfig = computed(() => + certType.value === "pem" + ? !!(certFiles.pem_cert && certFiles.pem_key) + : !!(certFiles.pfx && pfxPassword.value) + ) + + return { + certFiles, + certType, + pfxPassword, + pickPEMCertificate, + pickPEMKey, + pickPFXCertificate, + pickCACertificate, + removeCACertificate, + reset, + isValidCertConfig, + } +} diff --git a/packages/hoppscotch-common/src/composables/whats-new.ts b/packages/hoppscotch-common/src/composables/whats-new.ts index 36d14709..547cd708 100644 --- a/packages/hoppscotch-common/src/composables/whats-new.ts +++ b/packages/hoppscotch-common/src/composables/whats-new.ts @@ -5,14 +5,18 @@ import { getService } from "~/modules/dioc" import { PersistenceService } from "~/services/persistence" import { version as hoppscotchCommonPkgVersion } from "./../../package.json" -export function useWhatsNewDialog() { +export async function useWhatsNewDialog() { const persistenceService = getService(PersistenceService) - const versionFromLocalStorage = persistenceService.getLocalConfig("hopp_v") + const versionFromLocalStorage = + await persistenceService.getLocalConfig("hopp_v") // Set new entry `hopp_v` under `localStorage` if not present if (!versionFromLocalStorage) { - persistenceService.setLocalConfig("hopp_v", hoppscotchCommonPkgVersion) + await persistenceService.setLocalConfig( + "hopp_v", + hoppscotchCommonPkgVersion + ) return } @@ -53,7 +57,7 @@ export function useWhatsNewDialog() { }, 10000) } - persistenceService.setLocalConfig("hopp_v", hoppscotchCommonPkgVersion) + await persistenceService.setLocalConfig("hopp_v", hoppscotchCommonPkgVersion) } async function getReleaseNotes(v: string): Promise { diff --git a/packages/hoppscotch-common/src/helpers/RequestRunner.ts b/packages/hoppscotch-common/src/helpers/RequestRunner.ts index 4f9f4f95..66ea291e 100644 --- a/packages/hoppscotch-common/src/helpers/RequestRunner.ts +++ b/packages/hoppscotch-common/src/helpers/RequestRunner.ts @@ -261,9 +261,8 @@ export function runRESTRequest$( variables: finalEnvsWithNonEmptyValues, }) - const [stream, cancelRun] = createRESTNetworkRequestStream( - await effectiveRequest - ) + const [stream, cancelRun] = + await createRESTNetworkRequestStream(effectiveRequest) cancelFunc = cancelRun const subscription = stream diff --git a/packages/hoppscotch-common/src/helpers/actions.ts b/packages/hoppscotch-common/src/helpers/actions.ts index 10d4a38d..c6887a1b 100644 --- a/packages/hoppscotch-common/src/helpers/actions.ts +++ b/packages/hoppscotch-common/src/helpers/actions.ts @@ -67,6 +67,7 @@ export type HoppAction = | "response.save" // Save response | "response.save-as-example" // Save response as example | "modals.login.toggle" // Login to Hoppscotch + | "modals.instance-switcher.toggle" // Switch Hoppscotch instances (self-hosted) | "history.clear" // Clear REST History | "user.login" // Login to Hoppscotch | "user.logout" // Log out of Hoppscotch diff --git a/packages/hoppscotch-common/src/helpers/auth/digest.ts b/packages/hoppscotch-common/src/helpers/auth/digest.ts index c74f92d7..a61b1c03 100644 --- a/packages/hoppscotch-common/src/helpers/auth/digest.ts +++ b/packages/hoppscotch-common/src/helpers/auth/digest.ts @@ -3,7 +3,7 @@ import { md5 } from "js-md5" import { getService } from "~/modules/dioc" import { getI18n } from "~/modules/i18n" -import { InterceptorService } from "~/services/interceptor.service" +import { KernelInterceptorService } from "~/services/kernel-interceptor.service" export interface DigestAuthParams { username: string @@ -85,11 +85,15 @@ export async function fetchInitialDigestAuthInfo( const t = getI18n() try { - const service = getService(InterceptorService) - const initialResponse = await service.runRequest({ + const interceptorService = getService(KernelInterceptorService) + const exec = await interceptorService.execute({ + id: Date.now(), url, method, - }).response + version: "HTTP/1.1", + }) + + const initialResponse = await exec.response if (E.isLeft(initialResponse)) { const initialFetchFailureReason = diff --git a/packages/hoppscotch-common/src/helpers/functional/domain-settings.ts b/packages/hoppscotch-common/src/helpers/functional/domain-settings.ts new file mode 100644 index 00000000..052c426c --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/functional/domain-settings.ts @@ -0,0 +1,228 @@ +import { pipe } from "fp-ts/function" +import * as O from "fp-ts/Option" +import * as E from "fp-ts/Either" +import { StoreFile, RelayRequest } from "@hoppscotch/kernel" + +export type InputDomainSetting = { + version: "v1" + security?: { + certificates?: { + client?: + | { + kind: "pem" + cert?: StoreFile + key?: StoreFile + } + | { + kind: "pfx" + data?: StoreFile + password?: string + } + ca?: StoreFile[] + } + verifyHost?: boolean + verifyPeer?: boolean + } + proxy?: { + url: string + auth?: { + username?: string + password?: string + } + certificates?: { + ca?: StoreFile[] + client?: + | { + kind: "pem" + cert?: StoreFile + key?: StoreFile + } + | { + kind: "pfx" + data?: StoreFile + password?: string + } + } + } +} + +const convertStoreFile = (file: StoreFile): O.Option => + file.include === false ? O.none : O.some(file.content) + +const convertClientCert = ( + cert?: + | { + kind: "pem" + cert?: StoreFile + key?: StoreFile + } + | { + kind: "pfx" + data?: StoreFile + password?: string + } +): O.Option< + NonNullable< + NonNullable< + Pick["security"] + >["certificates"] + >["client"] +> => { + if (!cert) return O.none + + switch (cert.kind) { + case "pem": { + const certContent = pipe( + O.fromNullable(cert.cert), + O.chain(convertStoreFile) + ) + const keyContent = pipe( + O.fromNullable(cert.key), + O.chain(convertStoreFile) + ) + + return pipe( + O.Do, + O.bind("cert", () => certContent), + O.bind("key", () => keyContent), + O.map(({ cert, key }) => ({ + kind: "pem" as const, + cert, + key, + })) + ) + } + case "pfx": { + const dataContent = pipe( + O.fromNullable(cert.data), + O.chain(convertStoreFile) + ) + + return pipe( + O.Do, + O.bind("data", () => dataContent), + O.bind("password", () => O.fromNullable(cert.password)), + O.map(({ data, password }) => ({ + kind: "pfx" as const, + data, + password, + })) + ) + } + } +} + +const convertCaCerts = (certs?: StoreFile[]): O.Option => + pipe( + O.fromNullable(certs), + O.chain((certs: StoreFile[]) => { + const converted = certs + .map(convertStoreFile) + .filter(O.isSome) + .map((opt) => opt.value) + return converted.length > 0 ? O.some(converted) : O.none + }) + ) + +const convertSecurity = ( + security?: InputDomainSetting["security"] +): O.Option["security"]> => + pipe( + O.fromNullable(security), + O.chain((security) => { + return pipe( + O.fromNullable(security.certificates), + O.chain((certificates) => + pipe( + O.Do, + O.bind("client", () => convertClientCert(certificates.client)), + O.bind("ca", () => convertCaCerts(certificates.ca)), + O.map((convertedCerts) => ({ + certificates: { + client: convertedCerts.client, + ca: convertedCerts.ca, + }, + // Default to `false` if not explicitly set + verifyHost: security.verifyHost ?? false, + verifyPeer: security.verifyPeer ?? false, + })) + ) + ), + // If no certificates but security object exists, still return verify settings + O.alt(() => + O.some({ + verifyHost: security.verifyHost ?? false, + verifyPeer: security.verifyPeer ?? false, + }) + ) + ) + }), + // If no security object at all, return default settings + O.alt(() => + O.some({ + verifyHost: false, + verifyPeer: false, + }) + ) + ) + +const convertProxy = ( + proxy?: InputDomainSetting["proxy"] +): O.Option["proxy"]> => + pipe( + O.fromNullable(proxy), + O.chain((proxy) => { + if (!proxy.url) return O.none + + const auth = proxy.auth && { + username: proxy.auth.username || "", + password: proxy.auth.password || "", + } + + return pipe( + O.fromNullable(proxy.certificates), + O.chain((certificates) => + pipe( + O.Do, + O.bind("client", () => convertClientCert(certificates.client)), + O.bind("ca", () => convertCaCerts(certificates.ca)), + O.map((certs) => ({ + client: certs.client, + ca: certs.ca, + })) + ) + ), + O.fold( + () => + O.some({ + url: proxy.url, + ...(auth && { auth }), + }), + (certificates) => + O.some({ + url: proxy.url, + ...(auth && { auth }), + certificates, + }) + ) + ) + }) + ) + +export const convertDomainSetting = ( + input: InputDomainSetting +): E.Either> => { + if (input.version !== "v1") { + return E.left(new Error("Invalid version")) + } + + const security = convertSecurity(input.security) + const proxy = convertProxy(input.proxy) + + const result: Pick = { + proxy: O.isSome(proxy) ? proxy.value : undefined, + security: O.isSome(security) ? security.value : undefined, + } + + return E.right(result) +} diff --git a/packages/hoppscotch-common/src/helpers/functional/filter-active.ts b/packages/hoppscotch-common/src/helpers/functional/filter-active.ts new file mode 100644 index 00000000..568701bf --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/functional/filter-active.ts @@ -0,0 +1,13 @@ +import { HoppRESTRequestVariables } from "@hoppscotch/data" +import { pipe } from "fp-ts/function" +import * as A from "fp-ts/Array" + +export const filterActiveToRecord = ( + headers: HoppRESTRequestVariables +): Record => + pipe( + headers, + A.filter((header) => header.active), + A.map((header): [string, string] => [header.key, header.value]), + (entries) => Object.fromEntries(entries) + ) diff --git a/packages/hoppscotch-common/src/helpers/functional/json.ts b/packages/hoppscotch-common/src/helpers/functional/json.ts index 2bf63ea8..bf4b3f32 100644 --- a/packages/hoppscotch-common/src/helpers/functional/json.ts +++ b/packages/hoppscotch-common/src/helpers/functional/json.ts @@ -1,5 +1,10 @@ import * as O from "fp-ts/Option" -import { flow } from "fp-ts/function" +import * as E from "fp-ts/Either" +import { pipe, flow } from "fp-ts/function" + +import { MediaType, RelayResponseBody } from "@hoppscotch/kernel" + +import { decodeToString } from "~/helpers/functional/parse" type SafeParseJSON = { (str: string, convertToArray: true): O.Option> @@ -26,3 +31,21 @@ export const safeParseJSON: SafeParseJSON = (str, convertToArray = false) => * @returns If string is a JSON string */ export const isJSON = flow(safeParseJSON, O.isSome) + +export const parseBytesToJSON = (content: Uint8Array): O.Option => + pipe( + content, + decodeToString, + E.chain(parseJSONAs), + E.fold(() => O.none, O.some) + ) + +export const parseJSONAs = (str: string): E.Either => + E.tryCatch(() => JSON.parse(str) as T, E.toError) + +export const parseBodyAsJSON = (body: RelayResponseBody): O.Option => + pipe( + O.fromNullable(body.mediaType), + O.filter((type) => type.includes(MediaType.APPLICATION_JSON)), + O.chain(() => parseBytesToJSON(body.body)) + ) diff --git a/packages/hoppscotch-common/src/helpers/functional/parse.ts b/packages/hoppscotch-common/src/helpers/functional/parse.ts new file mode 100644 index 00000000..01f12239 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/functional/parse.ts @@ -0,0 +1,7 @@ +import * as E from "fp-ts/Either" + +export const decodeToString = (content: Uint8Array): E.Either => + E.tryCatch( + () => new TextDecoder("utf-8").decode(content).replace(/\x00/g, ""), + E.toError + ) diff --git a/packages/hoppscotch-common/src/helpers/functional/preprocess.ts b/packages/hoppscotch-common/src/helpers/functional/preprocess.ts new file mode 100644 index 00000000..e70835a4 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/functional/preprocess.ts @@ -0,0 +1,69 @@ +import { pipe } from "fp-ts/function" +import * as O from "fp-ts/Option" +import * as E from "fp-ts/Either" +import * as R from "fp-ts/Record" +import { cloneDeep } from "lodash-es" +import { useSetting } from "~/composables/settings" +import type { RelayRequest } from "@hoppscotch/kernel" + +const isEncoded = (value: string): boolean => + pipe( + E.tryCatch( + () => value !== decodeURIComponent(value), + () => false + ), + E.getOrElse(() => false) + ) + +const encodeParam = (value: string): string => + pipe( + O.some(value), + O.filter((v) => !isEncoded(v)), + O.map(encodeURIComponent), + O.getOrElse(() => value) + ) + +const processParams = ( + params: Record +): Record => { + const encodeMode = useSetting("ENCODE_MODE").value + const needsEncoding = (v: string) => + /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/.test(v) + + return pipe( + params, + R.map((value) => + encodeMode === "enable" || (encodeMode === "auto" && needsEncoding(value)) + ? encodeParam(value) + : value + ) + ) +} + +const updateUrl = ( + url: string, + params: Record +): E.Either => + pipe( + E.tryCatch( + () => new URL(url), + (e) => new Error(`Invalid URL: ${e}`) + ), + E.map((u) => { + Object.entries(processParams(params)).forEach(([k, v]) => + u.searchParams.append(k, v) + ) + return decodeURIComponent(u.toString()) + }) + ) + +export const preProcessRelayRequest = (req: RelayRequest): RelayRequest => + pipe(cloneDeep(req), (req) => + req.params + ? pipe( + updateUrl(req.url, req.params), + E.map((url) => ({ ...req, url, params: {} })), + E.getOrElse(() => req) + ) + : req + ) diff --git a/packages/hoppscotch-common/src/helpers/graphql/connection.ts b/packages/hoppscotch-common/src/helpers/graphql/connection.ts index ce9dd859..5286a75d 100644 --- a/packages/hoppscotch-common/src/helpers/graphql/connection.ts +++ b/packages/hoppscotch-common/src/helpers/graphql/connection.ts @@ -25,9 +25,13 @@ import { getI18n } from "~/modules/i18n" import { addGraphqlHistoryEntry, makeGQLHistoryEntry } from "~/newstore/history" -import { InterceptorService } from "~/services/interceptor.service" +import { KernelInterceptorService } from "~/services/kernel-interceptor.service" import { GQLTabService } from "~/services/tab/graphql" +import { MediaType, content, Method, RelayRequest } from "@hoppscotch/kernel" +import { GQLRequest } from "~/helpers/kernel/gql/request" +import { GQLResponse } from "~/helpers/kernel/gql/response" + const GQL_SCHEMA_POLL_INTERVAL = 7000 type ConnectionRequestOptions = { @@ -111,49 +115,44 @@ export const connection = reactive({ }) export const schema = computed(() => connection.schema) -export const subscriptionState = computed(() => { - return connection.subscriptionState.get(currentTabID.value) -}) +export const subscriptionState = computed(() => + connection.subscriptionState.get(currentTabID.value) +) export const gqlMessageEvent = ref() export const schemaString = computed(() => { - if (!connection.schema) return "" + if (!connection.schema || !(connection.schema instanceof GraphQLSchema)) + return "" return printSchema(connection.schema) }) export const queryFields = computed(() => { - if (!connection.schema) return [] - + if (!connection.schema || !(connection.schema instanceof GraphQLSchema)) + return [] const fields = connection.schema.getQueryType()?.getFields() - if (!fields) return [] - - return Object.values(fields) + return fields ? Object.values(fields) : [] }) export const mutationFields = computed(() => { - if (!connection.schema) return [] - + if (!connection.schema || !(connection.schema instanceof GraphQLSchema)) + return [] const fields = connection.schema.getMutationType()?.getFields() - if (!fields) return [] - - return Object.values(fields) + return fields ? Object.values(fields) : [] }) export const subscriptionFields = computed(() => { - if (!connection.schema) return [] - + if (!connection.schema || !(connection.schema instanceof GraphQLSchema)) + return [] const fields = connection.schema.getSubscriptionType()?.getFields() - if (!fields) return [] - - return Object.values(fields) + return fields ? Object.values(fields) : [] }) export const graphqlTypes = computed(() => { - if (!connection.schema) return [] + if (!connection.schema || !(connection.schema instanceof GraphQLSchema)) + return [] const typeMap = connection.schema.getTypeMap() - const queryTypeName = connection.schema.getQueryType()?.name ?? "" const mutationTypeName = connection.schema.getMutationType()?.name ?? "" const subscriptionTypeName = @@ -193,7 +192,6 @@ export const connect = async ( const poll = async () => { try { await getSchema(options) - // polling for schema if (connection.state !== "CONNECTED") connection.state = "CONNECTED" timeoutSubscription = setTimeout(() => { poll() @@ -201,7 +199,6 @@ export const connect = async ( } catch (error) { connection.state = "ERROR" - // Show an error toast if the introspection attempt failed and not while sending a request if (!isRunGQLOperation) { toast.error(t("graphql.connection_error_http")) } @@ -232,10 +229,6 @@ export const reset = () => { const getSchema = async (options: ConnectionRequestOptions) => { try { - const introspectionQuery = JSON.stringify({ - query: getIntrospectionQuery(), - }) - const { url, request, inheritedHeaders, inheritedAuth } = options const headers = request?.headers || [] @@ -271,62 +264,60 @@ const getSchema = async (options: ConnectionRequestOptions) => { .filter((item) => item.active && item.key !== "") .forEach(({ key, value }) => (finalHeaders[key] = value)) - const reqOptions = { - method: "POST", + const kernelRequest: RelayRequest = { + id: Date.now(), url: options.url, + method: "POST" as Method, + version: "HTTP/1.1", headers: { ...finalHeaders, "content-type": "application/json", }, - data: introspectionQuery, + content: content.json( + { query: getIntrospectionQuery() }, + MediaType.APPLICATION_JSON + ), } - const interceptorService = getService(InterceptorService) + const kernelInterceptorService = getService(KernelInterceptorService) + const { response } = kernelInterceptorService.execute(kernelRequest) - const res = await interceptorService.runRequest(reqOptions).response + const res = await response if (E.isLeft(res)) { connection.state = "ERROR" - if ( - res.left !== "cancellation" && - res.left.error === "NO_PW_EXT_HOOK" && - res.left.humanMessage - ) { + if (res.left !== "cancellation" && typeof res.left === "object") { connection.error = { - type: res.left.error, - message: (t: ReturnType) => - res.left.humanMessage.description(t), + type: res.left.error?.kind || "error", + message: (t: ReturnType) => { + if (res.left !== "cancellation" && typeof res.left === "object") { + return ( + res.left.humanMessage?.description(t) || + t("graphql.connection_error_http") + ) + } + return "Unknown" + }, component: res.left.component, } } - throw new Error(res.left.toString()) - } - - if (res.right.status !== 200) { - connection.state = "ERROR" - connection.error = { - type: "HTTP_ERROR", - message: (t: ReturnType) => - t("graphql.connection_error_http"), - component: undefined, - } - throw new Error("Failed to fetch schema. Status: " + res.right.status) + throw new Error( + typeof res.left === "string" ? res.left : res.left.error.message + ) } const data = res.right - // HACK : Temporary trailing null character issue from the extension fix - const response = new TextDecoder("utf-8") - .decode(data.data as any) - .replace(/\0+$/, "") + const decoder = new TextDecoder("utf-8") + const responseText = decoder.decode(data.body.body) - const introspectResponse = JSON.parse(response) + const introspectResponse = JSON.parse(responseText) - const schema = buildClientSchema(introspectResponse.data) + const schemaData = buildClientSchema(introspectResponse.data) - connection.schema = schema + connection.schema = schemaData connection.error = null } catch (e: any) { console.error(e) @@ -380,6 +371,15 @@ export const runGQLOperation = async (options: RunQueryOptions) => { const { authHeaders, authParams } = await generateAuthHeader(url, auth) + let finalUrl = url + if (Object.keys(authParams).length > 0) { + const urlObj = new URL(url) + for (const [key, value] of Object.entries(authParams)) { + urlObj.searchParams.append(key, value) + } + finalUrl = urlObj.toString() + } + runHeaders.forEach((header) => { if (header.active && header.key !== "") { finalHeaders[header.key] = header.value @@ -387,74 +387,91 @@ export const runGQLOperation = async (options: RunQueryOptions) => { }) Object.assign(finalHeaders, authHeaders) - const parsedVariables = JSON.parse(variables || "{}") - - const params: Record = {} - headers .filter((item) => item.active && item.key !== "") .forEach(({ key, value }) => (finalHeaders[key] = value)) - const reqOptions = { - method: "POST", - url, - headers: { - ...finalHeaders, - "content-type": "application/json", - }, - data: JSON.stringify({ - query, - variables: parsedVariables, - operationName, - }), - params: { - ...params, - ...authParams, - }, + const gqlRequest: HoppGQLRequest = { + v: 8, + name: options.name || "Untitled Request", + url: finalUrl, + headers: request.headers, + query, + variables, + auth: request.auth as HoppGQLAuth, } if (operationType === "subscription") { return runSubscription(options, finalHeaders) } - const interceptorService = getService(InterceptorService) - const result = await interceptorService.runRequest(reqOptions).response + try { + const kernelRequest = await GQLRequest.toRequest(gqlRequest) - if (E.isLeft(result)) { - if ( - result.left !== "cancellation" && - result.left.error === "NO_PW_EXT_HOOK" && - result.left.humanMessage - ) { - connection.error = { - type: result.left.error, - message: (t: ReturnType) => - result.left.humanMessage.description(t), - component: result.left.component, + if (operationName) { + if (kernelRequest.content?.kind === "json") { + const content = kernelRequest.content.content as any + content.operationName = operationName + kernelRequest.content.content = content } } - throw new Error(result.left.toString()) + + const kernelInterceptorService = getService(KernelInterceptorService) + const { response } = kernelInterceptorService.execute(kernelRequest) + + const result = await response + + if (E.isLeft(result)) { + if (result.left !== "cancellation" && typeof result.left === "object") { + connection.error = { + type: result.left.error?.kind || "error", + message: (t: ReturnType) => { + if ( + result.left !== "cancellation" && + typeof result.left === "object" + ) { + return ( + result.left.humanMessage?.description(t) || + t("graphql.operation_error") + ) + } + return "Unknown" + }, + component: result.left.component, + } + } + + throw new Error( + typeof result.left === "string" + ? result.left + : result.left.error.message + ) + } + + const relayResponse = result.right + + const parsedResponse = await GQLResponse.toResponse(relayResponse, options) + + if (parsedResponse.type === "error") { + throw new Error(parsedResponse.error.message) + } + + gqlMessageEvent.value = parsedResponse + + addQueryToHistory(options, parsedResponse.data) + + return parsedResponse.data + } catch (error: any) { + gqlMessageEvent.value = { + type: "error", + error: { + type: "network_error", + message: error.message || "An unknown error occurred", + }, + } + + throw error } - - const res = result.right - - // HACK: Temporary trailing null character issue from the extension fix - const responseText = new TextDecoder("utf-8") - .decode(res.data as any) - .replace(/\0+$/, "") - - gqlMessageEvent.value = { - type: "response", - time: Date.now(), - operationName: operationName ?? "query", - data: responseText, - rawQuery: options, - operationType, - } - - addQueryToHistory(options, responseText) - - return responseText } const generateAuthHeader = async ( diff --git a/packages/hoppscotch-common/src/helpers/import-export/export/index.ts b/packages/hoppscotch-common/src/helpers/import-export/export/index.ts index 84a14348..06b25c56 100644 --- a/packages/hoppscotch-common/src/helpers/import-export/export/index.ts +++ b/packages/hoppscotch-common/src/helpers/import-export/export/index.ts @@ -16,7 +16,7 @@ export const initializeDownloadFile = async ( const fileName = name ?? url.split("/").pop()!.split("#")[0].split("?")[0] - const result = await platform.io.saveFileWithDialog({ + const result = await platform.kernelIO.saveFileWithDialog({ data: contentsJSON, contentType: "application/json", suggestedFilename: `${fileName}.json`, diff --git a/packages/hoppscotch-common/src/helpers/import-export/export/testResults.ts b/packages/hoppscotch-common/src/helpers/import-export/export/testResults.ts index 6c9bea65..a86b9269 100644 --- a/packages/hoppscotch-common/src/helpers/import-export/export/testResults.ts +++ b/packages/hoppscotch-common/src/helpers/import-export/export/testResults.ts @@ -9,7 +9,7 @@ export const exportTestResults = async (testResults: HoppTestResult) => { const fileName = url.split("/").pop()!.split("#")[0].split("?")[0] - const result = await platform.io.saveFileWithDialog({ + const result = await platform.kernelIO.saveFileWithDialog({ data: contentsJSON, contentType: "application/json", suggestedFilename: `${fileName}.json`, diff --git a/packages/hoppscotch-common/src/helpers/import-export/import/import-sources/GistSource.ts b/packages/hoppscotch-common/src/helpers/import-export/import/import-sources/GistSource.ts index b7486783..7a37f5fb 100644 --- a/packages/hoppscotch-common/src/helpers/import-export/import/import-sources/GistSource.ts +++ b/packages/hoppscotch-common/src/helpers/import-export/import/import-sources/GistSource.ts @@ -2,14 +2,16 @@ import UrlImport from "~/components/importExport/ImportExportSteps/UrlImport.vue import { defineStep } from "~/composables/step-components" import * as E from "fp-ts/Either" +import * as O from "fp-ts/Option" import { z } from "zod" import { v4 as uuidv4 } from "uuid" import { Ref } from "vue" import { getService } from "~/modules/dioc" -import { InterceptorService } from "~/services/interceptor.service" +import { KernelInterceptorService } from "~/services/kernel-interceptor.service" +import { parseBodyAsJSON } from "~/helpers/functional/json" -const interceptorService = getService(InterceptorService) +const interceptorService = getService(KernelInterceptorService) export function GistSource(metadata: { caption: string @@ -48,31 +50,27 @@ export function GistSource(metadata: { } const fetchGistFromUrl = async (url: string) => { - const res = await interceptorService.runRequest({ + const { response } = interceptorService.execute({ + id: Date.now(), url: `https://api.github.com/gists/${url.split("/").pop()}`, + method: "GET", + version: "HTTP/1.1", headers: { Accept: "application/vnd.github.v3+json", }, }) - const response = await res.response + const res = await response - if (E.isLeft(response)) { + if (E.isLeft(res)) { return E.left("REQUEST_FAILED") } - // convert ArrayBuffer to string - if (!(response.right.data instanceof ArrayBuffer)) { - return E.left("REQUEST_FAILED") + const responsePayload = parseBodyAsJSON(res.right.body) + + if (O.isSome(responsePayload)) { + return E.right(responsePayload) } - try { - return E.right( - JSON.parse( - InterceptorService.convertArrayBufferToString(response.right.data) - ) - ) - } catch (e) { - return E.left("REQUEST_FAILED") - } + return E.left("REQUEST_FAILED") } diff --git a/packages/hoppscotch-common/src/helpers/kernel/__tests__/kernel.spec.ts b/packages/hoppscotch-common/src/helpers/kernel/__tests__/kernel.spec.ts new file mode 100644 index 00000000..de00eec4 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/kernel/__tests__/kernel.spec.ts @@ -0,0 +1,353 @@ +import { describe, expect, it } from "vitest" +import { GQLResponse } from "../gql/response" +import { GQLRequest } from "../gql/request" +import { RESTResponse } from "../rest/response" +import { RESTRequest } from "../rest/request" +import { HoppRESTRequest } from "@hoppscotch/data" +import { RunQueryOptions } from "~/helpers/graphql/connection" +import { MediaType } from "@hoppscotch/kernel" +import { HoppGQLRequest } from "@hoppscotch/data" +import { EffectiveHoppRESTRequest } from "~/helpers/utils/EffectiveURL" +import { RelayResponse } from "@hoppscotch/kernel" + +describe("GraphQL Response Transformation", () => { + const baseOptions: RunQueryOptions = { + url: "https://api.example.com/graphql", + headers: [], + query: "", + variables: "", + auth: { authType: "none", authActive: true }, + operationName: "TestQuery", + operationType: "query", + } + + it("successfully transforms a valid GraphQL response with data", async () => { + const json = JSON.stringify({ data: { hello: "world" } }) + const mockResponse: RelayResponse = { + status: 200, + headers: {}, + body: { + mediaType: "application/json", + body: new Uint8Array(Buffer.from(json)), + }, + meta: { + timing: { + start: 100, + end: 200, + }, + }, + } + + const options = { + ...baseOptions, + query: "query TestQuery { hello }", + } + + const result = await GQLResponse.toResponse( + mockResponse as RelayResponse, + options + ) + + expect(result).toHaveProperty("type", "response") + if (result.type === "response") { + expect(result.operationName).toBe("TestQuery") + expect(result.time).toBe(100) + expect(JSON.parse(result.data)).toHaveProperty("data.hello", "world") + } + }) + + it("successfully transforms a GraphQL response with errors", async () => { + const mockResponse: RelayResponse = { + status: 200, + headers: {}, + body: { + mediaType: "application/json", + body: new Uint8Array( + Buffer.from( + JSON.stringify({ + errors: [ + { message: "Field 'invalid' does not exist" }, + { message: "Syntax error" }, + ], + }) + ) + ), + }, + meta: { + timing: { + start: 0, + end: 150, + }, + }, + } + + const options = { + ...baseOptions, + query: "query TestQuery { invalid }", + } + + const result = await GQLResponse.toResponse( + mockResponse as RelayResponse, + options + ) + + expect(result).toHaveProperty("type", "response") + if (result.type === "response") { + const parsedData = JSON.parse(result.data) + expect(parsedData.errors).toHaveLength(2) + expect(parsedData.errors[0].message).toBe( + "Field 'invalid' does not exist" + ) + } + }) + + it("returns transform error for non-GraphQL JSON response", async () => { + const mockResponse: RelayResponse = { + status: 200, + headers: {}, + body: { + mediaType: "application/json", + body: new Uint8Array( + Buffer.from( + JSON.stringify({ + someField: "not a graphql response", + }) + ) + ), + }, + } + + const options = { + ...baseOptions, + query: "query TestQuery { hello }", + } + + const result = await GQLResponse.toResponse( + mockResponse as RelayResponse, + options + ) + + expect(result).toHaveProperty("type", "error") + if (result.type === "error") { + expect(result.error.message).toBe("Invalid GraphQL response structure") + } + }) +}) + +describe("GraphQL Request Transformation", () => { + const baseRequest: HoppGQLRequest = { + v: 8, + name: "Test Query", + url: "https://api.example.com/graphql", + headers: [], + query: "", + variables: "", + auth: { authType: "none", authActive: true }, + } + + it("transforms a basic GraphQL request correctly", async () => { + const request: HoppGQLRequest = { + ...baseRequest, + query: "query { hello }", + } + + const result = await GQLRequest.toRequest(request) + + expect(result).toMatchObject({ + url: "https://api.example.com/graphql", + method: "POST", + version: "HTTP/1.1", + headers: { "content-type": "application/json" }, + auth: { kind: "none" }, + content: { + kind: "json", + content: { query: "query { hello }", variables: undefined }, + mediaType: "application/json", + }, + }) + + expect(result.content?.mediaType).toBe(MediaType.APPLICATION_JSON) + expect(result.content?.content).toBeDefined() + if (result.content?.body) { + const parsedBody = + typeof result.content.body === "string" + ? JSON.parse(result.content.body) + : JSON.parse(new TextDecoder().decode(result.content.body)) + + expect(parsedBody).toEqual({ + query: request.query, + variables: undefined, + }) + } + }) + + it("properly parses and includes variables when provided", async () => { + const request: HoppGQLRequest = { + ...baseRequest, + query: "query($id: ID!) { item(id: $id) { name } }", + variables: '{ "id": "123" }', + } + + const result = await GQLRequest.toRequest(request) + + expect(result.content?.kind).toBe("json") + expect(result.content?.content).toBeDefined() + if (result.content?.content) { + expect(result.content?.content).toEqual({ + query: request.query, + variables: { id: "123" }, + }) + } + }) + + it("throws error for invalid JSON in variables", async () => { + const request: HoppGQLRequest = { + ...baseRequest, + query: "query($id: ID!) { item(id: $id) { name } }", + variables: "{ invalid json }", + } + + await expect(GQLRequest.toRequest(request)).rejects.toThrow("Invalid JSON") + }) +}) + +describe("REST Response Transformation", () => { + it("transforms a successful REST response", async () => { + const mockResponse = { + body: { + body: new Uint8Array([72, 101, 108, 108, 111]), + }, + headers: { + "content-type": "text/plain", + "x-custom": "test", + }, + status: 200, + statusText: "OK", + meta: { + timing: { + start: 100, + end: 200, + }, + size: { + total: 5, + }, + }, + } + + const originalRequest: HoppRESTRequest = { + v: "11", + endpoint: "https://api.example.com", + name: "Test Request", + method: "GET", + params: [], + headers: [], + preRequestScript: "", + testScript: "", + auth: { authType: "none", authActive: true }, + body: { contentmediaType: null, body: null }, + requestVariables: [], + responses: {}, + } + + const result = await RESTResponse.toResponse( + mockResponse as any, + originalRequest + ) + + expect(result).toHaveProperty("type", "success") + if (result.type === "success") { + expect(result.statusCode).toBe(200) + expect(result.meta.responseDuration).toBe(100) + expect(result.meta.responseSize).toBe(5) + expect(result.headers).toHaveLength(2) + } + }) + + it("returns transform error for invalid response body", async () => { + const mockResponse = { + body: { + body: "invalid body format", + }, + } + + const originalRequest: HoppRESTRequest = { + v: "11", + endpoint: "https://api.example.com", + name: "Test Request", + method: "GET", + params: [], + headers: [], + preRequestScript: "", + testScript: "", + auth: { authType: "none", authActive: true }, + body: { contentmediaType: null, body: null }, + requestVariables: [], + responses: {}, + } + + const result = await RESTResponse.toResponse( + mockResponse as any, + originalRequest + ) + + expect(result).toHaveProperty("type", "fail") + if (result.type === "fail") { + expect(result.error.type).toBe("transform_error") + expect(result.error.message).toBe("Invalid response body format") + } + }) +}) + +describe("REST Request Transformation", () => { + const baseEffectiveRequest: EffectiveHoppRESTRequest = { + v: "11", + name: "Test Request", + method: "GET", + endpoint: "https://api.example.com", + effectiveFinalURL: "https://api.example.com", + params: [], + headers: [], + preRequestScript: "", + testScript: "", + auth: { authType: "none", authActive: true }, + body: { contentmediaType: null, body: null }, + requestVariables: [], + responses: {}, + effectiveFinalHeaders: [], + effectiveFinalParams: [], + effectiveFinalBody: null, + effectiveFinalRequestVariables: [], + } + + it("transforms a basic REST request correctly", async () => { + const result = await RESTRequest.toRequest(baseEffectiveRequest) + + expect(result).toMatchObject({ + url: baseEffectiveRequest.effectiveFinalURL, + method: "GET", + headers: {}, + params: {}, + auth: { kind: "none" }, + }) + }) + + it("includes auth parameters when basic auth is active", async () => { + const request: EffectiveHoppRESTRequest = { + ...baseEffectiveRequest, + auth: { + authType: "basic", + authActive: true, + username: "testuser", + password: "testpass", + }, + } + + const result = await RESTRequest.toRequest(request) + + expect(result.auth).toMatchObject({ + kind: "basic", + username: "testuser", + password: "testpass", + }) + }) +}) diff --git a/packages/hoppscotch-common/src/helpers/kernel/common/auth.ts b/packages/hoppscotch-common/src/helpers/kernel/common/auth.ts new file mode 100644 index 00000000..dd686179 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/kernel/common/auth.ts @@ -0,0 +1,286 @@ +import { HoppRESTRequest, HoppRESTAuthOAuth2 } from "@hoppscotch/data" +import { AuthType } from "@hoppscotch/kernel" +import * as E from "fp-ts/Either" +import * as O from "fp-ts/Option" +import * as TE from "fp-ts/TaskEither" +import { flow, pipe } from "fp-ts/function" + +type HoppAuth = HoppRESTRequest["auth"] +type OAuth2GrantType = HoppRESTAuthOAuth2["grantTypeInfo"] +type GrantType = Extract["grantType"] + +export const defaultAuth: AuthType = { kind: "none" } + +const isAuthActive = (auth: HoppAuth): boolean => + auth.authActive && auth.authType !== "none" && auth.authType !== "inherit" + +const Guards = { + basic: flow( + O.fromPredicate( + (auth: HoppAuth): auth is HoppAuth & { authType: "basic" } => + auth.authType === "basic" + ) + ), + bearer: flow( + O.fromPredicate( + (auth: HoppAuth): auth is HoppAuth & { authType: "bearer" } => + auth.authType === "bearer" + ) + ), + apiKey: flow( + O.fromPredicate( + (auth: HoppAuth): auth is HoppAuth & { authType: "api-key" } => + auth.authType === "api-key" + ) + ), + aws: flow( + O.fromPredicate( + (auth: HoppAuth): auth is HoppAuth & { authType: "aws-signature" } => + auth.authType === "aws-signature" + ) + ), + digest: flow( + O.fromPredicate( + (auth: HoppAuth): auth is HoppAuth & { authType: "digest" } => + auth.authType === "digest" + ) + ), + oauth2: flow( + O.fromPredicate( + (auth: HoppAuth): auth is HoppAuth & { authType: "oauth-2" } => + auth.authType === "oauth-2" + ) + ), + grants: { + authCode: flow( + O.fromPredicate( + ( + g: OAuth2GrantType + ): g is Extract => + g.grantType === "AUTHORIZATION_CODE" + ) + ), + clientCreds: flow( + O.fromPredicate( + ( + g: OAuth2GrantType + ): g is Extract => + g.grantType === "CLIENT_CREDENTIALS" + ) + ), + password: flow( + O.fromPredicate( + ( + g: OAuth2GrantType + ): g is Extract => + g.grantType === "PASSWORD" + ) + ), + implicit: flow( + O.fromPredicate( + ( + g: OAuth2GrantType + ): g is Extract => + g.grantType === "IMPLICIT" + ) + ), + }, +} + +type AuthProcessor = ( + auth: T +) => E.Either + +const Processors: { + basic: AuthProcessor + bearer: AuthProcessor + apiKey: AuthProcessor + aws: AuthProcessor + digest: AuthProcessor + oauth2: { + processGrant: ( + grant: OAuth2GrantType + ) => E.Either + process: AuthProcessor + } +} = { + basic: flow( + Guards.basic, + O.map((a) => ({ + kind: "basic" as const, + username: a.username, + password: a.password, + })), + E.fromOption(() => new Error("Invalid basic auth")) + ), + + bearer: flow( + Guards.bearer, + O.map((a) => ({ + kind: "bearer" as const, + token: a.token, + })), + E.fromOption(() => new Error("Invalid bearer auth")) + ), + + apiKey: flow( + Guards.apiKey, + O.map((a) => ({ + kind: "apikey" as const, + key: a.key, + value: a.value, + in: a.addTo === "HEADERS" ? "header" : "query", + })), + E.fromOption(() => new Error("Invalid API key auth")) + ), + + aws: flow( + Guards.aws, + O.map((a) => ({ + kind: "aws" as const, + accessKey: a.accessKey, + secretKey: a.secretKey, + region: a.region, + service: a.serviceName, + sessionToken: a.serviceToken, + in: a.addTo === "HEADERS" ? "header" : "query", + })), + E.fromOption(() => new Error("Invalid AWS auth")) + ), + + digest: flow( + Guards.digest, + O.map((a) => ({ + kind: "digest" as const, + username: a.username, + password: a.password, + realm: a.realm, + nonce: a.nonce, + algorithm: a.algorithm === "MD5" ? "MD5" : "SHA-256", + qop: a.qop, + nc: a.nc, + cnonce: a.cnonce, + opaque: a.opaque, + })), + E.fromOption(() => new Error("Invalid digest auth")) + ), + + oauth2: { + processGrant: ( + grant: OAuth2GrantType + ): E.Either => + pipe( + grant, + (g) => + pipe( + O.none as O.Option, + O.alt(() => + pipe( + Guards.grants.authCode(g), + O.map( + (g): GrantType => ({ + kind: "authorization_code", + authEndpoint: g.authEndpoint, + tokenEndpoint: g.tokenEndpoint, + clientId: g.clientID, + clientSecret: g.clientSecret, + }) + ) + ) + ), + O.alt(() => + pipe( + Guards.grants.clientCreds(g), + O.map( + (g): GrantType => ({ + kind: "client_credentials", + tokenEndpoint: g.authEndpoint, + clientId: g.clientID, + clientSecret: g.clientSecret, + }) + ) + ) + ), + O.alt(() => + pipe( + Guards.grants.password(g), + O.map( + (g): GrantType => ({ + kind: "password", + tokenEndpoint: g.authEndpoint, + username: g.username, + password: g.password, + }) + ) + ) + ), + O.alt(() => + pipe( + Guards.grants.implicit(g), + O.map( + (g): GrantType => ({ + kind: "implicit", + authEndpoint: g.authEndpoint, + clientId: g.clientID, + }) + ) + ) + ) + ), + E.fromOption(() => new Error("Invalid grant type")) + ), + process: flow( + Guards.oauth2, + E.fromOption(() => new Error("Invalid OAuth2 auth")), + E.chain((a) => + pipe( + Processors.oauth2.processGrant(a.grantTypeInfo), + E.map((grantType) => ({ + kind: "oauth2" as const, + accessToken: a.grantTypeInfo.token, + refreshToken: + "refreshToken" in a.grantTypeInfo + ? a.grantTypeInfo.refreshToken + : undefined, + grantType, + })) + ) + ) + ), + }, +} + +const getProcessor = ( + auth: HoppAuth +): O.Option<(auth: HoppAuth) => E.Either> => + pipe( + O.fromNullable(auth.authType), + O.chain((type) => { + switch (type) { + case "basic": + return O.some(Processors.basic) + case "bearer": + return O.some(Processors.bearer) + case "api-key": + return O.some(Processors.apiKey) + case "aws-signature": + return O.some(Processors.aws) + case "digest": + return O.some(Processors.digest) + case "oauth-2": + return O.some(Processors.oauth2.process) + default: + return O.none + } + }) + ) + +export const transformAuth = (auth: HoppAuth): TE.TaskEither => + pipe( + auth, + O.fromPredicate(isAuthActive), + O.chain(getProcessor), + O.map((processor) => processor(auth)), + O.getOrElse(() => E.right(defaultAuth)), + TE.fromEither + ) diff --git a/packages/hoppscotch-common/src/helpers/kernel/common/content.ts b/packages/hoppscotch-common/src/helpers/kernel/common/content.ts new file mode 100644 index 00000000..1aa54629 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/kernel/common/content.ts @@ -0,0 +1,192 @@ +import * as E from "fp-ts/Either" +import * as TE from "fp-ts/TaskEither" +import * as O from "fp-ts/Option" +import * as A from "fp-ts/Array" +import { pipe } from "fp-ts/function" + +import { parseJSONAs } from "~/helpers/functional/json" +import { ContentType, MediaType, content } from "@hoppscotch/kernel" +import { EffectiveHoppRESTRequest } from "~/helpers/utils/EffectiveURL" + +type FormDataValue = { + kind: "file" + filename: string + contentType: string + data: Uint8Array +} + +const Processors = { + json: { + process: (body: string): E.Either => + pipe( + parseJSONAs(body), + E.map((json) => content.json(json, MediaType.APPLICATION_JSON)), + E.orElse(() => E.right(content.text(body, MediaType.TEXT_PLAIN))) + ), + }, + + multipart: { + processFile: (entry: { + key: string + file: Blob + contentType?: string + }): TE.TaskEither => + pipe( + TE.tryCatch( + () => entry.file.arrayBuffer(), + () => new Error("File read failed") + ), + TE.map((buffer) => ({ + key: entry.key, + value: [ + { + kind: "file", + filename: + entry.file instanceof File ? entry.file.name : "unknown", + contentType: + entry.contentType ?? + (entry.file instanceof File + ? entry.file.type + : "application/octet-stream"), + data: new Uint8Array(buffer), + }, + ], + })) + ), + + process: (formData: FormData): TE.TaskEither => + pipe( + TE.tryCatch( + async () => { + const entries = [] as { + key: string + file: Blob + contentType?: string + }[] + // @ts-expect-error: `formData.entries` does exist but isn't visible, + // see `"lib": ["ESNext", "DOM"],` in `tsconfig.json` + for (const [key, value] of formData.entries()) { + if (value instanceof Blob) { + entries.push({ + key, + file: value, + contentType: value.type || undefined, + }) + } + } + return entries + }, + () => new Error("FormData processing failed") + ), + TE.chain((entries) => + pipe( + entries, + A.traverse(TE.ApplicativePar)(Processors.multipart.processFile), + TE.map( + A.reduce( + new Map(), + (acc, { key, value }) => { + acc.set(key, value) + return acc + } + ) + ) + ) + ), + TE.map((entries) => content.multipart(entries)) + ), + }, + + binary: { + process: (file: Blob): TE.TaskEither => + pipe( + TE.tryCatch( + () => file.arrayBuffer(), + () => new Error("Binary read failed") + ), + TE.map((buffer) => + content.binary( + new Uint8Array(buffer), + file.type || "application/octet-stream", + file instanceof File ? file.name : "unknown" + ) + ) + ), + }, + + urlencoded: { + process: (body: string): E.Either => + pipe( + E.right(body), + E.map((contents) => { + return content.urlencoded(contents) + }) + ), + }, + + xml: { + process: (body: string): E.Either => + E.right(content.xml(body, MediaType.APPLICATION_XML)), + }, + + text: { + process: (body: string): E.Either => + E.right(content.text(body, MediaType.TEXT_PLAIN)), + }, +} + +const getProcessor = (contentType: string) => { + switch (contentType) { + case "application/json": + case "application/ld+json": + case "application/hal+json": + case "application/vnd.api+json": + return Processors.json.process + case "application/xml": + case "text/xml": + return Processors.xml.process + case "application/x-www-form-urlencoded": + return Processors.urlencoded.process + case "text/html": + case "text/plain": + return Processors.text.process + default: + return Processors.text.process + } +} + +export const transformContent = ( + request: EffectiveHoppRESTRequest +): TE.TaskEither> => { + const { body, effectiveFinalBody } = request + + if (!body.contentType || !effectiveFinalBody) { + return TE.right(O.none) + } + + switch (body.contentType) { + case "multipart/form-data": + if (!(effectiveFinalBody instanceof FormData)) { + return TE.right(O.none) + } + return pipe( + Processors.multipart.process(effectiveFinalBody), + TE.map(O.some) + ) + + case "application/octet-stream": + if (!(effectiveFinalBody instanceof Blob)) { + return TE.right(O.none) + } + return pipe(Processors.binary.process(effectiveFinalBody), TE.map(O.some)) + + default: + if (typeof effectiveFinalBody !== "string") { + return TE.right(O.none) + } + return pipe( + TE.fromEither(getProcessor(body.contentType)(effectiveFinalBody)), + TE.map(O.some) + ) + } +} diff --git a/packages/hoppscotch-common/src/helpers/kernel/common/index.ts b/packages/hoppscotch-common/src/helpers/kernel/common/index.ts new file mode 100644 index 00000000..f7e55e51 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/kernel/common/index.ts @@ -0,0 +1,2 @@ +export { transformContent } from "./content" +export { transformAuth } from "./auth" diff --git a/packages/hoppscotch-common/src/helpers/kernel/gql/request.ts b/packages/hoppscotch-common/src/helpers/kernel/gql/request.ts new file mode 100644 index 00000000..d153cce0 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/kernel/gql/request.ts @@ -0,0 +1,48 @@ +import * as TE from "fp-ts/TaskEither" +import * as T from "fp-ts/Task" +import { pipe } from "fp-ts/function" + +import { AuthType, MediaType, content } from "@hoppscotch/kernel" +import { HoppGQLRequest } from "@hoppscotch/data" + +import { transformAuth } from "~/helpers/kernel/common" +import { defaultAuth } from "~/helpers/kernel/common/auth" +import { filterActiveToRecord } from "~/helpers/functional/filter-active" + +const parseVariables = async (variables: string | null): Promise => { + if (!variables) return undefined + try { + return JSON.parse(variables) + } catch { + throw new Error("Invalid JSON") + } +} + +export const GQLRequest = { + async toRequest(request: HoppGQLRequest) { + const headers = { + ...filterActiveToRecord(request.headers), + "content-type": "application/json", + } + + const auth = await pipe( + transformAuth(request.auth), + TE.getOrElse(() => T.of(defaultAuth)) + )() + + const variables = await parseVariables(request.variables) + + return { + id: Date.now(), + url: request.url, + method: "POST", // GQL specs + version: "HTTP/1.1", + headers, + auth, + content: content.json( + { query: request.query, variables }, + MediaType.APPLICATION_JSON + ), + } + }, +} diff --git a/packages/hoppscotch-common/src/helpers/kernel/gql/response.ts b/packages/hoppscotch-common/src/helpers/kernel/gql/response.ts new file mode 100644 index 00000000..e194a273 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/kernel/gql/response.ts @@ -0,0 +1,89 @@ +import * as O from "fp-ts/Option" +import { pipe } from "fp-ts/function" +import { RelayResponse } from "@hoppscotch/kernel" +import { RunQueryOptions } from "~/helpers/graphql/connection" +import { OperationType } from "@urql/core" +import { parseBodyAsJSON } from "~/helpers/functional/json" + +export type HoppGQLSuccessResponse = { + type: "response" + time: number + operationName: string | undefined + operationType: OperationType + data: string + rawQuery?: RunQueryOptions +} + +export type GQLTransformError = { + type: "error" + error: { + type: "transform_error" + message: string + } +} + +type GQLParsedResponse = { + data?: unknown + errors?: Array<{ message: string }> +} + +const determineOperationType = (query: string): OperationType => { + const trimmed = query.trim().toLowerCase() + if (trimmed.startsWith("mutation")) return "mutation" + if (trimmed.startsWith("subscription")) return "subscription" + return "query" +} + +const createTransformError = (message: string): GQLTransformError => ({ + type: "error", + error: { + type: "transform_error" as const, + message, + }, +}) + +const validateResponse = (data: unknown): GQLParsedResponse | null => { + if (typeof data !== "object" || data === null) return null + + const hasValidData = "data" in data && data.data !== undefined + const hasValidErrors = + "errors" in data && + Array.isArray(data.errors) && + data.errors.every( + (e) => typeof e === "object" && e !== null && "message" in e + ) + + return hasValidData || hasValidErrors ? (data as GQLParsedResponse) : null +} + +export const GQLResponse = { + async toResponse( + response: RelayResponse, + options: RunQueryOptions + ): Promise { + const parsedJSON = pipe( + response.body, + parseBodyAsJSON, + O.fold( + () => createTransformError("Invalid JSON response"), + (json) => { + const validBody = validateResponse(json) + return validBody + ? { + type: "response" as const, + time: response.meta?.timing + ? response.meta.timing.end - response.meta.timing.start + : 0, + operationName: options.operationName, + operationType: determineOperationType(options.query), + data: JSON.stringify(validBody, null, 2), + rawQuery: options, + } + : createTransformError("Invalid GraphQL response structure") + } + ) + ) + + return parsedJSON + }, +} diff --git a/packages/hoppscotch-common/src/helpers/kernel/rest/index.ts b/packages/hoppscotch-common/src/helpers/kernel/rest/index.ts new file mode 100644 index 00000000..6e2e2a7e --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/kernel/rest/index.ts @@ -0,0 +1,2 @@ +export { RESTRequest } from "./request" +export { RESTResponse } from "./response" diff --git a/packages/hoppscotch-common/src/helpers/kernel/rest/request.ts b/packages/hoppscotch-common/src/helpers/kernel/rest/request.ts new file mode 100644 index 00000000..bba7fa8f --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/kernel/rest/request.ts @@ -0,0 +1,40 @@ +import * as TE from "fp-ts/TaskEither" +import * as T from "fp-ts/Task" +import * as O from "fp-ts/Option" +import { pipe } from "fp-ts/function" + +import { Method, RelayRequest, ContentType, AuthType } from "@hoppscotch/kernel" +import { EffectiveHoppRESTRequest } from "~/helpers/utils/EffectiveURL" + +import { transformAuth, transformContent } from "~/helpers/kernel/common" +import { defaultAuth } from "~/helpers/kernel/common/auth" +import { filterActiveToRecord } from "~/helpers/functional/filter-active" + +export const RESTRequest = { + async toRequest(request: EffectiveHoppRESTRequest): Promise { + const auth = await pipe( + transformAuth(request.auth), + TE.getOrElse(() => T.of(defaultAuth)) + )() + + const content = await pipe( + transformContent(request), + TE.getOrElse(() => T.of>(O.none)), + T.map(O.toUndefined) + )() + + const headers = filterActiveToRecord(request.effectiveFinalHeaders) + const params = filterActiveToRecord(request.effectiveFinalParams) + + return { + id: Date.now(), + url: request.effectiveFinalURL, + method: request.method.toUpperCase() as Method, + version: "HTTP/1.1", + headers, + params, + auth, + content, + } + }, +} diff --git a/packages/hoppscotch-common/src/helpers/kernel/rest/response.ts b/packages/hoppscotch-common/src/helpers/kernel/rest/response.ts new file mode 100644 index 00000000..338f54a1 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/kernel/rest/response.ts @@ -0,0 +1,54 @@ +import { RelayResponse } from "@hoppscotch/kernel" +import { HoppRESTRequest } from "@hoppscotch/data" +import { + HoppRESTResponseHeader, + HoppRESTSuccessResponse, +} from "~/helpers/types/HoppRESTResponse" + +export type HoppRESTTransformError = { + type: "fail" + error: { + type: "transform_error" + message: string + } +} + +const extractTiming = (response: RelayResponse): number => + response.meta?.timing + ? response.meta.timing.end - response.meta.timing.start + : 0 + +const extractSize = (response: RelayResponse): number => + response.meta?.size?.total ?? 0 + +export const RESTResponse = { + async toResponse( + response: RelayResponse, + originalRequest: HoppRESTRequest + ): Promise { + if (!response.body.body || !(response.body.body instanceof Uint8Array)) { + return { + type: "fail", + error: { + type: "transform_error", + message: "Invalid response body format", + }, + } + } + + return { + type: "success", + headers: Object.entries(response.headers ?? {}).map( + ([key, value]) => ({ key, value }) as HoppRESTResponseHeader + ), + body: response.body.body.buffer, + statusCode: response.status, + statusText: response.statusText ?? "", + meta: { + responseSize: extractSize(response), + responseDuration: extractTiming(response), + }, + req: originalRequest, + } + }, +} diff --git a/packages/hoppscotch-common/src/helpers/network.ts b/packages/hoppscotch-common/src/helpers/network.ts index 1c0b07e5..d8808be9 100644 --- a/packages/hoppscotch-common/src/helpers/network.ts +++ b/packages/hoppscotch-common/src/helpers/network.ts @@ -1,49 +1,16 @@ -import { AxiosRequestConfig } from "axios" +import * as TE from "fp-ts/TaskEither" import { BehaviorSubject, Observable } from "rxjs" import { cloneDeep } from "lodash-es" -import * as E from "fp-ts/Either" -import * as TE from "fp-ts/TaskEither" import { HoppRESTResponse } from "./types/HoppRESTResponse" import { EffectiveHoppRESTRequest } from "./utils/EffectiveURL" import { getService } from "~/modules/dioc" -import { - InterceptorService, - NetworkResponse, -} from "~/services/interceptor.service" +import { KernelInterceptorService } from "~/services/kernel-interceptor.service" +import { RESTRequest, RESTResponse } from "~/helpers/kernel/rest" +import { RelayError } from "@hoppscotch/kernel" export type NetworkStrategy = ( - req: AxiosRequestConfig -) => TE.TaskEither - -function processResponse( - res: NetworkResponse, - req: EffectiveHoppRESTRequest, - backupTimeStart: number, - backupTimeEnd: number, - successState: HoppRESTResponse["type"] -) { - const contentLength = res.headers["content-length"] - ? parseInt(res.headers["content-length"]) - : (res.data as ArrayBuffer).byteLength - return { - type: successState, - statusCode: res.status, - statusText: res.statusText, - body: res.data, - // If multi headers are present, then we can just use that, else fallback to Axios type - headers: - res.additional?.multiHeaders ?? - Object.keys(res.headers).map((x) => ({ - key: x, - value: res.headers[x], - })), - meta: { - responseSize: contentLength, - responseDuration: backupTimeEnd - backupTimeStart, - }, - req, - } -} + req: EffectiveHoppRESTRequest +) => TE.TaskEither export function createRESTNetworkRequestStream( request: EffectiveHoppRESTRequest @@ -55,52 +22,58 @@ export function createRESTNetworkRequestStream( const req = cloneDeep(request) - const headers = req.effectiveFinalHeaders.reduce((acc, { key, value }) => { - return Object.assign(acc, { [key]: value }) - }, {}) + console.info("[helpers/network]: req", req) - const params = new URLSearchParams() - for (const param of req.effectiveFinalParams) { - params.append(param.key, param.value) - } + const execResult = RESTRequest.toRequest(req).then((kernelRequest) => { + console.info("[helpers/network]: kernelRequest", kernelRequest) - const backupTimeStart = Date.now() - - const service = getService(InterceptorService) - - const res = service.runRequest({ - method: req.method as any, - url: req.effectiveFinalURL.trim(), - headers, - params, - data: req.effectiveFinalBody, - }) - - res.response.then((res) => { - const backupTimeEnd = Date.now() - - if (E.isRight(res)) { - const processedRes = processResponse( - res.right, + if (!kernelRequest) { + response.next({ + type: "network_fail", req, - backupTimeStart, - backupTimeEnd, - "success" - ) - - response.next(processedRes) + error: new Error("Failed to create kernel request"), + }) response.complete() - return } - response.next({ - type: "network_fail", - req, - error: res.left, - }) - response.complete() + return service.execute(kernelRequest) }) - return [response, () => res.cancel()] + const service = getService(KernelInterceptorService) + + execResult.then((result) => { + if (!result) return + + result.response.then(async (res) => { + if (res._tag === "Right") { + const processedRes = await RESTResponse.toResponse(res.right, req) + + if (processedRes.type === "success") { + response.next(processedRes) + } else { + response.next({ + type: "network_fail", + req, + error: processedRes.error, + }) + } + } else { + response.next({ + type: "interceptor_error", + req, + error: res.left, + }) + } + response.complete() + }) + }) + + return [ + response, + async () => { + const result = await execResult + if (result) await result.cancel() + }, + ] } diff --git a/packages/hoppscotch-common/src/helpers/oauth.ts b/packages/hoppscotch-common/src/helpers/oauth.ts index 7c630d4b..d0daa3db 100644 --- a/packages/hoppscotch-common/src/helpers/oauth.ts +++ b/packages/hoppscotch-common/src/helpers/oauth.ts @@ -1,129 +1,17 @@ -import { getService } from "~/modules/dioc" -import { PersistenceService } from "~/services/persistence" - import * as E from "fp-ts/Either" import { z } from "zod" -import { InterceptorService } from "~/services/interceptor.service" -import { AxiosRequestConfig } from "axios" +import { getService } from "~/modules/dioc" +import { PersistenceService } from "~/services/persistence" +import { KernelInterceptorService } from "~/services/kernel-interceptor.service" +import { content } from "@hoppscotch/kernel" + +const kernelInterceptor = getService(KernelInterceptorService) +const persistenceService = getService(PersistenceService) const redirectUri = `${window.location.origin}/oauth` -const interceptorService = getService(InterceptorService) -const persistenceService = getService(PersistenceService) - -// GENERAL HELPER FUNCTIONS - -/** - * Parse a query string into an object - * - * @param {String} searchQuery - The search query params - * @returns {Object} - */ - -const parseQueryString = (searchQuery: string): Record => { - if (searchQuery === "") { - return {} - } - const segments = searchQuery.split("&").map((s) => s.split("=")) - const queryString = segments.reduce( - (obj, el) => ({ ...obj, [el[0]]: el[1] }), - {} - ) - return queryString -} - -/** - * Get OAuth configuration from OpenID Discovery endpoint - * - * @returns {Object} - */ - -const getTokenConfiguration = async (endpoint: string) => { - const options = { - method: "GET", - headers: { - "Content-type": "application/json", - }, - } - try { - const res = await runRequestThroughInterceptor({ - url: endpoint, - ...options, - }) - - if (E.isLeft(res)) { - return E.left("OIDC_DISCOVERY_FAILED") - } - - return E.right(JSON.parse(res.right)) - } catch (e) { - return E.left("OIDC_DISCOVERY_FAILED") - } -} - -// PKCE HELPER FUNCTIONS - -/** - * Generates a secure random string using the browser crypto functions - * - * @returns {Object} - */ - -const generateRandomString = () => { - const array = new Uint32Array(28) - window.crypto.getRandomValues(array) - return Array.from(array, (dec) => `0${dec.toString(16)}`.slice(-2)).join("") -} - -/** - * Calculate the SHA256 hash of the input text - * - * @returns {Promise} - */ - -const sha256 = (plain: string) => { - const encoder = new TextEncoder() - const data = encoder.encode(plain) - return window.crypto.subtle.digest("SHA-256", data) -} - -/** - * Encodes the input string into Base64 format - * - * @param {String} str - The string to be converted - * @returns {Promise} - */ - -const base64urlencode = ( - str: ArrayBuffer // Converts the ArrayBuffer to string using Uint8 array to convert to what btoa accepts. -) => { - const hashArray = Array.from(new Uint8Array(str)) - - // btoa accepts chars only within ascii 0-255 and base64 encodes them. - // Then convert the base64 encoded to base64url encoded - // (replace + with -, replace / with _, trim trailing =) - return btoa(String.fromCharCode.apply(null, hashArray)) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/, "") -} - -/** - * Return the base64-urlencoded sha256 hash for the PKCE challenge - * - * @param {String} v - The randomly generated string - * @returns {String} - */ - -const pkceChallengeFromVerifier = async (v: string) => { - const hashed = await sha256(v) - return base64urlencode(hashed) -} - -// OAUTH REQUEST - -type TokenRequestParams = { +export type TokenRequestParams = { oidcDiscoveryUrl: string grantType: string authUrl: string @@ -133,14 +21,48 @@ type TokenRequestParams = { scope: string } -/** - * Initiates PKCE Auth Code flow when requested - * - * @param {Object} - The necessary params - * @returns {Void} - */ +async function getTokenConfiguration(endpoint: string) { + const { response } = kernelInterceptor.execute({ + id: Date.now(), + url: endpoint, + method: "GET", + version: "HTTP/1.1", + headers: { + "Content-Type": ["application/json"], + }, + }) -const tokenRequest = async ({ + const result = await response + if (E.isLeft(result)) return E.left("OIDC_DISCOVERY_FAILED") + + const jsonContent = result.right.content + if (jsonContent.kind !== "json") return E.left("OIDC_DISCOVERY_FAILED") + + return E.right(jsonContent.content) +} + +const generateRandomString = () => { + const array = new Uint32Array(28) + window.crypto.getRandomValues(array) + return Array.from(array, (dec) => `0${dec.toString(16)}`.slice(-2)).join("") +} + +const base64urlencode = (str: ArrayBuffer) => { + const hashArray = Array.from(new Uint8Array(str)) + return btoa(String.fromCharCode.apply(null, hashArray)) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, "") +} + +const pkceChallengeFromVerifier = async (v: string) => { + const encoder = new TextEncoder() + const data = encoder.encode(v) + const hashed = await window.crypto.subtle.digest("SHA-256", data) + return base64urlencode(hashed) +} + +export const tokenRequest = async ({ oidcDiscoveryUrl, grantType, authUrl, @@ -157,123 +79,91 @@ const tokenRequest = async ({ token_endpoint: z.string(), }) - if (E.isLeft(res)) { - return E.left("OIDC_DISCOVERY_FAILED" as const) - } + if (E.isLeft(res)) return E.left("OIDC_DISCOVERY_FAILED") const parsedOIDCConfiguration = OIDCConfigurationSchema.safeParse(res.right) - - if (!parsedOIDCConfiguration.success) { - return E.left("OIDC_DISCOVERY_FAILED" as const) - } + if (!parsedOIDCConfiguration.success) return E.left("OIDC_DISCOVERY_FAILED") authUrl = parsedOIDCConfiguration.data.authorization_endpoint accessTokenUrl = parsedOIDCConfiguration.data.token_endpoint } - // Store oauth information - persistenceService.setLocalConfig("tokenEndpoint", accessTokenUrl) - persistenceService.setLocalConfig("client_id", clientId) - persistenceService.setLocalConfig("client_secret", clientSecret) - // Create and store a random state value + await persistenceService.setLocalConfig("tokenEndpoint", accessTokenUrl) + await persistenceService.setLocalConfig("client_id", clientId) + await persistenceService.setLocalConfig("client_secret", clientSecret) + const state = generateRandomString() - persistenceService.setLocalConfig("pkce_state", state) + await persistenceService.setLocalConfig("pkce_state", state) - // Create and store a new PKCE codeVerifier (the plaintext random secret) const codeVerifier = generateRandomString() - persistenceService.setLocalConfig("pkce_codeVerifier", codeVerifier) + await persistenceService.setLocalConfig("pkce_codeVerifier", codeVerifier) - // Hash and base64-urlencode the secret to use as the challenge const codeChallenge = await pkceChallengeFromVerifier(codeVerifier) - // Build the authorization URL - const buildUrl = () => - `${authUrl + `?response_type=${grantType}`}&client_id=${encodeURIComponent( - clientId - )}&state=${encodeURIComponent(state)}&scope=${encodeURIComponent( - scope - )}&redirect_uri=${encodeURIComponent( - redirectUri - )}&code_challenge=${encodeURIComponent( - codeChallenge - )}&code_challenge_method=S256` + const url = new URL(authUrl) + url.searchParams.set("response_type", grantType) + url.searchParams.set("client_id", clientId) + url.searchParams.set("state", state) + url.searchParams.set("scope", scope) + url.searchParams.set("redirect_uri", redirectUri) + url.searchParams.set("code_challenge", codeChallenge) + url.searchParams.set("code_challenge_method", "S256") - // Redirect to the authorization server - window.location.assign(buildUrl()) + window.location.assign(url.toString()) } -// OAUTH REDIRECT HANDLING +export const handleOAuthRedirect = async () => { + const queryParams = Object.fromEntries( + new URLSearchParams(window.location.search) + ) -/** - * Handle the redirect back from the authorization server and - * get an access token from the token endpoint - * - * @returns {Promise} - */ - -const handleOAuthRedirect = async () => { - const queryParams = parseQueryString(window.location.search.substring(1)) - - // Check if the server returned an error string - if (queryParams.error) { - return E.left("AUTH_SERVER_RETURNED_ERROR" as const) + if (queryParams.error) return E.left("AUTH_SERVER_RETURNED_ERROR") + if (!queryParams.code) return E.left("NO_AUTH_CODE") + if ( + (await persistenceService.getLocalConfig("pkce_state")) !== + queryParams.state + ) { + return E.left("INVALID_STATE") } - if (!queryParams.code) { - return E.left("NO_AUTH_CODE" as const) - } + const tokenEndpoint = await persistenceService.getLocalConfig("tokenEndpoint") + const clientID = await persistenceService.getLocalConfig("client_id") + const clientSecret = await persistenceService.getLocalConfig("client_secret") + const codeVerifier = + await persistenceService.getLocalConfig("pkce_codeVerifier") - // If the server returned an authorization code, attempt to exchange it for an access token - // Verify state matches what we set at the beginning - if (persistenceService.getLocalConfig("pkce_state") !== queryParams.state) { - return E.left("INVALID_STATE" as const) - } + if (!tokenEndpoint) return E.left("NO_TOKEN_ENDPOINT") + if (!clientID) return E.left("NO_CLIENT_ID") + if (!clientSecret) return E.left("NO_CLIENT_SECRET") + if (!codeVerifier) return E.left("NO_CODE_VERIFIER") - const tokenEndpoint = persistenceService.getLocalConfig("tokenEndpoint") - const clientID = persistenceService.getLocalConfig("client_id") - const clientSecret = persistenceService.getLocalConfig("client_secret") - const codeVerifier = persistenceService.getLocalConfig("pkce_codeVerifier") - - if (!tokenEndpoint) { - return E.left("NO_TOKEN_ENDPOINT" as const) - } - - if (!clientID) { - return E.left("NO_CLIENT_ID" as const) - } - - if (!clientSecret) { - return E.left("NO_CLIENT_SECRET" as const) - } - - if (!codeVerifier) { - return E.left("NO_CODE_VERIFIER" as const) - } - - const data = new URLSearchParams({ + const requestParams = { grant_type: "authorization_code", code: queryParams.code, client_id: clientID, client_secret: clientSecret, redirect_uri: redirectUri, code_verifier: codeVerifier, - }) + } - // Exchange the authorization code for an access token - const tokenResponse = await runRequestThroughInterceptor({ + const { response } = kernelInterceptor.execute({ + id: Date.now(), url: tokenEndpoint, - data: data.toString(), method: "POST", + version: "HTTP/1.1", headers: { - "Content-Type": "application/x-www-form-urlencoded", + "Content-Type": ["application/x-www-form-urlencoded"], }, + content: content.urlencoded(requestParams), }) - // Clean these up since we don't need them anymore clearPKCEState() - if (E.isLeft(tokenResponse)) { - return E.left("AUTH_TOKEN_REQUEST_FAILED" as const) + const result = await response + if (E.isLeft(result)) return E.left("AUTH_TOKEN_REQUEST_FAILED") + + if (result.right.content.kind !== "json") { + return E.left("AUTH_TOKEN_REQUEST_FAILED") } const withAccessTokenSchema = z.object({ @@ -281,36 +171,18 @@ const handleOAuthRedirect = async () => { }) const parsedTokenResponse = withAccessTokenSchema.safeParse( - JSON.parse(tokenResponse.right) + result.right.content.content ) return parsedTokenResponse.success ? E.right(parsedTokenResponse.data) - : E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const) + : E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE") } -const clearPKCEState = () => { - persistenceService.removeLocalConfig("pkce_state") - persistenceService.removeLocalConfig("pkce_codeVerifier") - persistenceService.removeLocalConfig("tokenEndpoint") - persistenceService.removeLocalConfig("client_id") - persistenceService.removeLocalConfig("client_secret") +const clearPKCEState = async () => { + await persistenceService.removeLocalConfig("pkce_state") + await persistenceService.removeLocalConfig("pkce_codeVerifier") + await persistenceService.removeLocalConfig("tokenEndpoint") + await persistenceService.removeLocalConfig("client_id") + await persistenceService.removeLocalConfig("client_secret") } - -async function runRequestThroughInterceptor(config: AxiosRequestConfig) { - const res = await interceptorService.runRequest(config).response - - if (E.isLeft(res)) { - return E.left("REQUEST_FAILED") - } - - // convert ArrayBuffer to string - if (!(res.right.data instanceof ArrayBuffer)) { - return E.left("REQUEST_FAILED") - } - - const data = new TextDecoder().decode(res.right.data).replace(/\0+$/, "") - return E.right(data) -} - -export { tokenRequest, handleOAuthRedirect } diff --git a/packages/hoppscotch-common/src/helpers/teams/TeamsSearch.service.ts b/packages/hoppscotch-common/src/helpers/teams/TeamsSearch.service.ts index 0f6c7e1e..e72a1a37 100644 --- a/packages/hoppscotch-common/src/helpers/teams/TeamsSearch.service.ts +++ b/packages/hoppscotch-common/src/helpers/teams/TeamsSearch.service.ts @@ -211,7 +211,12 @@ export class TeamSearchService extends Service { this.searchResultsRequests = {} this.expandedCollections.value = [] - const axiosPlatformConfig = platform.auth.axiosPlatformConfig?.() ?? {} + const getAxiosPlatformConfig = async () => { + await platform.auth.waitProbableLoginToConfirm() + return platform.auth.axiosPlatformConfig?.() ?? {} + } + + const axiosPlatformConfig = await getAxiosPlatformConfig() try { const searchResponse = await axios.get( diff --git a/packages/hoppscotch-common/src/helpers/types/HoppRESTResponse.ts b/packages/hoppscotch-common/src/helpers/types/HoppRESTResponse.ts index f5032d4d..a1036b24 100644 --- a/packages/hoppscotch-common/src/helpers/types/HoppRESTResponse.ts +++ b/packages/hoppscotch-common/src/helpers/types/HoppRESTResponse.ts @@ -1,49 +1,69 @@ import { HoppRESTRequest } from "@hoppscotch/data" import { Component } from "vue" +import { KernelInterceptorError } from "~/services/kernel-interceptor.service" export type HoppRESTResponseHeader = { key: string; value: string } +export type HoppRESTSuccessResponse = { + type: "success" + headers: HoppRESTResponseHeader[] + body: ArrayBuffer + statusCode: number + statusText: string + meta: { + responseSize: number // in bytes + responseDuration: number // in millis + } + req: HoppRESTRequest +} + +export type HoppRESTFailureResponse = { + type: "failure" + headers: HoppRESTResponseHeader[] + body: ArrayBuffer + statusCode: number + statusText: string + meta: { + responseSize: number // in bytes + responseDuration: number // in millis + } + req: HoppRESTRequest +} + +export type HoppRESTFailureNetwork = { + type: "network_fail" + error: unknown + req: HoppRESTRequest +} + +export type HoppRESTFailureScript = { + type: "script_fail" + error: Error +} + +export type HoppRESTErrorExtension = { + type: "extension_error" + error: string + component: Component + req: HoppRESTRequest +} + +export type HoppRESTErrorInterceptor = { + type: "interceptor_error" + error: KernelInterceptorError + req: HoppRESTRequest +} + +export type HoppRESTLoadingResponse = { + type: "loading" + req: HoppRESTRequest +} + export type HoppRESTResponse = - | { type: "loading"; req: HoppRESTRequest } - | { - type: "fail" - headers: HoppRESTResponseHeader[] - body: ArrayBuffer - statusCode: number - statusText: string - meta: { - responseSize: number // in bytes - responseDuration: number // in millis - } - - req: HoppRESTRequest - } - | { - type: "network_fail" - error: unknown - - req: HoppRESTRequest - } - | { - type: "script_fail" - error: Error - } - | { - type: "success" - headers: HoppRESTResponseHeader[] - body: ArrayBuffer - statusCode: number - statusText: string - meta: { - responseSize: number // in bytes - responseDuration: number // in millis - } - - req: HoppRESTRequest - } - | { - type: "extension_error" - error: string - component: Component - req: HoppRESTRequest - } + | HoppRESTLoadingResponse + | HoppRESTSuccessResponse + | HoppRESTFailureResponse + | HoppRESTFailureNetwork + | HoppRESTFailureScript + | HoppRESTFailureExtension + | HoppRESTFailureInterceptor diff --git a/packages/hoppscotch-common/src/index.ts b/packages/hoppscotch-common/src/index.ts index db618d9a..dff7b9f8 100644 --- a/packages/hoppscotch-common/src/index.ts +++ b/packages/hoppscotch-common/src/index.ts @@ -1,9 +1,7 @@ import { HOPP_MODULES } from "@modules/." import { createApp } from "vue" -import { initializeApp } from "./helpers/app" -import { initBackendGQLClient } from "./helpers/backend/GQLClient" -import { performMigrations } from "./helpers/migrations" import { PlatformDef, setPlatformDef } from "./platform" +import { initKernel, getKernelMode } from "@hoppscotch/kernel" import "../assets/scss/tailwind.scss" import "../assets/themes/themes.scss" @@ -14,27 +12,37 @@ import "unfonts.css" import App from "./App.vue" import { getService } from "./modules/dioc" -import { PersistenceService } from "./services/persistence" +import { InitializationService } from "./services/initialization.service" -export function createHoppApp(el: string | Element, platformDef: PlatformDef) { +export async function createHoppApp( + el: string | Element, + platformDef: PlatformDef +) { + initKernel(getKernelMode()) setPlatformDef(platformDef) const app = createApp(App) - // Some basic work that needs to be done before module inits even - initBackendGQLClient() - initializeApp() + // Initialize core services before app mounting + const initService = getService(InitializationService) + + await initService.initPre() + + try { + await initService.initAuthAndSync() + } catch { + console.error( + "Failed connecting to the backend, make sure the service is running and accessible on the network" + ) + } HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app)) platformDef.addedHoppModules?.forEach((mod) => mod.onVueAppInit?.(app)) - // TODO: Explore possibilities of moving this invocation to the service constructor - // `toast` was coming up as `null` in the previous attempts - getService(PersistenceService).setupLocalPersistence() - performMigrations() - app.mount(el) + await initService.initPost() + console.info( "%cWE ♥️ OPEN SOURCE", "margin:8px 0;font-family:sans-serif;font-weight:600;font-size:60px;color:violet;" diff --git a/packages/hoppscotch-common/src/kernel/index.ts b/packages/hoppscotch-common/src/kernel/index.ts new file mode 100644 index 00000000..add8a56f --- /dev/null +++ b/packages/hoppscotch-common/src/kernel/index.ts @@ -0,0 +1,13 @@ +import { KernelAPI } from "@hoppscotch/kernel" + +export { Io } from "./io" +export { Relay } from "./relay" +export { Store } from "./store" + +export const getModule = ( + name: K +): NonNullable => { + const kernel = window.__KERNEL__ + if (!kernel?.[name]) throw new Error(`Kernel ${name} not initialized`) + return kernel[name] +} diff --git a/packages/hoppscotch-common/src/kernel/io.ts b/packages/hoppscotch-common/src/kernel/io.ts new file mode 100644 index 00000000..bf55cf8d --- /dev/null +++ b/packages/hoppscotch-common/src/kernel/io.ts @@ -0,0 +1,34 @@ +import type { + SaveFileWithDialogOptions, + OpenExternalLinkOptions, + SaveFileResponse, + OpenExternalLinkResponse, + EventCallback, + UnlistenFn, +} from "@hoppscotch/kernel" +import { getModule } from "." + +export const Io = (() => { + const module = () => getModule("io") + + return { + saveFileWithDialog: ( + opts: SaveFileWithDialogOptions + ): Promise => module().saveFileWithDialog(opts), + + openExternalLink: ( + opts: OpenExternalLinkOptions + ): Promise => module().openExternalLink(opts), + + listen: ( + event: string, + handler: EventCallback + ): Promise => module().listen(event, handler), + + once: (event: string, handler: EventCallback): Promise => + module().once(event, handler), + + emit: (event: string, payload?: unknown): Promise => + module().emit(event, payload), + } as const +})() diff --git a/packages/hoppscotch-common/src/kernel/relay.ts b/packages/hoppscotch-common/src/kernel/relay.ts new file mode 100644 index 00000000..46a8d4ea --- /dev/null +++ b/packages/hoppscotch-common/src/kernel/relay.ts @@ -0,0 +1,26 @@ +import type { + RelayRequest, + RelayRequestEvents, + RelayError, + RelayResponse, + RelayEventEmitter, +} from "@hoppscotch/kernel" +import * as E from "fp-ts/Either" +import { getModule } from "." + +export const Relay = (() => { + const module = () => getModule("relay") + + return { + capabilities: () => module().capabilities, + canHandle: (request: RelayRequest): E.Either => + module().canHandle(request), + execute: ( + request: RelayRequest + ): { + cancel: () => Promise + emitter: RelayEventEmitter + response: Promise> + } => module().execute(request), + } as const +})() diff --git a/packages/hoppscotch-common/src/kernel/store.ts b/packages/hoppscotch-common/src/kernel/store.ts new file mode 100644 index 00000000..fa0756bc --- /dev/null +++ b/packages/hoppscotch-common/src/kernel/store.ts @@ -0,0 +1,46 @@ +import type { + StorageOptions, + StoreError, + StoreEvents, + StoreEventEmitter, +} from "@hoppscotch/kernel" +import * as E from "fp-ts/Either" +import { getModule } from "." + +export const Store = (() => { + const module = () => getModule("store") + + return { + capabilities: () => module().capabilities, + init: () => module().init(), + set: ( + namespace: string, + key: string, + value: unknown, + options?: StorageOptions + ): Promise> => + module().set(namespace, key, value, options), + get: ( + namespace: string, + key: string + ): Promise> => + module().get(namespace, key), + remove: ( + namespace: string, + key: string + ): Promise> => + module().remove(namespace, key), + clear: (namespace?: string): Promise> => + module().clear(namespace), + has: ( + namespace: string, + key: string + ): Promise> => module().has(namespace, key), + listNamespaces: (): Promise> => + module().listNamespaces(), + listKeys: (namespace: string): Promise> => + module().listKeys(namespace), + watch: (namespace: string, key: string): StoreEventEmitter => + module().watch(namespace, key), + } as const +})() diff --git a/packages/hoppscotch-common/src/layouts/default.vue b/packages/hoppscotch-common/src/layouts/default.vue index 8edb88a5..2959b850 100644 --- a/packages/hoppscotch-common/src/layouts/default.vue +++ b/packages/hoppscotch-common/src/layouts/default.vue @@ -118,9 +118,9 @@ onBeforeMount(() => { } }) -onMounted(() => { +onMounted(async () => { const cookiesAllowed = - persistenceService.getLocalConfig("cookiesAllowed") === "yes" + (await persistenceService.getLocalConfig("cookiesAllowed")) === "yes" const platformAllowsCookiePrompts = platform.platformFeatureFlags.promptAsUsingCookies ?? true @@ -130,8 +130,8 @@ onMounted(() => { action: [ { text: `${t("action.learn_more")}`, - onClick: (_, toastObject) => { - persistenceService.setLocalConfig("cookiesAllowed", "yes") + onClick: async (_, toastObject) => { + await persistenceService.setLocalConfig("cookiesAllowed", "yes") toastObject.goAway(0) window .open("https://docs.hoppscotch.io/support/privacy", "_blank") @@ -140,8 +140,8 @@ onMounted(() => { }, { text: `${t("action.dismiss")}`, - onClick: (_, toastObject) => { - persistenceService.setLocalConfig("cookiesAllowed", "yes") + onClick: async (_, toastObject) => { + await persistenceService.setLocalConfig("cookiesAllowed", "yes") toastObject.goAway(0) }, }, diff --git a/packages/hoppscotch-common/src/modules/i18n.ts b/packages/hoppscotch-common/src/modules/i18n.ts index 3bdde0fb..c5609643 100644 --- a/packages/hoppscotch-common/src/modules/i18n.ts +++ b/packages/hoppscotch-common/src/modules/i18n.ts @@ -71,10 +71,10 @@ let i18nInstance: I18n< true > | null = null -const resolveCurrentLocale = () => +const resolveCurrentLocale = async () => pipe( // Resolve from locale and make sure it is in languages - persistenceService.getLocalConfig("locale"), + await persistenceService.getLocalConfig("locale"), O.fromNullable, O.filter((locale) => pipe( @@ -123,7 +123,7 @@ export const changeAppLanguage = async (locale: string) => { // TODO: Look into the type issues here i18nInstance.global.locale.value = locale - persistenceService.setLocalConfig("locale", locale) + await persistenceService.setLocalConfig("locale", locale) } /** @@ -134,7 +134,7 @@ export function getI18n() { } export default { - onVueAppInit(app) { + async onVueAppInit(app) { const i18n = createI18n({ locale: "en", // TODO: i18n system! fallbackLocale: "en", @@ -153,10 +153,10 @@ export default { ) // TODO: Global loading state to hide the resolved lang loading - const currentLocale = resolveCurrentLocale() + const currentLocale = await resolveCurrentLocale() changeAppLanguage(currentLocale) - persistenceService.setLocalConfig("locale", currentLocale) + await persistenceService.setLocalConfig("locale", currentLocale) }, onBeforeRouteChange(to, _, router) { // Convert old locale path format to new format diff --git a/packages/hoppscotch-common/src/modules/index.ts b/packages/hoppscotch-common/src/modules/index.ts index b4c63f69..0c5037b8 100644 --- a/packages/hoppscotch-common/src/modules/index.ts +++ b/packages/hoppscotch-common/src/modules/index.ts @@ -4,6 +4,12 @@ import * as A from "fp-ts/Array" import { RouteLocationNormalized, Router } from "vue-router" export type HoppModule = { + /** + * Optional flag to mark a module as deprecated. + * Deprecated modules will be filtered out during initialization. + */ + deprecated?: boolean + /** * Define this function to get access to Vue App instance and augment * it (installing components, directives and plugins). Also useful for @@ -48,5 +54,6 @@ export type HoppModule = { export const HOPP_MODULES = pipe( import.meta.glob("@modules/*.ts", { eager: true }), Object.values, - A.map(({ default: defaultVal }) => defaultVal as HoppModule) + A.map(({ default: defaultVal }) => defaultVal as HoppModule), + A.filter((module) => !module.deprecated) ) diff --git a/packages/hoppscotch-common/src/modules/interceptors.ts b/packages/hoppscotch-common/src/modules/interceptors.ts index 43d71a32..28670296 100644 --- a/packages/hoppscotch-common/src/modules/interceptors.ts +++ b/packages/hoppscotch-common/src/modules/interceptors.ts @@ -7,6 +7,7 @@ import { applySetting } from "~/newstore/settings" import { useSettingStatic } from "~/composables/settings" export default { + deprecated: true, onVueAppInit() { const interceptorService = getService(InterceptorService) diff --git a/packages/hoppscotch-common/src/modules/kernel-interceptors.ts b/packages/hoppscotch-common/src/modules/kernel-interceptors.ts new file mode 100644 index 00000000..636b7c81 --- /dev/null +++ b/packages/hoppscotch-common/src/modules/kernel-interceptors.ts @@ -0,0 +1,68 @@ +import { watch } from "vue" +import { HoppModule } from "." +import { getService } from "./dioc" +import { platform } from "~/platform" +import { applySetting } from "~/newstore/settings" +import { useSettingStatic } from "~/composables/settings" +import { KernelInterceptorService } from "~/services/kernel-interceptor.service" + +export default { + onVueAppInit() { + const kernelInterceptorService = initKernelInterceptorService() + setupInterceptorSync(kernelInterceptorService) + }, +} + +function initKernelInterceptorService(): KernelInterceptorService { + const service = getService(KernelInterceptorService) + + registerInterceptors(service) + initializeDefaultInterceptor(service) + + return service +} + +function registerInterceptors(service: KernelInterceptorService): void { + platform.kernelInterceptors.interceptors.forEach((interceptorDef) => { + if (interceptorDef.type === "standalone") { + service.register(interceptorDef.interceptor) + } else { + const interceptorService = getService(interceptorDef.service) + service.register(interceptorService) + } + }) +} + +function initializeDefaultInterceptor(service: KernelInterceptorService): void { + service.setActive(platform.kernelInterceptors.default) +} + +function setupInterceptorSync(service: KernelInterceptorService): void { + syncServiceToSettings(service) + syncSettingsToService(service) +} + +function syncServiceToSettings(service: KernelInterceptorService): void { + watch( + () => service.current.value?.id, + (id) => { + applySetting( + "CURRENT_KERNEL_INTERCEPTOR_ID", + id ?? platform.kernelInterceptors.default + ) + } + ) +} + +function syncSettingsToService(service: KernelInterceptorService): void { + const [setting] = useSettingStatic("CURRENT_KERNEL_INTERCEPTOR_ID") + + watch( + setting, + () => { + const fallback = setting.value ?? platform.kernelInterceptors.default + service.setActive(fallback) + }, + { immediate: true } + ) +} diff --git a/packages/hoppscotch-common/src/newstore/MQTTSession.ts b/packages/hoppscotch-common/src/newstore/MQTTSession.ts index d328901b..4657da67 100644 --- a/packages/hoppscotch-common/src/newstore/MQTTSession.ts +++ b/packages/hoppscotch-common/src/newstore/MQTTSession.ts @@ -16,7 +16,7 @@ type MQTTTab = { type HoppMQTTRequest = { endpoint: string - clientID: string + clientID?: string } type HoppMQTTSession = { diff --git a/packages/hoppscotch-common/src/newstore/settings.ts b/packages/hoppscotch-common/src/newstore/settings.ts index 3028af6c..eeb28d03 100644 --- a/packages/hoppscotch-common/src/newstore/settings.ts +++ b/packages/hoppscotch-common/src/newstore/settings.ts @@ -55,6 +55,7 @@ export type SettingsDef = { } CURRENT_INTERCEPTOR_ID: string + CURRENT_KERNEL_INTERCEPTOR_ID: string URL_EXCLUDES: { auth: boolean @@ -117,6 +118,7 @@ export const getDefaultSettings = (): SettingsDef => { // Set empty because interceptor module will set the default value CURRENT_INTERCEPTOR_ID: "", + CURRENT_KERNEL_INTERCEPTOR_ID: "", // TODO: Interceptor related settings should move under the interceptor systems PROXY_URL: defaultProxyURL, diff --git a/packages/hoppscotch-common/src/pages/index.vue b/packages/hoppscotch-common/src/pages/index.vue index 53c949c5..fff26994 100644 --- a/packages/hoppscotch-common/src/pages/index.vue +++ b/packages/hoppscotch-common/src/pages/index.vue @@ -144,15 +144,13 @@ import { platform } from "~/platform" import { useReadonlyStream } from "~/composables/stream" import { useService } from "dioc/vue" import { InspectionService } from "~/services/inspection" -import { HeaderInspectorService } from "~/services/inspection/inspectors/header.inspector" +import { RequestInspectorService } from "~/services/inspection/inspectors/request.inspector" import { EnvironmentInspectorService } from "~/services/inspection/inspectors/environment.inspector" -import { InterceptorsInspectorService } from "~/services/inspection/inspectors/interceptors.inspector" import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector" import { cloneDeep } from "lodash-es" import { RESTTabService } from "~/services/tab/rest" import { HoppTab } from "~/services/tab" import { HoppRequestDocument, HoppTabDocument } from "~/helpers/rest/document" -import { AuthorizationInspectorService } from "~/services/inspection/inspectors/authorization.inspector" const savingRequest = ref(false) const confirmingCloseForTabID = ref(null) @@ -414,11 +412,9 @@ defineActionHandler("tab.close-other", () => { }) defineActionHandler("tab.open-new", addNewTab) -useService(HeaderInspectorService) +useService(RequestInspectorService) useService(EnvironmentInspectorService) useService(ResponseInspectorService) -useService(AuthorizationInspectorService) -useService(InterceptorsInspectorService) for (const inspectorDef of platform.additionalInspectors ?? []) { useService(inspectorDef.service) diff --git a/packages/hoppscotch-common/src/pages/oauth.vue b/packages/hoppscotch-common/src/pages/oauth.vue index 5a94bc6d..6b2f7a2e 100644 --- a/packages/hoppscotch-common/src/pages/oauth.vue +++ b/packages/hoppscotch-common/src/pages/oauth.vue @@ -69,7 +69,7 @@ function translateOAuthRedirectError(error: string) { onMounted(async () => { const localOAuthTempConfig = - persistenceService.getLocalConfig("oauth_temp_config") + await persistenceService.getLocalConfig("oauth_temp_config") if (!localOAuthTempConfig) { toast.error(t("authorization.oauth.something_went_wrong_on_oauth_redirect")) @@ -102,7 +102,7 @@ onMounted(async () => { authConfig.refresh_token = tokenInfo.right.refresh_token } - persistenceService.setLocalConfig( + await persistenceService.setLocalConfig( "oauth_temp_config", JSON.stringify(authConfig) ) diff --git a/packages/hoppscotch-common/src/pages/settings.vue b/packages/hoppscotch-common/src/pages/settings.vue index 5b88e71e..b7cbc3a1 100644 --- a/packages/hoppscotch-common/src/pages/settings.vue +++ b/packages/hoppscotch-common/src/pages/settings.vue @@ -197,6 +197,7 @@
+ + +
+
+

+ {{ t("settings.kernel_interceptor") }} +

+

+ {{ t("settings.kernel_interceptor_description") }} +

+
+
+
+

+ {{ t("settings.kernel_interceptor") }} +

+ +
+
+

+ {{ settings.title(t) }} +

+ +
+
+