Feat/ia merge (#9904)

* refactor: route fs backed cleanup (#9806)

* refactor: shared browser safe helper cleanup (#9810)

* refactor: shared browser-safe helper cleanup

* style: run eslint autofix

* fix: preserve empty url handling

* fix: address remaining copilot comments on pr3

* remove loader class

* fix: unhandledrejection error (#9774)

* fix: resolve sentry promise error (#9786)

* fix: resolve sentry promise error

* fix: leave fallback when error

* fix: improve credential validation handling in GitRepoForm to avoid a loop of re-loading the list of repos and branches (#9820)

* add e2e and cli skills (#9818)

* add e2e and cli skills

* address feedback

* address feedback

* move to claude

* feat: konnect integration proxy url and regex support (#9811)

* chore: move konnect sync behind feature flag (#9832)

* chore: isolate gRPC proto file preparation behind IPC boundary (#9828)

* chore: isolate gRPC proto file preparation behind IPC boundary

Move proto temp-file creation out of the renderer by adding a
grpc.writeProtoFile IPC handler (main process) and wiring it up
in the preload bridge. The renderer's ProtoFilesModal previously
called writeProtoFile() directly, pulling node:fs / node:os /
node:path into the renderer bundle. It now calls
window.main.grpc.writeProtoFile(protoFile._id) instead.

Changes:
- src/main/ipc/electron.ts: add 'grpc.writeProtoFile' to HandleChannels
- src/main/ipc/grpc.ts: export writeProtoFileById helper, add to
  gRPCBridgeAPI, register ipcMainHandle('grpc.writeProtoFile')
- src/entry.preload.ts: wire grpc.writeProtoFile via ipcRenderer.invoke
- src/ui/components/modals/proto-files-modal.tsx: remove direct
  write-proto-file import; use window.main.grpc.writeProtoFile in
  the directory-import validation loop
- config/renderer-node-import-baseline.json: remove 5 stale/resolved
  baseline entries (proto-directory-loader.tsx x2 already gone;
  write-proto-file.ts fs/os/path x3 now main-process-only)
- src/main/ipc/__tests__/grpc.test.ts: add writeProtoFileById unit
  tests as contract coverage for the new privileged bridge path

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: validate proto syntax in writeProtoFileById IPC handler

The directory-import validation loop relied on writeProtoFile for proto
content validation, but writeProtoFile only writes the temp file without
parsing. Add a protoLoader.load call inside writeProtoFileById so invalid
proto syntax throws before the result is returned to the renderer.

Also update the test to mock @grpc/proto-loader.load and assert it is
called with the correct file path and includeDirs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: konnect integration strips nunjucks templates on sync (#9831)

* fix(Git Sync): auto-resolve non-YAML file conflicts to remote during merge (#9798)

* fix: filter conflict paths to include only YAML files

* fix: enhance conflict resolution by auto-resolving non-YAML files to theirs

* fix: keep buffer raw so that binary files are not corrupted

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: enhance merge conflict handling by introducing auto-resolved conflicts for non-YAML files

* fix: add test for handling merge conflicts, ensuring only YAML conflicts are returned

* fix: prevent HEAD update during auto-resolve of merge conflicts

* fix: enhance merge conflict resolution by auto-completing merges when all conflicts are non-YAML

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat: konnect integration expressions support (#9830)

* Show more specific error when creating mock route fails (#9841)

* fix: insomnia-ai-plugin uses securedPath (INS-2244) (#9748)

* feat: add custom npm registry mirror setting for plugin installation (#9837)

* feat: default user-agent for cURL imports [INS-2416] (#9838)

* feat: default user-agent for cURL imports

* respect disableAppVersionUserAgent setting

* fix: view transition error - [INS-2316] (#9792)

* fix: view transition error

* fix

* change default behavior when delete cloud sync workspaces (#9844)

* feat: integrate v3 user endpoints (#9785)

* feat: integrate v3 user endpoints

* feat: use public sdk for insomnia-api

* chore: applied PoLP to workflows (#9840)

* chore: resolve GHA warning annotations and reduce CI time [INS-2312] (#9839)

* fix: resolves INS-2366 (#9852)

* fix: resolves INS-2366 dependency issues

* Refactor:use electron store for oauth session (#9851)

* move oauth session to electron storage

* create electron storage bridge

* use electronStorage bridge for managing oauth window handles

* fix build

* move key to constants

* tolerate changing userData folder path

* Update packages/insomnia/src/main/ipc/electron-storage.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* init store

* fix singleton class

* feedback

* feedback

* Update packages/insomnia/src/main/electron-storage.ts

Co-authored-by: aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>

* chore: decouple releases (#9842)

* INS-2145 Decouple releases

* fix security error

* fix

* check version

* refactor: auth header to main (#9834)

* remove deprecated baseUrl

* add failing test

* fix AI playwright runs

* move getAuthHeader to main

* address feedback about dynamic import

* move oauth 1 + 2 flow to main

* handle bad cookie

* handle bad apikey

* fix imports

* block main process imports

* extract plugins

* fix vite config

* console log

* move init store

* Fix OAuth imports after rebase

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* clean up

* Revert config changes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* clean up hawk

* use bridge

* update node require

* remove this

* define process type

* remove 14

* ignore reports folder

* fix e2e tests

* address feedback

* remove unused

* tidy constants

* feat: add getOAuth2Token IPC bridge

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix tests

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: Support pin and unpin websocket and socketio requests (#9865)

* support pin websocket and socketio requests

* feat(Git Sync): Add support for canonical repository output (#9789)

* initial support for canonical repo output (#9739)

* Feat/git repo output sync queue (#9790)

* feat: implement SyncQueue for serial async task processing

* refactor: enhance repo file watcher for improved sync and error handling

- Replace NeDB client with a unified disk client for all file operations.
- Introduce a serial queue to manage sync tasks and prevent race conditions.
- Implement content-hash deduplication to avoid unnecessary file imports.
- Add problem tracking for YAML files with conflicts or parse errors.
- Streamline watcher start/stop logic and improve notification handling.
- Ensure immediate DB→FS flush before git operations to maintain consistency.
- Enhance import logic to handle workspace deletions and renames effectively.

* refactor: simplify projectRoutableFSClient by removing unused parameters and consolidating logic

* feat: add git.db-synced event listener for revalidation in Root component

* feat: add button to open local repository folder in ProjectSettingsForm

* refactor: remove unused GitProjectNeDBClient

* refactor: update imports to use services for workspace and workspaceMeta

* refactor: update models usage to services in git repo migration and project settings form

* refactor: streamline file watcher initialization and import process

* feat: ensure immediate processing of pending debounced imports in RepoFileWatcher

* refactor: improve file rename handling in RepoFileWatcher to prevent data loss

* feat: enhance RepoFileWatcher to track last written hash and sync mtime for improved file management

* refactor: remove unused parameters from upsertDocs in RepoFileWatcher for cleaner code

* fix revalidator (#9826)

* fix: handle detached HEAD during rebase in getCurrentBranch method (#9843)

* fix: handle detached HEAD during rebase in getCurrentBranch method

* fix: add return type to getCurrentBranch method

* fix: refresh ui after sync (#9848)

* fix: (git cli)skip flush problematic files (#9846)

* fix: skip flush problematic files

* fix

* feat: (git cli)ux for invalide status (#9836)

* feat: ux for invalide status

* update ux

* fix

* fix

* add tab warning

* del log

* feat(Git Sync): Handle non-origin remotes (#9833)

* feat(git): detect non-origin branch tracking and guard sync operations

- Add getBranchTrackingRemote(), getRemoteUrl(), getBranchRemoteInfo() to GitVCS
- Add getBranchRemoteInfo IPC endpoint with BranchRemoteInfo interface
- Add assertBranchOnOrigin() guard to push, pull, fetch, commitAndPush
- canPushLoader returns { canPush: false } for non-origin branches
- Add unit tests for remote detection methods

* feat(git): add support for non-origin branch tracking and display warnings in UI

* Show local git repo path [INS-2315] (#9858)

* Update the style of local git folder path in project setting modal

* Add Git CLI tip in commit changes modal

* Repo Migration flow [INS-2256] (#9824)

* initial support for canonical repo output (#9739)

* feat: enhance git repository migration with concurrency guard and symlink handling

* feat: enhance git repository migration with config sanitization and file overwrite handling

* feat: implement repo migration version tracking and improve migration idempotency

* feat: add runAllGitRepoMigrations function and migration view for Git projects

Co-authored-by: Copilot <copilot@github.com>

* fix: reset initial migration status to 'default' in MigrationView component

* refactor: simplify MigrationView component and update navigation logic

* refactor: remove legacy directory structure migration from loadGitRepository function

* feat: enhance runAllGitRepoMigrations to return logs and improve error handling in MigrationView

* feat: update runAllGitRepoMigrations to return detailed logs and failed projects; enhance MigrationView to handle migration results

* feat: optimize runAllGitRepoMigrations by batch-fetching git repositories and improving project filtering

* feat: introduce CURRENT_MIGRATION_VERSION constant for migration tracking and update references in git-repo-migration and router

* feat: handle failed projects in runAllGitRepoMigrations by converting them to local projects

Co-authored-by: Copilot <copilot@github.com>

* feat: integrate CURRENT_MIGRATION_VERSION for migration tracking and update router logic to handle migration screen visibility

* feat: reorder import statements in ProjectSettingsForm for consistency

* feat: update MigrationStatus type and related logic for better error handling

* feat: enhance migration logging with detailed error stack and include CURRENT_MIGRATION_VERSION in logs

* feat: simplify migration logging messages for clarity and consistency

* feat: improve migration check logic to prioritize version stamp over disk layout

* feat: add tests for migrateRepoStructureIfNeeded function to ensure migration logic correctness

* feat: update migration logic to re-run when old git/ directory exists, ensuring correct migration handling

* test: update migration tests to ensure directory existence checks are accurate

* refactor: remove redundant useEffect for localStorage in Component

* feat: enhance path validation in runAllGitRepoMigrations to prevent path traversal vulnerabilities

* feat: enhance path handling in migration functions to prevent directory traversal vulnerabilities

* feat: enhance directory traversal protection in moveDirectoryContents function

---------

Co-authored-by: James Gatz <jamesgatzos@gmail.com>
Co-authored-by: Copilot <copilot@github.com>

* fix: Delete old folders (#9867)

* refactor: remove unused migration version handling from localStorage

* fix: update directory removal logic to handle non-empty directories

---------

Co-authored-by: Curry Yang <163384738+CurryYangxx@users.noreply.github.com>
Co-authored-by: yaoweiprc <6896642+yaoweiprc@users.noreply.github.com>
Co-authored-by: Pavlos Koutoglou <pkoutoglou@gmail.com>
Co-authored-by: Copilot <copilot@github.com>

* refactor: move sync code to main (#9827)

* move sync code to main

* improve sync tests

* update plan

* test: reset cloud sync smoke state

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* guard against bad test env

* fix skill

* remove new test

* udpate plan

* with proxy

* checkpoint

* move files

* autofix

* update plan

* make all sync bridge async

* fix window imports

* refactor: move main-only sync helpers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix lint

* move chunkArray tests

* smaller interfaces

* move store under vcs

* move cloud-sync to main

* create a second vcs for pull operations

* added a invoke wrapper to remove error prefixes

* rebase error

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: update insomnia-plugin-ai (#9862)

* fix: bump node-libcurl and add ipv6 tests (#9869)

* feat: revamping pre/post scripting sandbox (#9794)

* feat: revamping pre/post scripting sandbox
* feat: added UI setting to enable/disable specific checks

* fix false positives

* revert

* fix: user can not resolve conflict in app (#9872)

* fix: conflict ux

* fix

* fix

* Git server for smoke test [INS-2258] (#9816)

* Git server for smoke test

* Try to solve flaky test

* feat: remove unused Git hook samples and add Credentials tab functionality

- Deleted various sample Git hook scripts from the git-server fixture, including post-update, pre-applypatch, pre-commit, pre-merge-commit, pre-push, pre-rebase, pre-receive, prepare-commit-msg, push-to-checkout, sendemail-validate, and update hooks.
- Introduced a new PreferencesCredentialsTab class to manage Git credentials within the Insomnia Preferences.
- Updated the PreferencesPage to include the new Credentials tab for Git credentials management.
- Enhanced the ProjectPage with a method to create a Git Sync project, including branch creation and switching.
- Added comprehensive tests for Git Sync functionality, including creating branches, committing changes, and merging branches.
- Updated UI components to support new features, including data-testid attributes for better testability.

Co-authored-by: Copilot <copilot@github.com>

* feat: update path import and add Git sync tests

* revert package.json

* Update package.json

* feat: add new dependencies for Git HTTP mock server and related utilities

* refactor: remove commented-out code in addAccessTokenGitCredential function

* fix: update export tests to use toHaveLength for file count assertions

---------

Co-authored-by: Copilot <copilot@github.com>

* feat: import deep-link login experience [INS-2416] (#9860)

* refactor: replace node:url with URL in cert and proxy match (#9515)

* refactor: import to main (#9809)

* squash

* re add comments

* fix process fork

* update base line 18 left

* revert

* check cert url without node

* fix handlerId

* exclude url matches cert host from scope

* fix rebase

* Fix style issue that file list in the middle of commit modal is collapsed [INS-2315] (#9875)

* Fix style issue that file list in the middle of commit modal is collapsed

* fix: update links to Git Sync documentation in staging modal and project settings form

* Chore: playwright dx v2 (#9876)

* Update E2E test for git sync [INS-2258] (#9878)

* Add more test cases for git sync

* tmp

* Update package.json

* feat: update migration image and urls (#9868)

* feat: update migration image and path for improved clarity

* feat: update error message and support links in migration view

* feat(Git Sync): Downgrade -> Upgrade path (#9882)

* feat: add mechanism to flush newer DB workspaces to disk during downgrade

* feat: implement effective Git repository ID handling for project connections

* feat: enhance Git repository ID handling for improved project queries and updates

* Chore: refine e2e docs by agent for agent (#9881)

* improve agent docs

* added error context note

* chore: Security update for dependencies and github workflows (#9884)

* chore: resolves INS-2457, INS-2458, INS-2459, and INS-2460.

* feat: include app version in window title [INS-2465] (#9888)

* feat(Git Sync): improve git migration onboarding UX and local file system access [INS-2462] (#9890)

* feat(Migration): enhance migration summary with total projects count and improve UI feedback

* fix(Migration): clarify update instructions and improve user messaging

* style(ManualCommitForm): adjust text sizes for improved readability

* style(StagingModal): adjust layout and spacing for improved UI consistency

* fix(ManualCommitForm): update clipboard text to include 'cd' command for easier navigation

* fix(ProjectSettingsForm): update repository path copy functionality and add option to open in file system

* fix(ManualCommitForm): enhance file system interaction with tooltips for better user guidance

* fix(ProjectSettingsForm): add tooltip for 'Open in file system' button to enhance user guidance

* fix(GitProjectSyncDropdown): add 'Open folder' action to sync dropdown for easier access to repository path

* fix(Component): display relative path of current issue in modal for better context

* fix(git-service): count only successfully migrated projects in totalProjects

* fix(project-settings-form): platform-aware shell quoting for cd command

* fix(git-project-staging-modal): platform-aware shell quoting for cd command

* fix(project-settings-form): update aria-label to reflect cd command clipboard content

* fix(git-project-staging-modal): update aria-label to reflect shell command clipboard content

* fix(ManualCommitForm): replace tooltip with dialog for enhanced information display

* fix(MigrationView): update migrated count calculation to reflect total projects

---------

Co-authored-by: James Gatz <jamesgatzos@gmail.com>

* chore: normalize konnect api responses (#9895)

* feat(Git Sync): enhance migration view with best practices note and UI improvements (#9900)

* chore: comment out smctl credentials save in workflow (#9898)

the command is no longer executed while keeping it in place for future reference.

* feat: migrate model imports, base types, org model and helpers (#9802)

* fix

* fix vcsinstance

* fix type issues

---------

Co-authored-by: Jack Kavanagh <jackkav@gmail.com>
Co-authored-by: James Gatz <jamesgatzos@gmail.com>
Co-authored-by: Shelby <13246465+shelby-moore@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: yaoweiprc <6896642+yaoweiprc@users.noreply.github.com>
Co-authored-by: kwburns-kong <kyle.burns@konghq.com>
Co-authored-by: jeremyjpj0916 <31913027+jeremyjpj0916@users.noreply.github.com>
Co-authored-by: Ryan Willis <ryan.willis@konghq.com>
Co-authored-by: Kent Wang <kent.wang@konghq.com>
Co-authored-by: Alison Sabuwala <alison.sabuwala1024@gmail.com>
Co-authored-by: aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
Co-authored-by: Jay Wu <jay.wu@konghq.com>
Co-authored-by: Pavlos Koutoglou <pkoutoglou@gmail.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Fares Osman <43153226+fiosman@users.noreply.github.com>
Co-authored-by: Bingbing <ZxBing0066@gmail.com>
Co-authored-by: Vivek Thuravupala <2700229+godfrzero@users.noreply.github.com>
This commit is contained in:
Curry Yang
2026-05-08 13:46:28 +08:00
committed by GitHub
parent f0a45d231f
commit d524dfb9a3
460 changed files with 12658 additions and 4215 deletions

View File

@@ -0,0 +1,58 @@
---
name: fix-test-cli-ci
description: 'Debug failures from the test-cli.yml workflow locally. Use when insomnia-inso bundle tests fail in CI, especially node-vs-electron dependency/runtime mismatches.'
argument-hint: 'Provide the failing test-cli.yml logs, a link to the failing workflow run, or the failing test name from npm run test:bundle -w insomnia-inso'
---
# Fix test-cli.yml CI Failures
## When to Use
- `.github/workflows/test-cli.yml` or the `Test CLI` workflow failed in CI.
- `inso` bundle tests fail locally or in GitHub Actions.
- You suspect node module/runtime differences between Node.js and Electron.
## Procedure
1. Start with the failing CI evidence.
- Use the provided logs or workflow run link to identify whether the failure happened during the `test:bundle` run, the packaged binary run, or the build/package setup before the tests.
- Note the failing test name and the first actionable error message before reproducing locally.
2. Ensure Node.js native dependencies are installed (not Electron-targeted variants):
```bash
npm run install-libcurl-node
```
3. Build `insomnia-inso` to generate `dist`:
```bash
npm run build -w insomnia-inso
```
4. Start the smoke-test echo server used by the CLI tests:
```bash
npm run serve -w insomnia-smoke-test
```
5. In another terminal, run the bundled CLI test suite:
```bash
npm run test:bundle -w insomnia-inso
```
6. If the CI failure was in the packaged binary path or the bundle suite passes locally, run:
```bash
npm run test:binary -w insomnia-inso
```
## Notes
- Keep the smoke-test server running while bundle tests execute.
- Common failure patterns:
- Errors involving `@getinsomnia/node-libcurl`, `NODE_MODULE_VERSION`, `dlopen`, or native module loading usually mean the Node-targeted libcurl binary needs to be reinstalled with `npm run install-libcurl-node`.
- Connection failures to `http://localhost:4010` usually mean the smoke-test server is not running.
- Missing `dist` output or missing `binaries/inso` usually means `npm run build -w insomnia-inso` or the package step needs to be rerun.
- Success criteria:
- `npm run test:bundle -w insomnia-inso` exits successfully and Vitest reports the `inso dev bundle` tests as passing.
- If validating the packaged binary path, `npm run test:binary -w insomnia-inso` also exits successfully.
## Teardown
- Stop the `npm run serve -w insomnia-smoke-test` process once validation is complete.
- Reinstall the electron-targeted libcurl binary for continued Electron development:
```bash
npm run install-libcurl-electron
```

16
.codegraph/.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed
# Database
*.db
*.db-wal
*.db-shm
# Cache
cache/
# Logs
*.log
# Hook markers
.dirty

View File

@@ -7,12 +7,15 @@ on:
workflow_dispatch:
permissions: {}
jobs:
update_homebrew:
timeout-minutes: 5
name: Update Insomnia Formula
# must be macos, linux-brew doesn't have casks
runs-on: macos-latest
permissions: {}
steps:
- name: Set up Homebrew
id: set-up-homebrew
@@ -22,7 +25,7 @@ jobs:
- name: Cache Homebrew Bundler RubyGems
id: cache
uses: actions/cache@v3
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ steps.set-up-homebrew.outputs.gems-path }}
key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}

View File

@@ -6,6 +6,8 @@ on:
- 'release/**'
workflow_dispatch:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -15,11 +17,10 @@ jobs:
generate-sbom-and-upload-assets:
runs-on: ubuntu-24.04
permissions:
packages: write
contents: write # publish sbom to GH releases/tag assets
contents: read # Required for actions/checkout
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Perform SCA / SBOM analysis for the entire monorepo code repository
# Produces SCA(SBOM and CVE) report
@@ -35,6 +36,9 @@ jobs:
build-and-upload-release-artifacts:
timeout-minutes: 45
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: read
env:
INSO_PACKAGE_NAME: insomnia-inso
INSO_DOCKER_TAR: inso-docker-image.tar
@@ -58,10 +62,10 @@ jobs:
# csc_key_password_secret: ''
steps:
- name: Checkout branch
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -124,7 +128,7 @@ jobs:
# smctl will be used in the next step for signing
- name: Setup Software Trust Manager
if: runner.os == 'Windows'
uses: digicert/code-signing-software-trust-action@9b30180369343eb1ce0dcbebb933cfa3e17b6cc8 # v1
uses: digicert/code-signing-software-trust-action@9b30180369343eb1ce0dcbebb933cfa3e17b6cc8 # v1.0.0
with:
simple-signing-mode: true
env:
@@ -137,7 +141,7 @@ jobs:
if: runner.os == 'Windows'
shell: bash
run: |
smctl credentials save ${SM_API_KEY} ${SM_CLIENT_CERT_PASSWORD}
# smctl credentials save ${SM_API_KEY} ${SM_CLIENT_CERT_PASSWORD}
NODE_OPTIONS='--max_old_space_size=6144' npm run package:windows:unpacked -w insomnia
env:
SM_HOST: ${{ vars.DIGICERT_SM_HOST }}
@@ -164,7 +168,7 @@ jobs:
# this installs smctl as well
- name: Code-sign unpacked .exe (Windows only)
if: runner.os == 'Windows'
uses: digicert/code-signing-software-trust-action@9b30180369343eb1ce0dcbebb933cfa3e17b6cc8 # v1
uses: digicert/code-signing-software-trust-action@9b30180369343eb1ce0dcbebb933cfa3e17b6cc8 # v1.0.0
with:
simple-signing-mode: true
# If the below 2 parameters are supplied, then smctl executable is invoked to attempt the signing.
@@ -242,7 +246,7 @@ jobs:
- name: Code-sign inso exe (Windows only)
if: runner.os == 'Windows'
uses: digicert/code-signing-software-trust-action@9b30180369343eb1ce0dcbebb933cfa3e17b6cc8 # v1
uses: digicert/code-signing-software-trust-action@9b30180369343eb1ce0dcbebb933cfa3e17b6cc8 # v1.0.0
with:
simple-signing-mode: true
# If the below 2 parameters are supplied, then smctl executable is invoked to attempt the signing.
@@ -273,7 +277,7 @@ jobs:
- name: Notarize Inso CLI installer (macOS only)
if: runner.os == 'macOS'
uses: lando/notarize-action@b5c3ef16cf2fbcf2af26dc58c90255ec242abeed # v2
uses: lando/notarize-action@b5c3ef16cf2fbcf2af26dc58c90255ec242abeed # v2.0.2
with:
product-path: ./packages/${{ env.INSO_PACKAGE_NAME }}/artifacts/inso-${{ matrix.os }}-${{ env.INSO_VERSION }}.pkg
primary-bundle-id: com.insomnia.inso
@@ -290,7 +294,7 @@ jobs:
- name: Notarize Inso CLI binary (macOS only)
if: runner.os == 'macOS'
uses: lando/notarize-action@b5c3ef16cf2fbcf2af26dc58c90255ec242abeed # v2
uses: lando/notarize-action@b5c3ef16cf2fbcf2af26dc58c90255ec242abeed # v2.0.2
with:
product-path: ./packages/${{ env.INSO_PACKAGE_NAME }}/binaries/inso
primary-bundle-id: com.insomnia.inso-binary
@@ -303,7 +307,7 @@ jobs:
- name: Login to Docker Hub
if: runner.os == 'Linux' && runner.arch == 'X64'
uses: docker/login-action@3d58c274f17dffee475a5520cbe67f0a882c4dbb # v2.1.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_REGISTRY_USER }}
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
@@ -329,7 +333,7 @@ jobs:
SYFT_SOURCE_NAME: ${{ env.INSO_DOCKER_TAR }}
- name: Upload artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
if-no-files-found: ignore
name: ${{ runner.os }}-${{ runner.arch }}-artifacts
@@ -347,7 +351,7 @@ jobs:
packages/insomnia-inso/artifacts/*
- name: Upload source assets for Sentry
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ runner.os }}-${{ runner.arch }}-sentry
path: |
@@ -359,6 +363,8 @@ jobs:
timeout-minutes: ${{ fromJSON(vars.GHA_DEFAULT_TIMEOUT) }}
needs: build-and-upload-release-artifacts
runs-on: ubuntu-24.04
permissions:
pull-requests: write
steps:
- name: Get release version
id: release_version

View File

@@ -18,6 +18,8 @@ env:
INSO_DOCKER_IMAGE: &INSO_DOCKER_IMAGE 'kong/inso' # By default, registry is docker.io
NOTARY_REPOSITORY: &NOTARY_REPOSITORY 'kong/notary' # All signatures will be pushed to public notary repository
permissions: {}
jobs:
publish:
timeout-minutes: 30
@@ -35,15 +37,24 @@ jobs:
contents: write # Required to upload assets. Issue: https://github.com/slsa-framework/slsa-github-generator/tree/main/internal/builders/container#known-issues
packages: write
steps:
- name: Calculate Release Branch
env:
VERSION: ${{ github.event.inputs.version }}
run: |
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
# Rewrite the release branch to follow our new flow
echo "RELEASE_BRANCH=release/$MAJOR_MINOR" >> $GITHUB_ENV
- name: Checkout branch # Check out the release branch
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ env.RELEASE_BRANCH }}
fetch-depth: 0
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -57,8 +68,18 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- name: Compare input version with package.json version
env:
VERSION: ${{ github.event.inputs.version }}
run: |
PKG_VERSION=$(jq .version packages/insomnia-inso/package.json -rj)
if [ "$PKG_VERSION" != "$VERSION" ]; then
echo "Input version $VERSION does not match package.json version $PKG_VERSION"
exit 1
fi
- name: Download all artifacts from release-build.yml
uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e # v2
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
with:
github_token: ${{secrets.GITHUB_TOKEN}}
workflow: release-build.yml
@@ -82,13 +103,13 @@ jobs:
ELECTRON_ARTIFACT_BASE64_FILE: ${{runner.temp}}/electron_digests_file.text
- name: Calculate CLI Binary base64 file handle
uses: slsa-framework/slsa-github-generator/actions/generator/generic/create-base64-subjects-from-file@5a775b367a56d5bd118a224a811bba288150a563 # v2.0.0
uses: slsa-framework/slsa-github-generator/actions/generator/generic/create-base64-subjects-from-file@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0
id: cli_binary_hashes
with:
path: ${{ env.CLI_ARTIFACT_BASE64_FILE }}
- name: Calculate Electron Binary base64 file handle
uses: slsa-framework/slsa-github-generator/actions/generator/generic/create-base64-subjects-from-file@5a775b367a56d5bd118a224a811bba288150a563 # v2.0.0
uses: slsa-framework/slsa-github-generator/actions/generator/generic/create-base64-subjects-from-file@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0
id: electron_binary_hashes
with:
path: ${{ env.ELECTRON_ARTIFACT_BASE64_FILE }}
@@ -137,7 +158,7 @@ jobs:
docker image ls
- name: Login to Docker Hub
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.1.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_REGISTRY_USER }}
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
@@ -259,21 +280,6 @@ jobs:
--package-type insomnia
${{ env.IS_PRERELEASE == 'true' && '--internal' || '--publish' }}
- name: Configure Git user
uses: Homebrew/actions/git-user-config@266845213695c3047d210b2e8fbc42ecdaf45802 # master
with:
username: ${{ (github.event_name == 'workflow_dispatch' && github.actor) || 'insomnia-infra' }}
- name: Merge git branch into develop
run: |
remote_repo="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git checkout develop
git merge --no-ff ${{ env.RELEASE_BRANCH }}
git status
git push "${remote_repo}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
artifact-provenance:
needs: [publish]
permissions:
@@ -290,7 +296,7 @@ jobs:
- product: inso
binary_artifacts_subject_as_file: ${{ needs.publish.outputs.INSO_BINARY_ARTIFACTS_SUBJECTS_AS_FILE }}
# need to use non hash version because of: https://github.com/slsa-framework/slsa-github-generator/issues/3498
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects-as-file: '${{ matrix.binary_artifacts_subject_as_file }}'
upload-assets: true
@@ -306,7 +312,7 @@ jobs:
packages: write # Required for publishing provenance. Issue: https://github.com/slsa-framework/slsa-github-generator/tree/main/internal/builders/container#known-issues
# need to use non hash version because of: https://github.com/slsa-framework/slsa-github-generator/issues/3498
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: *INSO_DOCKER_IMAGE
digest: ${{ needs.publish.outputs.INSO_DOCKER_IMAGE_DIGEST }}

View File

@@ -15,6 +15,9 @@ concurrency:
cancel-in-progress: true
env:
PR_NUMBER: ${{ github.event.number }}
permissions: {}
jobs:
build-and-upload-artifacts:
timeout-minutes: 45
@@ -23,6 +26,7 @@ jobs:
if: ${{ !startsWith(github.head_ref, 'release/') }}
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: read
strategy:
fail-fast: false
@@ -38,10 +42,10 @@ jobs:
build-targets: tar.gz
steps:
- name: Checkout branch
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -64,6 +68,7 @@ jobs:
run: npm --workspaces version prerelease --preid="alpha-pr-$(git rev-parse --short HEAD)" --no-git-tag-version
- name: Package
if: ${{ matrix.os != 'windows-latest' }}
shell: bash
run: NODE_OPTIONS='--max_old_space_size=6144' BUILD_TARGETS='${{ matrix.build-targets }}' npm run app-package
@@ -83,7 +88,7 @@ jobs:
INSOMNIA_UPDATES_URL: http://localhost:4010
- name: Upload smoke test traces
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
if-no-files-found: ignore
@@ -91,7 +96,7 @@ jobs:
path: packages/insomnia-smoke-test/traces
- name: Upload artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
if-no-files-found: ignore
name: ${{ matrix.os }}-artifacts-${{ github.run_number }}

View File

@@ -16,20 +16,24 @@ on:
required: false
description: force version of the release (e.g. 9.0.0) if previous release was successful, this should auto increment
permissions: {}
jobs:
setup-release-branch:
timeout-minutes: 5
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Checkout branch
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: develop
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -49,23 +53,32 @@ jobs:
- name: App version (stable, with a specific version)
if: github.event.inputs.channel == 'stable' && github.event.inputs.version
run: npm --workspaces version "${{ github.event.inputs.version }}"
env:
VERSION: ${{ github.event.inputs.version }}
run: npm --workspaces version "$VERSION"
- name: App version (alpha/beta, patch latest alpha/beta)
if: github.event.inputs.channel != 'stable' && !github.event.inputs.version
run: npm --workspaces version --preid "${{ github.event.inputs.channel }}" prerelease
env:
CHANNEL: ${{ github.event.inputs.channel }}
run: npm --workspaces version --preid "$CHANNEL" prerelease
- name: App version (alpha/beta, with a specific version)
if: github.event.inputs.channel != 'stable' && github.event.inputs.version
run: npm --workspaces version "${{ github.event.inputs.version }}"
env:
VERSION: ${{ github.event.inputs.version }}
run: npm --workspaces version "$VERSION"
# ############################################################
- name: Get version
shell: bash
run: |
echo "RELEASE_VERSION=$(node -e "console.log(require('./packages/insomnia/package.json').version)")" >> $GITHUB_ENV
echo "RELEASE_BRANCH=release/$(node -e "console.log(require('./packages/insomnia/package.json').version)")" >> $GITHUB_ENV
VERSION=$(node -p "require('./packages/insomnia/package.json').version")
MAJOR_MINOR=$(echo $VERSION | cut -d. -f1,2)
echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV
echo "RELEASE_BRANCH=release/$MAJOR_MINOR" >> $GITHUB_ENV
- name: Create Branch # Create a branch if it doesn't exist
uses: peterjgrainger/action-create-branch@c2800a3a9edbba2218da6861fa46496cf8f3195a # v2.2.0
@@ -75,7 +88,7 @@ jobs:
branch: ${{ env.RELEASE_BRANCH }}
- name: Checkout branch
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ env.RELEASE_BRANCH }}
persist-credentials: false
@@ -95,15 +108,21 @@ jobs:
- name: (Re-run) App version (stable, with a specific version)
if: github.event.inputs.channel == 'stable' && github.event.inputs.version
run: npm --workspaces version "${{ github.event.inputs.version }}"
env:
VERSION: ${{ github.event.inputs.version }}
run: npm --workspaces version "$VERSION"
- name: (Re-run) App version (alpha/beta, patch latest alpha/beta)
if: github.event.inputs.channel != 'stable' && !github.event.inputs.version
run: npm --workspaces version --preid "${{ github.event.inputs.channel }}" prerelease
env:
CHANNEL: ${{ github.event.inputs.channel }}
run: npm --workspaces version --preid "$CHANNEL" prerelease
- name: (Re-run) App version (alpha/beta, with a specific version)
if: github.event.inputs.channel != 'stable' && github.event.inputs.version
run: npm --workspaces version "${{ github.event.inputs.version }}"
env:
VERSION: ${{ github.event.inputs.version }}
run: npm --workspaces version "$VERSION"
# ############################################################

View File

@@ -8,20 +8,18 @@ on:
- release/*
workflow_dispatch: {}
permissions: {}
jobs:
semgrep:
timeout-minutes: 5
name: Semgrep SAST
runs-on: ubuntu-24.04
permissions:
# required for all workflows
security-events: write
# only required for workflows in private repositories
actions: read
contents: read
if: (github.actor != 'dependabot[bot]')
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: Kong/public-shared-actions/security-actions/semgrep@a18abf762d6e2444bcbfd20de70451ea1e3bc1b1 # 4.0.1

View File

@@ -15,20 +15,21 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: read
permissions: {}
jobs:
Test:
timeout-minutes: 10
runs-on: ubuntu-24.04
permissions:
contents: read
packages: read
steps:
- name: Checkout branch
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -82,7 +83,7 @@ jobs:
VERSION: ${{ steps.inso-variables.outputs.inso-version }}
- name: Upload Inso CLI artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
if-no-files-found: ignore
name: ${{ steps.inso-variables.outputs.pkg-name }}

View File

@@ -15,20 +15,21 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: read
permissions: {}
jobs:
Test:
timeout-minutes: 40
build:
timeout-minutes: 20
runs-on: ubuntu-24.04
permissions:
contents: read
packages: read
steps:
- name: Checkout branch
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: npm
@@ -45,13 +46,57 @@ jobs:
- name: Build app for smoke tests
run: NODE_OPTIONS='--max_old_space_size=6144' npm run app-build
- name: Smoke test electron app
run: npm run test:build -w packages/insomnia-smoke-test -- --project=Smoke
- name: Upload build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: app-build
path: packages/insomnia/build/
retention-days: 1
- name: Upload smoke test
uses: actions/upload-artifact@v7
test:
needs: build
timeout-minutes: 25
runs-on: ubuntu-24.04
permissions:
contents: read
packages: read
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6]
shardTotal: [6]
steps:
- name: Checkout branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: package-lock.json
registry-url: 'https://npm.pkg.github.com'
scope: '@kong'
- name: Install packages
run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- name: Download build artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: app-build
path: packages/insomnia/build/
- name: Smoke test electron app (shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
run: npm run test:build -w packages/insomnia-smoke-test -- --project=Smoke --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload smoke test traces
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ !cancelled() }}
with:
if-no-files-found: ignore
name: ubuntu-smoke-test-traces-${{ github.run_number }}
name: ubuntu-smoke-test-traces-${{ github.run_number }}-shard-${{ matrix.shardIndex }}
path: packages/insomnia-smoke-test/traces

View File

@@ -15,6 +15,8 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
Test:
timeout-minutes: 20
@@ -25,10 +27,10 @@ jobs:
packages: read
steps:
- name: Checkout branch
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: .nvmrc
cache: npm
@@ -56,14 +58,14 @@ jobs:
- name: Checkout base branch (cycle comparison)
if: github.event_name == 'pull_request' && always()
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.ref }}
path: insomnia-base
- name: Setup Node (base branch tree)
if: github.event_name == 'pull_request' && always()
uses: actions/setup-node@v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: insomnia-base/.nvmrc
cache: npm
@@ -81,7 +83,7 @@ jobs:
- name: Check Circular References
if: github.event_name == 'pull_request' && always()
uses: actions/github-script@v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@@ -5,6 +5,8 @@ on:
release:
types: [released]
permissions: {}
jobs:
update:
runs-on: ubuntu-24.04
@@ -17,7 +19,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.release.target_commitish }}

1
.gitignore vendored
View File

@@ -22,6 +22,7 @@ node_modules/
.yarn-integrity
.env
.idea
.reports
*.iml
.DS_Store
*test-plugins

View File

@@ -43,5 +43,6 @@
],
"[cpp]": {
"editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd"
}
},
"editor.formatOnPaste": true
}

View File

@@ -65,6 +65,9 @@ Organization
- **HTTP Calls:** Use `insomniaFetch()` for Insomnia backend APIs. Use plain `fetch()` for external/third-party APIs.
- **Styling:** Tailwind utility classes only. Use `clsx`/`tailwind-merge` for conditionals. Use React Aria for interactive HTML elements.
- **Testing:** Use **Vitest** (unit) and **Playwright** (E2E). Co-locate unit tests as `filename.test.ts`. Use `vi.mock()`. Prefer testing logic via loaders over mounting components.
- **E2E tests** live in `packages/insomnia-smoke-test/`. Full docs: [`packages/insomnia-smoke-test/README.md`](packages/insomnia-smoke-test/README.md).
- Run E2E from repo root: `npm run test:smoke:dev` (filter: `npm run test:smoke:dev -- <title-substring>`).
- New test imports: `import { test } from '../../playwright/test'` and `import { expect } from '@playwright/test'`.
## Sensitive Data
- **Vault system (AES-GCM):** For environment secrets (`EnvironmentKvPairDataType.SECRET`).

View File

@@ -61,7 +61,7 @@ There are a few notable directories inside it:
- `/src/ui` React components and styling.
- `/src/common` Utilities used across both main and render processes.
- `/src/plugins` Logic around installation and usage of plugins.
- `/src/models` DB models used to store user data.
- `/src/insomnia-data` Data models, services and database for managing application state.
- `/src/network` Sending requests and performing auth (e.g. OAuth 2).
- `/src/templating` Nunjucks and rendering related code.
- `/src/sync` and `/src/account` Team sync and account stuff.

View File

@@ -12,6 +12,23 @@ import globals from 'globals';
import tseslint from 'typescript-eslint';
const rendererBuiltinSpecifiers = [...builtinModules, ...builtinModules.map(moduleName => `node:${moduleName}`)];
const generalRestrictedImportPatterns = [
// Shouldn't import packages by relative path
{
group: ['**/*/insomnia-api/**'],
message: "Please use 'insomnia-api' instead of relative paths",
},
// Block relative paths to insomnia-data
{
group: ['./**/insomnia-data', './**/insomnia-data/**', '../**/insomnia-data', '../**/insomnia-data/**'],
message: "Please use '~/insomnia-data' instead of relative paths",
},
// Only allow ~/insomnia-data and ~/insomnia-data/node
{
regex: '^~/insomnia-data/(?!node($|/)).+',
message: "Only '~/insomnia-data' and '~/insomnia-data/node' are allowed",
},
];
const rendererNodeMigrationOffenders = [
'packages/insomnia/src/common/misc.ts',
'packages/insomnia/src/common/significant-diff-detection.ts',
@@ -86,23 +103,6 @@ export default defineConfig([
'playwright/no-wait-for-timeout': 'error',
},
},
// nodeIntegration: false section
{
files: [
'packages/insomnia/src/ui/**/*.{ts,tsx}',
'packages/insomnia/src/routes/**/*.{ts,tsx}',
'packages/insomnia/src/common/**/*.{ts,tsx}',
],
ignores: rendererNodeRestrictionIgnores,
rules: {
'no-restricted-imports': [
'error',
{
paths: rendererBuiltinSpecifiers,
},
],
},
},
// React hooks section
{
files: ['packages/insomnia/src/**/*.{ts,tsx}'],
@@ -168,23 +168,25 @@ export default defineConfig([
'no-restricted-imports': [
'error',
{
patterns: [
// Shouldn't import packages by relative path
{
group: ['**/*/insomnia-api/**'],
message: "Please use 'insomnia-api' instead of relative paths",
},
// Block relative paths to insomnia-data
{
group: ['./**/insomnia-data', './**/insomnia-data/**', '../**/insomnia-data', '../**/insomnia-data/**'],
message: "Please use '~/insomnia-data' instead of relative paths",
},
// Only allow ~/insomnia-data and ~/insomnia-data/node
{
regex: '^~/insomnia-data/(?!node($|/)).+',
message: "Only '~/insomnia-data' and '~/insomnia-data/node' are allowed",
},
],
patterns: generalRestrictedImportPatterns,
},
],
},
},
// nodeIntegration: false section
{
files: [
'packages/insomnia/src/ui/**/*.{ts,tsx}',
'packages/insomnia/src/routes/**/*.{ts,tsx}',
'packages/insomnia/src/common/**/*.{ts,tsx}',
],
ignores: rendererNodeRestrictionIgnores,
rules: {
'no-restricted-imports': [
'error',
{
paths: rendererBuiltinSpecifiers,
patterns: generalRestrictedImportPatterns,
},
],
},

313
package-lock.json generated
View File

@@ -18,7 +18,7 @@
"packages/insomnia-scripting-environment"
],
"dependencies": {
"@getinsomnia/node-libcurl": "3.2.1",
"@getinsomnia/node-libcurl": "3.2.2",
"ajv": "^8.17.1"
},
"devDependencies": {
@@ -3527,10 +3527,16 @@
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/@getinsomnia/insomnia-v3-fetch": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@getinsomnia/insomnia-v3-fetch/-/insomnia-v3-fetch-1.0.1.tgz",
"integrity": "sha512-v/0lZ6Fz700xLd+YgqqsAi50HTuZ6l/klc2z8G4v7PgWAtEG2O4tIUY/9yn1WtdH21SQtgBz9kWxIxTPUGtzUQ==",
"license": "Apache-2.0"
},
"node_modules/@getinsomnia/node-libcurl": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@getinsomnia/node-libcurl/-/node-libcurl-3.2.1.tgz",
"integrity": "sha512-QaB81JOAvAxkGUNeXIBrERuUhONf+d46f7ZTyPg6ayc3q48NyAhQUznNcbajynbCJjXRsJBqb0qAY8pK2ShNGQ==",
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@getinsomnia/node-libcurl/-/node-libcurl-3.2.2.tgz",
"integrity": "sha512-O4FTODtfwvLpnk+pyPHCIINCmu8Io+UaDDmcY2bU73VumAu6p/p/egQ6ndUOOId6OQ7V2jyAppZFgNb+fev/Uw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -3572,9 +3578,9 @@
}
},
"node_modules/@google/genai": {
"version": "1.48.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.48.0.tgz",
"integrity": "sha512-plonYK4ML2PrxsRD9SeqmFt76eREWkQdPCglOA6aYDzL1AAbE+7PUnT54SvpWGfws13L0AZEqGSpL7+1IPnTxQ==",
"version": "1.50.1",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.50.1.tgz",
"integrity": "sha512-YbkX7H9+1Pt8wOt7DDREy8XSoiL6fRDzZQRyaVBarFf8MR3zHGqVdvM4cLbDXqPhxqvegZShgfxb8kw9C7YhAQ==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
@@ -4028,20 +4034,55 @@
}
},
"node_modules/@kong/insomnia-plugin-ai": {
"version": "1.0.9",
"resolved": "https://npm.pkg.github.com/download/@kong/insomnia-plugin-ai/1.0.9/1819b2d915bf1dcfc74211c2bb08005bc18813e6",
"integrity": "sha512-9dj+9IsgJlW9fKC3XRAVEI2165jA+m11Gbyx6Z7cbqXne+ksupGrcmU4HnndkRKMCoIlNH10f2VGVgN8n8vhWA==",
"version": "1.0.11",
"resolved": "https://npm.pkg.github.com/download/@kong/insomnia-plugin-ai/1.0.11/787a5587e7bcbcea66430dcec379253644ef00c7",
"integrity": "sha512-qF1+PD950aqDA5l3W7N4VFN3BAeLTzOs62+Ig6dLcP6uv4SNjnTgcRZ3ubRhScfPT+V5hB0qIUbG0We3fFEGvg==",
"optional": true,
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@google/genai": "^1.38.0",
"@apidevtools/swagger-parser": "^12.1.0",
"@google/genai": "^1.49.0",
"js-yaml": "^4.1.1",
"node-llama-cpp": "^3.15.0",
"openai": "^6.17.0",
"node-llama-cpp": "^3.18.1",
"openai": "^6.34.0",
"zod": "^4.3.6"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "^4.55.3"
"@rollup/rollup-linux-x64-gnu": "^4.60.1"
}
},
"node_modules/@kong/insomnia-plugin-ai/node_modules/@apidevtools/json-schema-ref-parser": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz",
"integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==",
"license": "MIT",
"optional": true,
"dependencies": {
"@types/json-schema": "^7.0.15",
"js-yaml": "^4.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/philsturgeon"
}
},
"node_modules/@kong/insomnia-plugin-ai/node_modules/@apidevtools/swagger-parser": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz",
"integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==",
"license": "MIT",
"optional": true,
"dependencies": {
"@apidevtools/json-schema-ref-parser": "14.0.1",
"@apidevtools/openapi-schemas": "^2.1.0",
"@apidevtools/swagger-methods": "^3.0.2",
"ajv": "^8.17.1",
"ajv-draft-04": "^1.0.0",
"call-me-maybe": "^1.0.2"
},
"peerDependencies": {
"openapi-types": ">=7"
}
},
"node_modules/@kong/insomnia-plugin-external-vault": {
@@ -5125,13 +5166,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz",
"integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==",
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.55.1"
"playwright": "1.59.1"
},
"bin": {
"playwright": "cli.js"
@@ -5165,9 +5206,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
"integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
@@ -5193,9 +5234,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz",
"integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
@@ -5211,9 +5252,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
"integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
"license": "BSD-3-Clause"
},
"node_modules/@ravanallc/grpc-server-reflection": {
@@ -9040,9 +9081,9 @@
}
},
"node_modules/@stoplight/spectral-core": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.21.0.tgz",
"integrity": "sha512-oj4e/FrDLUhBRocIW+lRMKlJ/q/rDZw61HkLbTFsdMd+f/FTkli2xHNB1YC6n1mrMKjjvy7XlUuFkC7XxtgbWw==",
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.22.0.tgz",
"integrity": "sha512-4hTxMDs4TFUG4/jKjaZttA65gNuV2PCKI9+51I+J4nL6ylo17DlbW+sl6byKnBuV/85HxaV33ri5fEGlp8lTSA==",
"license": "Apache-2.0",
"dependencies": {
"@stoplight/better-ajv-errors": "1.0.3",
@@ -9054,17 +9095,17 @@
"@stoplight/types": "~13.6.0",
"@types/es-aggregate-error": "^1.0.2",
"@types/json-schema": "^7.0.11",
"ajv": "^8.17.1",
"ajv": "^8.18.0",
"ajv-errors": "~3.0.0",
"ajv-formats": "~2.1.1",
"es-aggregate-error": "^1.0.7",
"expr-eval-fork": "^3.0.1",
"jsonpath-plus": "^10.3.0",
"lodash": "~4.17.23",
"lodash": "^4.18.1",
"lodash.topath": "^4.5.2",
"minimatch": "3.1.2",
"minimatch": "^3.1.4",
"nimma": "0.2.3",
"pony-cause": "^1.1.1",
"simple-eval": "1.0.1",
"tslib": "^2.8.1"
},
"engines": {
@@ -9095,25 +9136,19 @@
"license": "MIT"
},
"node_modules/@stoplight/spectral-core/node_modules/brace-expansion": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@stoplight/spectral-core/node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/@stoplight/spectral-core/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -9150,9 +9185,9 @@
"license": "0BSD"
},
"node_modules/@stoplight/spectral-functions": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.1.tgz",
"integrity": "sha512-obu8ZfoHxELOapfGsCJixKZXZcffjg+lSoNuttpmUFuDzVLT3VmH8QkPXfOGOL5Pz80BR35ClNAToDkdnYIURg==",
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.2.tgz",
"integrity": "sha512-PIfPUgTRo8EtAnL1MIrzhHoUuojSaE8shGSMaHS3BxGyc8d079BE5+TqJa1/WLUb9YT9JQnZ0Aj4xfi8NcJOIw==",
"license": "Apache-2.0",
"dependencies": {
"@stoplight/better-ajv-errors": "1.0.3",
@@ -9160,11 +9195,11 @@
"@stoplight/spectral-core": "^1.19.4",
"@stoplight/spectral-formats": "^1.8.1",
"@stoplight/spectral-runtime": "^1.1.2",
"ajv": "^8.17.1",
"ajv": "^8.18.0",
"ajv-draft-04": "~1.0.0",
"ajv-errors": "~3.0.0",
"ajv-formats": "~2.1.1",
"lodash": "~4.17.21",
"lodash": "^4.18.1",
"tslib": "^2.8.1"
},
"engines": {
@@ -9188,12 +9223,6 @@
}
}
},
"node_modules/@stoplight/spectral-functions/node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/@stoplight/spectral-functions/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -9257,9 +9286,9 @@
"license": "0BSD"
},
"node_modules/@stoplight/spectral-ruleset-bundler": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@stoplight/spectral-ruleset-bundler/-/spectral-ruleset-bundler-1.6.3.tgz",
"integrity": "sha512-AQFRO6OCKg8SZJUupnr3+OzI1LrMieDTEUHsYgmaRpNiDRPvzImE3bzM1KyQg99q58kTQyZ8kpr7sG8Lp94RRA==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@stoplight/spectral-ruleset-bundler/-/spectral-ruleset-bundler-1.7.0.tgz",
"integrity": "sha512-PpIdj5Wje0T7ktxY8EUzBWLU0+mGGQHznT8nlQxTMnRhWLNYsm6HvSZDXLtMi+86yqvTuf7loJy6JvLBDzHGAA==",
"license": "Apache-2.0",
"dependencies": {
"@rollup/plugin-commonjs": "~22.0.2",
@@ -9275,7 +9304,7 @@
"@stoplight/types": "^13.6.0",
"@types/node": "*",
"pony-cause": "1.1.1",
"rollup": "~2.79.2",
"rollup": "~2.80.0",
"tslib": "^2.8.1",
"validate-npm-package-name": "3.0.0"
},
@@ -9342,9 +9371,9 @@
"license": "0BSD"
},
"node_modules/@stoplight/spectral-rulesets": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/@stoplight/spectral-rulesets/-/spectral-rulesets-1.22.0.tgz",
"integrity": "sha512-l2EY2jiKKLsvnPfGy+pXC0LeGsbJzcQP5G/AojHgf+cwN//VYxW1Wvv4WKFx/CLmLxc42mJYF2juwWofjWYNIQ==",
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/@stoplight/spectral-rulesets/-/spectral-rulesets-1.22.1.tgz",
"integrity": "sha512-DaaQJioKuYkRsOuKIJfX2ek7G7f6OCU3CI3K7ABaOcTFMiHj29SJLDdb04mCjXZFXMlXHjmCl2ZpKW6heieXpw==",
"license": "Apache-2.0",
"dependencies": {
"@asyncapi/specs": "^6.8.0",
@@ -9356,11 +9385,11 @@
"@stoplight/spectral-runtime": "^1.1.2",
"@stoplight/types": "^13.6.0",
"@types/json-schema": "^7.0.7",
"ajv": "^8.17.1",
"ajv": "^8.18.0",
"ajv-formats": "~2.1.1",
"json-schema-traverse": "^1.0.0",
"leven": "3.1.0",
"lodash": "~4.17.21",
"lodash": "^4.18.1",
"tslib": "^2.8.1"
},
"engines": {
@@ -9384,12 +9413,6 @@
}
}
},
"node_modules/@stoplight/spectral-rulesets/node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/@stoplight/spectral-rulesets/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -11419,6 +11442,18 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -14618,9 +14653,9 @@
}
},
"node_modules/dompurify": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
"integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz",
"integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -16296,6 +16331,15 @@
"integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
"license": "Apache-2.0"
},
"node_modules/expr-eval-fork": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/expr-eval-fork/-/expr-eval-fork-3.0.3.tgz",
"integrity": "sha512-BhC+hbc5lIVjygr840n5DEkW3MQq7H9o+mc1/N7Z5uIiCFVyESLL5DIE7LNq4CYUNxy+XjA+3jRrL/h0Kt2xcg==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/express": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
@@ -16651,6 +16695,12 @@
"node": "^12.20 || >= 14.13"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -17316,6 +17366,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/git-http-backend": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/git-http-backend/-/git-http-backend-1.1.2.tgz",
"integrity": "sha512-Gx7n/kyCEXGFZlCGmbsEsyeyabLs8XWeb+E/6842up7p3PktQS2/8rlNfB6hCagnW0pJ13Tn8E3yhOkKS6ihdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"git-side-band-message": "~0.0.3",
"inherits": "~2.0.1"
}
},
"node_modules/git-side-band-message": {
"version": "0.0.3",
"resolved": "https://registry.npmmirror.com/git-side-band-message/-/git-side-band-message-0.0.3.tgz",
"integrity": "sha512-4Rq4xm1+zqCkmuHxRbGdA5ActF7F4UfgK8uI0B7ZfSkByZfikRuF7mqHlvqmycvqos7jpXNkgsZK7DThLLHG3w==",
"dev": true,
"license": "BSD"
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -19581,12 +19649,6 @@
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"license": "MIT"
},
"node_modules/jshint/node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/jshint/node_modules/minimatch": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
@@ -22865,9 +22927,9 @@
}
},
"node_modules/openai": {
"version": "6.33.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.33.0.tgz",
"integrity": "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw==",
"version": "6.34.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.34.0.tgz",
"integrity": "sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw==",
"license": "Apache-2.0",
"optional": true,
"bin": {
@@ -23444,13 +23506,13 @@
}
},
"node_modules/playwright": {
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
"integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.55.1"
"playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
@@ -23463,9 +23525,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.55.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
"integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -23927,22 +23989,22 @@
}
},
"node_modules/protobufjs": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz",
"integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/codegen": "^2.0.5",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/inquire": "^1.1.1",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@protobufjs/utf8": "^1.1.1",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
@@ -24871,9 +24933,9 @@
}
},
"node_modules/rollup": {
"version": "2.79.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"version": "2.80.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz",
"integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
"license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
@@ -25581,18 +25643,6 @@
],
"license": "MIT"
},
"node_modules/simple-eval": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz",
"integrity": "sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==",
"license": "MIT",
"dependencies": {
"jsep": "^1.3.6"
},
"engines": {
"node": ">=12"
}
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
@@ -27422,9 +27472,9 @@
"license": "MIT"
},
"node_modules/undici": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
"integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==",
"version": "7.25.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
"integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -29122,7 +29172,7 @@
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^3.0.2",
"@getinsomnia/node-libcurl": "3.2.1",
"@getinsomnia/node-libcurl": "3.2.2",
"@getinsomnia/srp-js": "1.0.0-alpha.1",
"@grpc/grpc-js": "^1.13.3",
"@grpc/proto-loader": "^0.7.13",
@@ -29135,13 +29185,15 @@
"@seald-io/nedb": "^4.1.1",
"@segment/analytics-node": "2.2.1",
"@sentry/electron": "^6.5.0",
"@stoplight/spectral-core": "^1.20.0",
"@stoplight/spectral-core": "^1.22.0",
"@stoplight/spectral-formats": "^1.8.2",
"@stoplight/spectral-ruleset-bundler": "1.6.3",
"@stoplight/spectral-rulesets": "^1.22.0",
"@stoplight/spectral-ruleset-bundler": "1.7.0",
"@stoplight/spectral-rulesets": "^1.22.1",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-virtual": "3.13.12",
"@xmldom/xmldom": "^0.9.8",
"acorn": "^8.16.0",
"acorn-walk": "^8.3.5",
"ajv": "^8.17.1",
"apiconnect-wsdl": "2.0.36",
"aws4": "^1.13.2",
@@ -29161,10 +29213,11 @@
"decompress": "^4.2.1",
"deep-equal": "2.2.3",
"diff-match-patch-ts": "^0.6.0",
"dompurify": "^3.2.5",
"dompurify": "^3.4.1",
"electron-context-menu": "^3.6.1",
"electron-updater": "^6.6.2",
"fastq": "^1.19.1",
"fflate": "^0.8.2",
"fuzzysort": "^1.9.0",
"graphql": "^16.10.0",
"graphql-ws": "^5.16.2",
@@ -29213,7 +29266,7 @@
"tinykeys": "^3.0.0",
"tough-cookie": "^4.1.4",
"tweetnacl": "^1.0.3",
"undici": "^7.16.0",
"undici": "^7.25.0",
"uuid": "^9.0.1",
"vkbeautify": "^0.99.3",
"ws": "^8.18.1",
@@ -29279,13 +29332,16 @@
"vite": "^7.1.3"
},
"optionalDependencies": {
"@kong/insomnia-plugin-ai": "^1.0.9",
"@kong/insomnia-plugin-ai": "^1.0.11",
"@kong/insomnia-plugin-external-vault": "0.1.4-dev.20251224090833"
}
},
"packages/insomnia-api": {
"version": "12.5.1-alpha.0",
"license": "Apache-2.0"
"license": "Apache-2.0",
"dependencies": {
"@getinsomnia/insomnia-v3-fetch": "^1.0.1"
}
},
"packages/insomnia-inso": {
"version": "12.5.1-alpha.0",
@@ -29293,10 +29349,10 @@
"dependencies": {
"@seald-io/nedb": "^4.1.1",
"@segment/analytics-node": "^2.2.1",
"@stoplight/spectral-core": "^1.20.0",
"@stoplight/spectral-core": "^1.22.0",
"@stoplight/spectral-formats": "^1.8.2",
"@stoplight/spectral-ruleset-bundler": "1.6.3",
"@stoplight/spectral-rulesets": "^1.22.0",
"@stoplight/spectral-ruleset-bundler": "1.7.0",
"@stoplight/spectral-rulesets": "^1.22.1",
"@stoplight/types": "^14.1.1",
"commander": "^12.1.0",
"consola": "^2.15.3",
@@ -29360,7 +29416,7 @@
"version": "12.5.1-alpha.0",
"license": "Apache-2.0",
"devDependencies": {
"@playwright/test": "1.55.1",
"@playwright/test": "1.59.1",
"@ravanallc/grpc-server-reflection": "^0.1.6",
"@types/express": "^4.17.21",
"@types/oidc-provider": "^8.8.1",
@@ -29370,6 +29426,7 @@
"esbuild-runner": "2.2.2",
"express": "^4.21.2",
"express-basic-auth": "^1.2.1",
"git-http-backend": "^1.1.2",
"graphql": "^16.10.0",
"graphql-http": "^1.22.4",
"http-errors": "^2.0.0",

View File

@@ -34,8 +34,8 @@
"install-libcurl-node": "node-pre-gyp install --directory node_modules/@getinsomnia/node-libcurl --update-binary --runtime=node --target=24.14.0",
"inso-start": "npm start -w insomnia-inso",
"inso-package": "npm run build -w insomnia-inso && npm run package -w insomnia-inso",
"watch:app": "cross-env PLAYWRIGHT=1 npm run build:electron-entrypoints -w insomnia && npm run start:dev-server -w insomnia",
"app-build": "cross-env PLAYWRIGHT=1 npm run build -w insomnia",
"watch:app": "npm run build:electron-entrypoints -w insomnia && npm run start:dev-server -w insomnia",
"app-build": "npm run build -w insomnia",
"app-package": "npm run package -w insomnia",
"test:smoke:dev": "npm run test:dev -w insomnia-smoke-test -- --project=Smoke",
"test:smoke:build": "npm run test:build -w insomnia-smoke-test -- --project=Smoke",
@@ -76,10 +76,13 @@
"overrides": {
"ajv-draft-04": {
"ajv": "$ajv"
},
"jshint": {
"lodash": "4.18.1"
}
},
"dependencies": {
"@getinsomnia/node-libcurl": "3.2.1",
"@getinsomnia/node-libcurl": "3.2.2",
"ajv": "^8.17.1"
}
}

View File

@@ -11,5 +11,5 @@ Uses npm workspace, so no need to install.
### Import
```ts
import { getUserProfile, type UserProfileResponse } from 'insomnia-api';
import { getUserProfile, getEncryptionKeys, type User, type UserEncryptionKeys } from 'insomnia-api';
```

View File

@@ -22,7 +22,10 @@
},
"scripts": {
"lint": "eslint . --ext .ts,.tsx --cache",
"type-check": "tsc --noEmit --project tsconfig.json"
"type-check": "tsc --noEmit --project tsconfig.json",
"test": "vitest run"
},
"dependencies": {}
"dependencies": {
"@getinsomnia/insomnia-v3-fetch": "^1.0.1"
}
}

View File

@@ -0,0 +1,115 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getEncryptionKeys, getUserProfile } from '../user';
const { mockFetch } = vi.hoisted(() => ({
mockFetch: vi.fn(),
}));
vi.mock('../fetch', () => ({
fetch: mockFetch,
}));
describe('getUserProfile', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('returns first_name and last_name from the API response', async () => {
mockFetch.mockResolvedValue({
id: 'usr_abc123',
email: 'jane@example.com',
first_name: 'Jane',
last_name: 'Doe',
picture: 'https://example.com/pic.jpg',
});
const result = await getUserProfile({ sessionId: 'sess_xyz' });
expect(result.first_name).toBe('Jane');
expect(result.last_name).toBe('Doe');
});
it('passes id through as-is', async () => {
mockFetch.mockResolvedValue({
id: 'usr_abc123',
email: 'jane@example.com',
first_name: 'Jane',
last_name: 'Doe',
picture: '',
});
const result = await getUserProfile({ sessionId: 'sess_xyz' });
expect(result.id).toBe('usr_abc123');
});
it('passes picture through as-is', async () => {
mockFetch.mockResolvedValue({
id: 'usr_abc123',
email: 'jane@example.com',
first_name: 'Jane',
last_name: 'Doe',
picture: 'https://example.com/pic.jpg',
});
const result = await getUserProfile({ sessionId: 'sess_xyz' });
expect(result.picture).toBe('https://example.com/pic.jpg');
});
it('calls fetch with the correct path and sessionId', async () => {
mockFetch.mockResolvedValue({
id: 'usr_abc123',
email: 'jane@example.com',
first_name: 'Jane',
last_name: 'Doe',
picture: '',
});
await getUserProfile({ sessionId: 'sess_xyz' });
expect(mockFetch).toHaveBeenCalledWith({ method: 'GET', path: '/v3/users/me', sessionId: 'sess_xyz' });
});
});
describe('getEncryptionKeys', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('passes encryption key fields through as-is', async () => {
mockFetch.mockResolvedValue({
public_key: '{"kty":"RSA"}',
enc_private_key: '{"iv":"abc"}',
enc_symmetric_key: '{"iv":"def"}',
salt_enc: 'deadbeef',
enc_driver_key: null,
});
const result = await getEncryptionKeys({ sessionId: 'sess_xyz' });
expect(result.public_key).toBe('{"kty":"RSA"}');
expect(result.enc_private_key).toBe('{"iv":"abc"}');
expect(result.enc_symmetric_key).toBe('{"iv":"def"}');
expect(result.salt_enc).toBe('deadbeef');
});
it('calls fetch with the correct path and sessionId', async () => {
mockFetch.mockResolvedValue({
public_key: '',
enc_private_key: '',
enc_symmetric_key: '',
salt_enc: '',
enc_driver_key: '',
});
await getEncryptionKeys({ sessionId: 'sess_xyz' });
expect(mockFetch).toHaveBeenCalledWith({
method: 'GET',
path: '/v3/users/me/encryption-keys',
sessionId: 'sess_xyz',
});
});
});

View File

@@ -1,5 +1,9 @@
import type { User, UserEncryptionKeys } from '@getinsomnia/insomnia-v3-fetch';
import { fetch } from './fetch';
export type { User, UserEncryptionKeys };
// POST /auth/logout
export const logout = ({ sessionId }: { sessionId: string }) => {
return fetch({
@@ -9,66 +13,14 @@ export const logout = ({ sessionId }: { sessionId: string }) => {
});
};
// GET /auth/whoami
interface WhoamiResponse {
sessionAge: number;
sessionExpiry: number;
accountId: string;
email: string;
firstName: string;
lastName: string;
created: number;
publicKey: string;
encSymmetricKey: string;
encPrivateKey: string;
saltEnc: string;
isPaymentRequired: boolean;
isTrialing: boolean;
isVerified: boolean;
isAdmin: boolean;
trialEnd: string;
planName: string;
planId: string;
canManageTeams: boolean;
maxTeamMembers: number;
}
export const whoami = async ({ sessionId }: { sessionId: string }): Promise<WhoamiResponse> => {
const response = await fetch<WhoamiResponse>({
method: 'GET',
path: '/auth/whoami',
sessionId,
});
if (typeof response === 'string') {
throw new TypeError('Unexpected plaintext response: ' + response);
}
if (response && !response?.encSymmetricKey) {
throw new Error('Unexpected response: ' + JSON.stringify(response));
}
return response;
// GET /v3/users/me
export const getUserProfile = async ({ sessionId }: { sessionId: string }): Promise<User> => {
return await fetch<User>({ method: 'GET', path: '/v3/users/me', sessionId });
};
// GET /v1/user/profile
export interface UserProfile {
id: string;
email: string;
name: string;
picture: string;
bio: string;
github: string;
linkedin: string;
twitter: string;
identities: any;
given_name: string;
family_name: string;
}
export const getUserProfile = async ({ sessionId }: { sessionId: string }) => {
return fetch<UserProfile>({
method: 'GET',
path: '/v1/user/profile',
sessionId,
});
// GET /v3/users/me/encryption-keys
export const getEncryptionKeys = async ({ sessionId }: { sessionId: string }): Promise<UserEncryptionKeys> => {
return fetch<UserEncryptionKeys>({ method: 'GET', path: '/v3/users/me/encryption-keys', sessionId });
};
// GET /v1/billing/current-plan

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
},
});

View File

@@ -53,10 +53,10 @@
"dependencies": {
"@segment/analytics-node": "^2.2.1",
"@seald-io/nedb": "^4.1.1",
"@stoplight/spectral-core": "^1.20.0",
"@stoplight/spectral-core": "^1.22.0",
"@stoplight/spectral-formats": "^1.8.2",
"@stoplight/spectral-ruleset-bundler": "1.6.3",
"@stoplight/spectral-rulesets": "^1.22.0",
"@stoplight/spectral-ruleset-bundler": "1.7.0",
"@stoplight/spectral-rulesets": "^1.22.1",
"@stoplight/types": "^14.1.1",
"commander": "^12.1.0",
"consola": "^2.15.3",

View File

@@ -42,6 +42,8 @@ const shouldReturnSuccessCode = [
'$PWD/packages/insomnia-inso/bin/inso run test -w packages/insomnia-inso/src/examples/folder-inheritance-document.yml spc_a8144e --verbose --disableCertValidation',
// run collection
// with auth
'$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-smoke-test/fixtures/auth-types.yaml wrk_ca4cb9',
// export file
'$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-smoke-test/fixtures/simple.yaml -e production wrk_dc393c',
// with regex filter

View File

@@ -1,4 +1,4 @@
import type { OAuth1SignatureMethod } from 'insomnia/src/network/o-auth-1/constants';
import type { OAuth1SignatureMethod } from 'insomnia/src/common/constants';
import type { OAuth2ResponseType, RequestAuthentication } from '~/insomnia-data';

View File

@@ -1,9 +1,10 @@
import { Ajv, type ErrorObject } from 'ajv';
import * as chai from 'chai';
import { RESPONSE_CODE_REASONS } from 'insomnia/src/common/constants';
import { readCurlResponse } from 'insomnia/src/models/helpers/response-operations';
import type { sendCurlAndWriteTimelineError, sendCurlAndWriteTimelineResponse } from 'insomnia/src/network/network';
import { services } from '~/insomnia-data';
import { Cookie, type CookieOptions } from './cookies';
import { CookieList } from './cookies';
import { Header, type HeaderDefinition, HeaderList } from './headers';
@@ -394,8 +395,7 @@ export async function readBodyFromPath(
} else if (!response.bodyPath) {
return '';
}
const nodejsReadCurlResponse = process.type === 'renderer' ? window.bridge.readCurlResponse : readCurlResponse;
const readResponseResult = await nodejsReadCurlResponse({
const readResponseResult = await services.helpers.readCurlResponse({
bodyPath: response.bodyPath,
bodyCompression: response.bodyCompression,
});

View File

@@ -1,9 +1,9 @@
import type { CurlRequestOutput } from 'insomnia/src/main/network/libcurl-promise';
import { readCurlResponse } from 'insomnia/src/models/helpers/response-operations';
import { Cookie } from 'tough-cookie';
import { v4 as uuidv4 } from 'uuid';
import type { Settings } from '~/insomnia-data';
import { services } from '~/insomnia-data';
import { RequestAuth } from './auth';
import { fromPreRequestAuth } from './auth';
@@ -264,8 +264,7 @@ async function curlOutputToResponse(
originalRequest,
});
}
const nodejsReadCurlResponse = process.type === 'renderer' ? window.bridge.readCurlResponse : readCurlResponse;
const bodyResult = await nodejsReadCurlResponse({
const bodyResult = await services.helpers.readCurlResponse({
bodyPath: result.responseBodyPath,
bodyCompression: result.patch.bodyCompression,
});

View File

@@ -1,144 +1,98 @@
# Insomnia Smoke Tests
[![Playwright](https://img.shields.io/badge/playwright-blue.svg?style=for-the-badge&logo=playwright)](https://github.com/microsoft/playwright)
Playwright E2E tests for the Insomnia desktop app.
This project contains the smoke testing suite for Insomnia App.
> CLI smoke tests: [CLI.md](CLI.md)
> To find more about Inso CLI smoke tests, check [this document](CLI.md).
## Test structure
- [Insomnia Smoke Tests](#insomnia-smoke-tests)
- [Quick-start](#quick-start)
- [Debugging and Developing Tests locally](#debugging-and-developing-tests-locally)
- [Playwright VS Code extension](#playwright-vs-code-extension)
- [Playwright Inspector](#playwright-inspector)
- [Playwright Trace viewer](#playwright-trace-viewer)
- [Additional Log levels](#additional-log-levels)
- [Reproducing CI Failures](#reproducing-ci-failures)
- [Getting traces from CI](#getting-traces-from-ci)
- [Build and package methods](#build-and-package-methods)
- [Non recurring tests](#non-recurring-tests)
- [Refresh certs](#refresh-certs)
```
tests/
smoke/ # Main suite — runs on every CI push (Ubuntu only)
critical/ # Single critical-path test — runs on release
migration/ # Data migration tests
```
All commands below must be run from the **repo root**.
## Quick-start
Prerequisites:
- Clone the project
- Run `npm install`
To run all tests:
- In one terminal run: `npm run watch:app` OR `npm run dev`
- In another terminal run: `npm run test:smoke:dev`
To run single tests:
- Filter by the file or test title, e.g. `npm run test:smoke:dev -- oauth`
- Use playwright UI `npm run test:dev -w insomnia-smoke-test -- --project=Smoke --ui` to show all tests by project
## Debugging and Developing Tests locally
### Playwright VS Code extension
In order to run/debug tests directly from VS Code:
- Install the [Playwright extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright).
- With the extension installed, run on terminal `npm run watch:app`.
You can trigger tests from the `Testing` tab, or within the test files clicking the run button.
![editor](docs/imgs/editor.png)
If no tests appear, you may need to run "Refresh playwright tests". This can be done from the command palette, or by using the button at the top of the `Testing` tab.
![refresh](docs/imgs/refresh.png)
### Playwright Inspector
You can step through tests with playwright inspector: `PWDEBUG=1 npm run test:smoke:dev`
This is also useful to help create new tests.
![playwright inspector](docs/imgs/playwright-inspector.jpg)
### Playwright Trace viewer
We generate [Playwright Traces](https://playwright.dev/docs/trace-viewer) when tests run. These can be used to debug local and CI test failures.
![playwright trace viewer](docs/imgs/playwright-trace.jpg)
To open a local trace viewer for a given test output, run:
```shell
# Example:
npx playwright show-trace packages/insomnia-smoke-test/traces/app-can-send-requests/trace.zip
```bash
npm install
npm run test:smoke:dev # run all Smoke tests (dev mode)
```
Alternatively you can upload this trace to [trace.playwright.dev](https://trace.playwright.dev/).
Both the echo server (port 4010) and the Vite dev server start automatically.
### Additional Log levels
**Filter to one test** — pass a substring of the test title or file name:
You can enable additional logging to help you debug tests:
```bash
npm run test:smoke:dev -- oauth
```
- Playwright logs: `DEBUG=pw:api npm run test:smoke:dev`
- Insomnia console logs: `DEBUG=pw:browser npm run test:smoke:dev`
- WebServer console logs: `DEBUG=pw:WebServer npm run test:smoke:dev`
**Interactive UI:**
## Reproducing CI Failures
```bash
npm run test:smoke:dev -- --ui
```
### Getting traces from CI
**Step-through debugger:**
Traces from CI execution can be found in the failed CI job's artifacts.
```bash
PWDEBUG=1 npm run test:smoke:dev
```
![artifacts](docs/imgs/artifacts.png)
## Additional log levels
After downloading the artifacts, these can be extracted and loaded up into the [Trace viewer](#playwright-trace-viewer).
```bash
DEBUG=pw:api npm run test:smoke:dev # Playwright API logs
DEBUG=pw:browser npm run test:smoke:dev # Insomnia console logs
DEBUG=pw:WebServer npm run test:smoke:dev # Web server logs
```
### Build and package methods
## Traces and error context
It's possible to run the smoke tests for:
On failure, two artifacts are written under `packages/insomnia-smoke-test/traces/<test-name>/`:
- A `build`, the JS bundle that is loaded into an electron client
- A `package`, the executable binary (e.g. `.dmg` or `.exe`)
- **`error-context.md`** — error details, ARIA page snapshot at point of failure, and annotated test source. Read this first.
- **`trace.zip`** — full Playwright trace (network, screenshots, DOM snapshots).
For `build`:
Open a trace:
```shell
# Transpile js bundle
```bash
npx playwright show-trace packages/insomnia-smoke-test/traces/<test-name>/trace.zip
```
Or upload to [trace.playwright.dev](https://trace.playwright.dev/).
CI traces are available as artifacts on failed workflow runs.
## Build / package modes
Run against a JS bundle (`build`) or a packaged binary (`package`) instead of the dev watcher:
```bash
# build mode
npm run app-build
# Run tests
npm run test:smoke:build
```
For `package`:
```shell
# Build executable in /packages/insomnia/dist
# package mode
npm run app-package
# Run tests
npm run test:smoke:package
```
> Note: for local testing of the packaged app on macOS you need to change the entitlements temporarily to allow unsigned apps to run. You can do this by changing the `com.apple.security.cs.disable-library-validation` key in `entitlements.mac.inherit.plist` file to `true`. (Remember do not commit to the origin repo!)
> macOS package mode: set `com.apple.security.cs.disable-library-validation` to `true` in `entitlements.mac.inherit.plist` to allow unsigned local binaries. Do not commit this change.
Each of the above commands will automatically run the Express server, so you do not need to take any extra steps.
## Non-CI / pre-release tests
### Non recurring tests
Non recurring / non-CI tests, like pre-release ones, can be run using [Playwright VS Code extension](#playwright-vs-code-extension) or by running `test:dev` against the desired test file:
```shell
npm run test:dev -w packages/insomnia-smoke-test -- preferences-interactions
```bash
npm run test:dev -w insomnia-smoke-test -- tests/smoke/preferences-interactions.test.ts
```
### Refresh certs
## Cert refresh
The certs might need to be replaced after 2026 to fix the custom ca cert test
If the custom CA cert test fails after 2026:
```sh
mkcert -install
mkcert localhost
mkcert -CAROOT
```bash
mkcert -install && mkcert localhost && mkcert -CAROOT
```

View File

@@ -0,0 +1,95 @@
type: collection.insomnia.rest/5.0
schema_version: "5.1"
name: Auth tests
meta:
id: wrk_ca4cb9634c1045479b67b94f61725442
created: 1776427934242
modified: 1776427995078
description: ""
collection:
- url: http://127.0.0.1:4010/auth/basic
name: sends request with basic authentication
meta:
id: req_c29164bea31840a5a68eb67858759705
created: 1636141100570
modified: 1636142586648
isPrivate: false
description: ""
sortKey: 0
method: GET
headers:
- name: Authorization
value: Basic dXNlcjpwYXNz
disabled: true
authentication:
type: basic
useISO88591: false
username: user
password: pass
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: http://127.0.0.1:4010/auth/oauth1
name: sends request with oauth1
meta:
id: req_e713e4eeac1942a089e6c687b66c38ac
created: 1776427942368
modified: 1776427981837
isPrivate: false
description: ""
sortKey: -1776427942368
method: GET
headers:
- name: User-Agent
value: insomnia/12.5.1-alpha.0
description: ""
disabled: false
authentication:
type: oauth1
disabled: false
signatureMethod: HMAC-SHA1
consumerKey: key
tokenKey: key
tokenSecret: secret
privateKey: ""
version: "1.0"
nonce: ""
timestamp: ""
callback: ""
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
cookieJar:
name: Default Jar
meta:
id: jar_0ada3b0255e747a383ddf8848f88f4b2
created: 1636140994434
modified: 1637279629638
cookies:
- id: "429589439757017"
key: foo
value: bar
domain: domain.com
path: /
secure: false
httpOnly: false
environments:
name: Base Environment
meta:
id: env_482f5a98dfe64a948fff489dbd761e42
created: 1636140994432
modified: 1636140994432
isPrivate: false
data:
customValue: fromEnvManager

View File

@@ -0,0 +1 @@
ref: refs/heads/master

View File

@@ -0,0 +1,6 @@
[core]
repositoryformatversion = 0
filemode = false
bare = true
symlinks = false
ignorecase = true

View File

@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@@ -0,0 +1,6 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

View File

@@ -0,0 +1,3 @@
x<01><>A
<EFBFBD>0@Ѯs<D1AE><73><05>d☁Rz<52>8<1D>P<1B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>'<27><>->_<><5F><EFBFBD><EFBFBD>z<07>ZT-b<>S`<60>؍0R<30>B<EFBFBD><42><EFBFBD>`<18><>$<24>gv&<26>u<EFBFBD><75>)t^<5E><>+E&
<EFBFBD><EFBFBD><EFBFBD>}ߴl<DFB4>;]_G<5F><47><EFBFBD><EFBFBD><0F><><EFBFBD>f]ߓG<DF93>Ķ<EFBFBD>`N=?<3F><><EFBFBD>h<EFBFBD><68><02><>D,

View File

@@ -0,0 +1 @@
73a07b09e3e790de0277df5ac98e0ad62ecf83d4

View File

@@ -0,0 +1,65 @@
type: collection.insomnia.rest/5.0
schema_version: "5.1"
name: IPV6 Smoke tests
meta:
id: wrk_92410798721649cc8cd6f7c312a8764d
created: 1736279960080
modified: 1736279960080
collection:
- url: http://[::1]:4010/pets/1
name: send JSON request
meta:
id: req_22084aca139d42c6878afdc1a99db23b
created: 1636141014552
modified: 1636707449231
isPrivate: false
method: GET
headers:
- name: test
value: test
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: http://[::1]:4010/file/dummy.csv
name: sends dummy.csv request and shows rich response
meta:
id: req_ed583d913fec40c8b29f51b87b1843ef
created: 1636141038448
modified: 1636141047337
isPrivate: false
method: GET
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
cookieJar:
name: Default Jar
meta:
id: jar_0a46909e87a24a05961f3b7c6b591d23
created: 1636140994434
modified: 1637279629638
cookies:
- key: foo
value: bar
domain: domain.com
path: /
creation: 2021-11-18T23:53:05.310Z
id: "429589439757017"
environments:
name: Base Environment
meta:
id: env_6fda0ba942704e71a81a02e2bf5fe5ff
created: 1636140994432
modified: 1636140994432
isPrivate: false
data:
customValue: fromEnvManager

View File

@@ -20,7 +20,7 @@
"serve": "esr server/index.ts"
},
"devDependencies": {
"@playwright/test": "1.55.1",
"@playwright/test": "1.59.1",
"@ravanallc/grpc-server-reflection": "^0.1.6",
"@types/express": "^4.17.21",
"@types/oidc-provider": "^8.8.1",
@@ -30,6 +30,7 @@
"esbuild-runner": "2.2.2",
"express": "^4.21.2",
"express-basic-auth": "^1.2.1",
"git-http-backend": "^1.1.2",
"graphql": "^16.10.0",
"graphql-http": "^1.22.4",
"http-errors": "^2.0.0",

View File

@@ -1,4 +1,33 @@
import os from 'node:os';
import type { PlaywrightTestConfig } from '@playwright/test';
const isWindows = os.platform() === 'win32';
const echoServer: PlaywrightTestConfig['webServer'] = {
name: 'Echo server',
command: 'npm run serve',
url: 'http://localhost:4010',
timeout: 15 * 1000,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
wait: {
stdout: /Listening at http/,
},
};
const viteServer: PlaywrightTestConfig['webServer'] = {
name: 'Vite Server',
cwd: '../../',
command: 'npm run watch:app',
url: 'http://localhost:3334',
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
wait: {
stdout: /VITE\s+ready in/,
},
};
const onlyStartWebServerInDev = !process.env.BUNDLE || process.env.BUNDLE === 'dev';
const config: PlaywrightTestConfig = {
projects: [
{
@@ -20,12 +49,7 @@ const config: PlaywrightTestConfig = {
retries: 0,
},
],
webServer: {
command: 'npm run serve',
url: 'http://127.0.0.1:4010',
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
webServer: [echoServer, ...(onlyStartWebServerInDev ? [viteServer] : [])],
use: {
trace: {
mode: 'retain-on-failure',
@@ -35,7 +59,7 @@ const config: PlaywrightTestConfig = {
},
},
reporter: process.env.CI ? [['github'], ['line']] : [['list']],
timeout: process.env.CI ? 60 * 1000 : 20 * 1000,
timeout: process.env.CI || isWindows ? 60 * 1000 : 20 * 1000,
forbidOnly: !!process.env.CI,
outputDir: 'traces',
testDir: 'tests',

View File

@@ -0,0 +1,38 @@
import type { ElectronApplication, Locator, Page } from '@playwright/test';
import { BasePage } from '../base-page';
/**
* Component for the **Credentials tab** within Insomnia Preferences.
*
* Handles credential management functionality:
* - Add, edit, and remove credentials
*/
export class PreferencesCredentialsTab extends BasePage {
constructor(
readonly page: Page,
readonly app: ElectronApplication,
) {
super(page);
}
get root(): Locator {
return this.page.getByTestId('credentials-settings-tab');
}
async addAccessTokenGitCredential() {
await this.page.getByRole('button', { name: 'Create Git Credential' }).click();
await this.page.getByText('Access Token').click();
await this.page.getByRole('textbox', { name: 'Author Email' }).click();
await this.page.getByRole('textbox', { name: 'Author Email' }).fill('a@b.com');
await this.page.getByRole('textbox', { name: 'Author Name' }).click();
await this.page.getByRole('textbox', { name: 'Author Name' }).fill('author');
await this.page.getByRole('textbox', { name: 'Username' }).click();
await this.page.getByRole('textbox', { name: 'Username' }).fill('username');
await this.page.getByRole('textbox', { name: 'Git Access Token' }).click();
await this.page.getByRole('textbox', { name: 'Git Access Token' }).fill('accesstoken');
await this.page.getByRole('textbox', { name: 'Repository base URL' }).click();
await this.page.getByRole('textbox', { name: 'Repository base URL' }).fill('http://localhost:4010/git/');
await this.page.getByRole('button', { name: 'Save Credential' }).click();
}
}

View File

@@ -1,25 +1,30 @@
import type { ElectronApplication, Locator, Page } from '@playwright/test';
import { PreferencesCredentialsTab } from './credentials-tab';
import { PreferencesDataTab } from './data-tab';
type PreferencesTab = 'Data' | 'General' | 'Themes' | 'Plugins' | 'Other';
type PreferencesTab = 'Data' | 'General' | 'Themes' | 'Credentials' | 'Plugins' | 'Other';
/**
* Page Object for **Insomnia Preferences** modal.
*
* Composes preference tabs:
* - Data tab (import/export)
* - Credentials tab (Git credentials management)
* - Other tabs (themes, plugins, etc.) can be added as needed
*/
export class PreferencesPage {
/** Data tab (import/export functionality). */
readonly dataTab: PreferencesDataTab;
/** Credentials tab (Git credentials management). */
readonly credentialsTab: PreferencesCredentialsTab;
constructor(
readonly page: Page,
readonly app: ElectronApplication,
) {
this.dataTab = new PreferencesDataTab(page, app);
this.credentialsTab = new PreferencesCredentialsTab(page, app);
}
/** The root preferences dialog. */
@@ -43,7 +48,7 @@ export class PreferencesPage {
* Closes the preferences modal.
*/
async closePreferences(): Promise<void> {
await this.page.locator('.app').press('Escape');
await this.page.getByRole('button', { name: 'Modal Close Button' }).click();
await this.root.waitFor({ state: 'hidden' });
}
}

View File

@@ -80,6 +80,33 @@ export class ProjectPage extends BasePage {
await this.page.getByRole('button', { name: 'Create', exact: true }).click();
}
async createGitSyncProject(name = 'My Git Project'): Promise<void> {
await this.page.getByRole('button', { name: 'Create new Project' }).click();
await this.page.getByRole('textbox', { name: 'Project name' }).click();
await this.page.getByRole('textbox', { name: 'Project name' }).press('ControlOrMeta+a');
await this.page.getByRole('textbox', { name: 'Project name' }).fill(name);
await this.page.getByText('Git Sync').click();
await this.page.getByRole('button', { name: 'Access Token author Git' }).click();
await this.page.getByRole('option', { name: 'Custom Git Credential' }).click();
await this.page.getByRole('textbox', { name: 'Repository URL' }).click();
await this.page.getByRole('textbox', { name: 'Repository URL' }).fill('git-server.git');
await this.page.getByRole('button', { name: 'Show suggestions Branch' }).click();
await this.page.getByRole('option', { name: 'master' }).click();
await this.page.getByRole('button', { name: 'Scan for files' }).click();
await this.page.getByRole('button', { name: 'Create Blank Project' }).click();
const projectModalCloseButton = this.page.locator('[data-test-id="project-modal-close-button"]');
await projectModalCloseButton.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});
if (await projectModalCloseButton.isVisible()) {
await projectModalCloseButton.click();
}
await this.page.getByRole('button', { name: 'Personal workspace' }).click();
await this.page.getByRole('option', { name: /Magic/ }).locator('span').click();
await this.page.getByRole('button', { name: /Magic/ }).click();
await this.page.getByRole('option', { name: 'Personal workspace' }).locator('span').click();
await this.page.getByText('Git Project').waitFor({ state: 'visible', timeout: 10_000 });
await this.page.getByText('Git Project').click();
}
// ===========================================================================
// Import Operations
// ===========================================================================

View File

@@ -69,7 +69,7 @@ const hasBinaryBeenBuilt = fs.existsSync(path.resolve(cwd, insomniaBinary));
// NOTE: guard against missing build artifacts
if (bundleType() === 'dev' && !hasMainBeenBuilt) {
console.error(`ERROR: ${mainPath} not found at ${path.resolve(cwd, mainPath)}
Have you run "npm run watch:app"?`);
Ensure that playwright has run npm run watch:app`);
exit(1);
}
if (bundleType() === 'build' && !hasMainBeenBuilt) {

View File

@@ -77,18 +77,17 @@ export const test = baseTest.extend<{
insomnia: InsomniaApp;
}>({
app: async ({ playwright, trace, dataPath, userConfig }, use, testInfo) => {
invariant(testInfo.config.webServer?.url, 'Requires web server config');
const webServerUrl = testInfo.config.webServer.url;
const echoServer = 'http://localhost:4010';
const options: EnvOptions = {
INSOMNIA_DATA_PATH: dataPath,
INSOMNIA_API_URL: webServerUrl,
INSOMNIA_APP_WEBSITE_URL: webServerUrl + '/website',
INSOMNIA_AI_URL: webServerUrl + '/ai',
INSOMNIA_GITHUB_REST_API_URL: webServerUrl + '/github-api/rest',
INSOMNIA_GITHUB_API_URL: webServerUrl + '/github-api/graphql',
INSOMNIA_GITLAB_API_URL: webServerUrl + '/gitlab-api',
INSOMNIA_UPDATES_URL: webServerUrl || 'https://updates.insomnia.rest',
INSOMNIA_API_URL: echoServer,
INSOMNIA_APP_WEBSITE_URL: echoServer + '/website',
INSOMNIA_AI_URL: echoServer + '/ai',
INSOMNIA_GITHUB_REST_API_URL: echoServer + '/github-api/rest',
INSOMNIA_GITHUB_API_URL: echoServer + '/github-api/graphql',
INSOMNIA_GITLAB_API_URL: echoServer + '/gitlab-api',
INSOMNIA_UPDATES_URL: echoServer || 'https://updates.insomnia.rest',
INSOMNIA_MOCK_API_URL: 'https://mock-stage.insomnia.run',
INSOMNIA_SKIP_ONBOARDING: String(userConfig.skipOnboarding),
INSOMNIA_PUBLIC_KEY: userConfig.publicKey,
@@ -98,13 +97,14 @@ export const test = baseTest.extend<{
INSOMNIA_VAULT_SRP_SECRET: userConfig.vaultSrpSecret || '',
...(userConfig.session ? { INSOMNIA_SESSION: JSON.stringify(userConfig.session) } : {}),
};
const { ELECTRON_RUN_AS_NODE: _ignored, ...launchEnv } = process.env;
const electronApp = await playwright._electron.launch({
cwd,
executablePath,
args: bundleType() === 'package' ? ['--no-sandbox'] : ['--no-sandbox', mainPath],
env: {
...process.env,
...launchEnv,
...options,
PLAYWRIGHT: 'true',
},

View File

@@ -291,6 +291,17 @@ let deletedProjectIds: string[] = [];
let cloudSyncApiEnabled = false;
let remoteHasNewCommit = false;
const resetCloudSyncTestState = () => {
Object.keys(newSnapshots).forEach(projectId => {
delete newSnapshots[projectId];
});
Object.keys(newBlobs).forEach(blobId => {
delete newBlobs[blobId];
});
deletedProjectIds = [];
remoteHasNewCommit = false;
};
const getSnapshotsForProject = (projectId: string) => {
const originalSnapshots = projectSnapshots[projectId] || [];
const addedSnapshots = newSnapshots[projectId] || [];
@@ -308,13 +319,16 @@ export default function setup(app: Application) {
}
cloudSyncApiEnabled = enabled;
if (!enabled) {
// clear the test data when cloud sync is disabled to avoid affecting other tests
deletedProjectIds = [];
remoteHasNewCommit = false;
resetCloudSyncTestState();
}
return res.status(200).send();
});
app.post('/__test-config/cloud-sync/reset', json(), (_req, res) => {
resetCloudSyncTestState();
return res.status(200).send();
});
app.post('/__test-config/cloud-sync/new-commit', json(), (req, res) => {
const { enabled = false } = req.body ?? {};
remoteHasNewCommit = !!enabled;

View File

@@ -1,13 +1,28 @@
import { spawn } from 'node:child_process';
import crypto from 'node:crypto';
import { readFileSync } from 'node:fs';
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { createServer } from 'node:https';
import { tmpdir } from 'node:os';
import nodePath from 'node:path';
import type { Duplex } from 'node:stream';
import * as bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import express from 'express';
import { createHandler } from 'graphql-http/lib/use/http';
interface GitBackendService {
type: string;
cmd: string;
args: string[];
createStream(): Duplex;
}
// git-http-backend has no @types package; require+cast is the standard workaround
const backend = require('git-http-backend') as (
url: string,
cb: (err: Error | null, service: GitBackendService) => void,
) => NodeJS.ReadWriteStream;
import { basicAuthRouter } from './basic-auth';
import cloudSyncApi from './cloud-sync-api';
import githubApi from './github-api';
@@ -152,9 +167,56 @@ app.get('/v1/oauth/azure/config', (_req, res) => {
});
});
const GIT_FIXTURE_ROOT = nodePath.join(__dirname, '../fixtures/git-repo');
let currentGitTmpDir: string | null = null;
// Create a fresh per-test copy of the git fixture repo
app.post('/v1/test-utils/git/setup', (_req, res) => {
if (currentGitTmpDir && existsSync(currentGitTmpDir)) {
rmSync(currentGitTmpDir, { recursive: true, force: true });
}
currentGitTmpDir = mkdtempSync(nodePath.join(tmpdir(), 'insomnia-git-'));
cpSync(GIT_FIXTURE_ROOT, currentGitTmpDir, { recursive: true });
res.json({ success: true });
});
// Remove the per-test copy
app.delete('/v1/test-utils/git/setup', (_req, res) => {
if (currentGitTmpDir && existsSync(currentGitTmpDir)) {
rmSync(currentGitTmpDir, { recursive: true, force: true });
}
currentGitTmpDir = null;
res.json({ success: true });
});
// Git smart HTTP server backed by git-http-backend — accepts real pushes.
// Falls back to the fixture root if no per-test dir is set up.
app.use('/git', (req, res) => {
const root = currentGitTmpDir ?? GIT_FIXTURE_ROOT;
// req.url has the '/git' prefix stripped by Express, e.g. '/git-server.git/info/refs?...'
const repoPath = nodePath.join(root, req.url.split('?')[0].split('/')[1]);
req.pipe(
backend(req.url, (err, service) => {
if (err) {
res.status(500).end(err.message);
return;
}
res.setHeader('content-type', service.type);
const ps = spawn(service.cmd, service.args.concat(repoPath), {
env: { ...process.env, GIT_HTTP_EXPORT_ALL: '1' },
});
ps.stderr.on('data', d => console.error('[git]', String(d)));
ps.stdout.pipe(service.createStream()).pipe(ps.stdin);
}),
).pipe(res);
});
startWebSocketServer(
app.listen(port, () => {
app.listen(port, '::', () => {
console.log(`Listening at http://localhost:${port}`);
console.log(`Listening at http://127.0.0.1:${port}`);
console.log(`Listening at http://[::1]:${port}`);
console.log(`Listening at ws://localhost:${port}`);
}),
);
@@ -169,8 +231,10 @@ startWebSocketServer(
rejectUnauthorized: false,
},
app,
).listen(httpsPort, () => {
).listen(httpsPort, '::', () => {
console.log(`Listening at https://localhost:${httpsPort}`);
console.log(`Listening at https://127.0.0.1:${httpsPort}`);
console.log(`Listening at https://[::1]:${httpsPort}`);
console.log(`Listening at wss://localhost:${httpsPort}`);
}),
);

View File

@@ -81,47 +81,40 @@ let organizationFeatures = {
},
};
const user = {
id: 'email|64f0dd619ab0786da330d83a',
const v3User = {
id: 'acct_64a477e6b59d43a5a607f84b4f73e3ce',
email: 'insomnia-user@konghq.com',
name: 'Rick Morty',
first_name: 'Rick',
last_name: 'Morty',
picture: '',
bio: 'My BIO',
github: '',
linkedin: '',
twitter: '',
identities: null,
given_name: '',
family_name: '',
emails: [],
encryption_enabled: true,
is_externally_provisioned: false,
};
const whoami = {
sessionExpiry: 4_838_400,
publicKey: {
const v3EncryptionKeys = {
public_key: JSON.stringify({
alg: 'RSA-OAEP-256',
e: 'AQAB',
ext: true,
key_ops: ['encrypt'],
kty: 'RSA',
n: 'pTQVaUaiqggIldSKm6ib6eFRLLoGj9W-2O4gTbiorR-2b8-ZmKUwQ0F-jgYX71AjYaFn5VjOHOHSP6byNAjN7WzJ6A_Z3tytNraLoZfwK8KdfflOCZiZzQeD3nO8BNgh_zEgCHStU61b6N6bSpCKjbyPkmZcOkJfsz0LJMAxrXvFB-I42WYA2vJKReTJKXeYx4d6L_XGNIoYtmGZit8FldT4AucfQUXgdlKvr4_OZmt6hgjwt_Pjcu-_jO7m589mMWMebfUhjte3Lp1jps0MqTOvgRb0FQf5eoBHnL01OZjvFPDKeqlvoz7II9wFNHIKzSvgAKnyemh6DiyPuIukyQ',
},
encPrivateKey: {
}),
enc_private_key: JSON.stringify({
iv: '3a1f2bdb8acbf15f469d57a2',
t: '904d6b1bc0ece8e5df6fefb9efefda7c',
d: '2a7b0c4beb773fa3e3c2158f0bfa654a88c4041184c3b1e01b4ddd2da2c647244a0d66d258b6abb6a9385251bf5d79e6b03ef35bdfafcb400547f8f88adb8bceb7020f2d873d5a74fb5fc561e7bd67cea0a37c49107bf5c96631374dc44ddb1e4a8b5688dc6560fc6143294ed92c3ad8e1696395dfdf15975aa67b9212366dbfcb31191e4f4fe3559c89a92fb1f0f1cc6cbf90d8a062307fce6e7701f6f5169d9247c56dae79b55fba1e10fde562b971ca708c9a4d87e6e9d9e890b88fa0480360420e610c4e41459570e52ae72f349eadf84fc0a68153722de3280becf8a1762e7faebe964f0ad706991c521feda3440d3e1b22f2c221a80490359879bd47c0d059ace81213c74a1e192dbebd8a80cf58c9eb1fe461a971b88d3899baf4c4ef7141623c93fb4a54758f5e1cf9ee35cd00777fa89b24e4ded57219e770de2670619c6e971935c61ae72e3276cf8db49dfa0e91c68222f02d7e0c69b399af505de7e5a90852d83e0a30934b0362db986f3aaefaaf1a96fef3e8165287a3a7f0ee1e072d9dee3aefb86194e1d877d6b34529d45a70ec4573c35a7fe27833c77c3154b0ad02187e4fcecd408bcf4b29a85a5dc358cb479140f4983fcd936141f581764669651530af97d2b7d9416aea7de67e787f3e29ae3eba6672bcd934dc1e308783aa63a4ab46d48d213cf53ad6bd8828011f5bfa3aa5ee24551c694e829b54c93b1dda6c3ddda04756d68a28bec8d044c8af4147680dc5b972d0ca74299b0ab6306b9e7b99bf0557558df120455a272145b7aa792654730f3d670b76d72408f5ce1cf5fbd453d2903fa72cf26397437854ba8abbb731a8107f6a86a01fa98edc81bb42a4c1330f779e7a0fbd1820eaed78e03e40a996e03884b707556be06fd14ee8f4035469210d1d2bb8f58285fc2ab6de3d3cc0e4e1f40c6d9d24b50dc8e2e2374a0aff52031b3736c2982133bb19dd551ce1f953f4ba02b0cf53382c15752e202c138cb42b2322df103ff17fd886dfd5f992b711673cdf16048c4bff19038138b161c2e1783b85fc7b965a91ac4795fcbfebf827940cacdeae57946863aee027df43b36612f3cb8f34dc44396e87c564bf10f5b1a9dfbd6da3d7f4f65024b0b4f8ce51d01c230840941fc4523b17eb1c2522032f410e8328239a11a15ab755c32945ce52966d5bfb4666909ed2ca04d536e4bf92091563dd44d46cbb35e53c2481400058ab3b52a0280d262551073f61db125ee280e2cc1ec0bdf9c4817824261465011e34c2296411384f7f5e16742157c5520f137631edf498aa39c7c32b107e3634cbeb70feea19a233c8bd939d665135c9f7c1bb33cb47edc58bdbbcde9b0b9eb73a46642e4639289a62638fb7813e1eeaadd105c803de8357236f33c4bcf31a876b5867591af8f165eba0b35cf0b0886af17dab35a6a39f8f576387d6ffb9e677ee46fc0f11ff069a2a068fce441ff8f4125095fad228c2bf45c788d641941ed13c0a16fffcafd7c7eff11bb7550c0b7d54eebdbd2066e3bbdb47aaee2b5f1e499726324a40015458c7de1db0abe872594d8e6802deff7ea9518bdb3a3e46f07139267fd67dc570ba8ab04c2b37ce6a34ec73b802c7052a2eef0cae1b0979322ef86395535db80cf2a9a88aa7c2e5cc28a93612a8dafe1982f741d7cec28a866f6c09dba5b99ead24c3df0ca03c6c5afae41f3d39608a8f49b0d6a0b541a159409791c25ede103eb4f79cfbd0cc9c9aa6b591755c1e9fd07b5b9e38ed85b5939e65d127256f6a4c078f8c9d655c4f072f9cbcfb2e1e17eaa83dc62aaab2a6dc3735ee76ce7a215740f795f1fbe7136c7734ae3714438015e8fc383d63775a8abddb23cbc5f906c046bb0b5b31d492a7c151b40ea82c7c966e25820641c55b343b89d6378f90de5983fa76547e9d6c634effdf019a0fd9b6d3e488a5aa94f0710d517ba4f7c1ed82f9f3072612e953e036c0ec7f3c618368362f6da6f3af76056a66aef914805cc8b628f1c11695f760b535ded9ff66727273ae7e12d67a01243d75f22fec8ed1b043122a211c923aa92ecbbe01dd0d7195c3c0e09a2a6ab3eca354963122d5a0ec16e2b2b81b0ddce6ec0a312c492a96a4fd392f1deb6a1f3318541a3f87e5c9e73ee7edd3b855910f412789e25038108e1eaae04dcfb02b4d958c00c630dc8caa87a40798ce7156d2ade882e68832d39fe8f9bce6a995249a7383013a5093c4af55c3b7232de0f2593d82c30b8dabd0784455037f25f6bb66a6d0d8f72bc7be0dee2d0a8af44bb4e143257d873268d331722c3253ea5c004e72daf04c875e2054f2b4b2bca2979fd046a1e835600045edf2f159d851a540a91a1ab8fbcb64594d21942bbaa2160535d32496ba7ce4a76c6bdeb9bb4c5cab7bed1ae26564058d0be125803d7019b83b3953c4b0cc1f8299c4edcf6a5faa4765092412d368b277689900e71fb5d47581057adaa2dd494e0f66dc1aa16f3741973b0d9ffa1728aeafab84b777394a7afae0f8eabaa6b740f1c60ca26469f0c9356ec880ad6f4dc01b99bd14d7a4bb8afc97662a9e68b0155e4cdf3caa3402819ac6ce562c8fe06edb50a31cfd7a',
ad: '',
},
symmetricKey: {
alg: 'A256GCM',
ext: true,
k: 'w62OJNWF4G8iWA8ZrTpModiY8dICyHI7ko1vMLb877g=',
key_ops: ['encrypt', 'decrypt'],
kty: 'oct',
},
email: 'insomnia-user@konghq.com',
accountId: 'acct_64a477e6b59d43a5a607f84b4f73e3ce',
firstName: 'Rick',
lastName: 'Morty',
}),
enc_symmetric_key: JSON.stringify({
iv: '3a1f2bdb8acbf15f469d57a2',
t: '904d6b1bc0ece8e5df6fefb9efefda7c',
d: '2a7b0c4beb773fa3e3c2158f0bfa654a88c4041184c3b1e01b4ddd2da2c647244a0d66d258b6abb6a9385251bf5d79e6b03ef35bdfafcb400547f8f88adb8bceb7020f2d873d5a74fb5fc561e7bd67cea0a37c49107bf5c96631374dc44ddb1e4a8b5688dc6560fc6143294ed92c3ad8e1696395dfdf15975aa67b9212366dbfcb31191e4f4fe3559c89a92fb1f0f1cc6cbf90d8a062307fce6e7701f6f5169d9247c56dae79b55fba1e10fde562b971ca708c9a4d87e6e9d9e890b88fa0480360420e610c4e41459570e52ae72f349eadf84fc0a68153722de3280becf8a1762e7faebe964f0ad706991c521feda3440d3e1b22f2c221a80490359879bd47c0d059ace81213c74a1e192dbebd8a80cf58c9eb1fe461a971b88d3899baf4c4ef7141623c93fb4a54758f5e1cf9ee35cd00777fa89b24e4ded57219e770de2670619c6e971935c61ae72e3276cf8db49dfa0e91c68222f02d7e0c69b399af505de7e5a90852d83e0a30934b0362db986f3aaefaaf1a96fef3e8165287a3a7f0ee1e072d9dee3aefb86194e1d877d6b34529d45a70ec4573c35a7fe27833c77c3154b0ad02187e4fcecd408bcf4b29a85a5dc358cb479140f4983fcd936141f581764669651530af97d2b7d9416aea7de67e787f3e29ae3eba6672bcd934dc1e308783aa63a4ab46d48d213cf53ad6bd8828011f5bfa3aa5ee24551c694e829b54c93b1dda6c3ddda04756d68a28bec8d044c8af4147680dc5b972d0ca74299b0ab6306b9e7b99bf0557558df120455a272145b7aa792654730f3d670b76d72408f5ce1cf5fbd453d2903fa72cf26397437854ba8abbb731a8107f6a86a01fa98edc81bb42a4c1330f779e7a0fbd1820eaed78e03e40a996e03884b707556be06fd14ee8f4035469210d1d2bb8f58285fc2ab6de3d3cc0e4e1f40c6d9d24b50dc8e2e2374a0aff52031b3736c2982133bb19dd551ce1f953f4ba02b0cf53382c15752e202c138cb42b2322df103ff17fd886dfd5f992b711673cdf16048c4bff19038138b161c2e1783b85fc7b965a91ac4795fcbfebf827940cacdeae57946863aee027df43b36612f3cb8f34dc44396e87c564bf10f5b1a9dfbd6da3d7f4f65024b0b4f8ce51d01c230840941fc4523b17eb1c2522032f410e8328239a11a15ab755c32945ce52966d5bfb4666909ed2ca04d536e4bf92091563dd44d46cbb35e53c2481400058ab3b52a0280d262551073f61db125ee280e2cc1ec0bdf9c4817824261465011e34c2296411384f7f5e16742157c5520f137631edf498aa39c7c32b107e3634cbeb70feea19a233c8bd939d665135c9f7c1bb33cb47edc58bdbbcde9b0b9eb73a46642e4639289a62638fb7813e1eeaadd105c803de8357236f33c4bcf31a876b5867591af8f165eba0b35cf0b0886af17dab35a6a39f8f576387d6ffb9e677ee46fc0f11ff069a2a068fce441ff8f4125095fad228c2bf45c788d641941ed13c0a16fffcafd7c7eff11bb7550c0b7d54eebdbd2066e3bbdb47aaee2b5f1e499726324a40015458c7de1db0abe872594d8e6802deff7ea9518bdb3a3e46f07139267fd67dc570ba8ab04c2b37ce6a34ec73b802c7052a2eef0cae1b0979322ef86395535db80cf2a9a88aa7c2e5cc28a93612a8dafe1982f741d7cec28a866f6c09dba5b99ead24c3df0ca03c6c5afae41f3d39608a8f49b0d6a0b541a159409791c25ede103eb4f79cfbd0cc9c9aa6b591755c1e9fd07b5b9e38ed85b5939e65d127256f6a4c078f8c9d655c4f072f9cbcfb2e1e17eaa83dc62aaab2a6dc3735ee76ce7a215740f795f1fbe7136c7734ae3714438015e8fc383d63775a8abddb23cbc5f906c046bb0b5b31d492a7c151b40ea82c7c966e25820641c55b343b89d6378f90de5983fa76547e9d6c634effdf019a0fd9b6d3e488a5aa94f0710d517ba4f7c1ed82f9f3072612e953e036c0ec7f3c618368362f6da6f3af76056a66aef914805cc8b628f1c11695f760b535ded9ff66727273ae7e12d67a01243d75f22fec8ed1b043122a211c923aa92ecbbe01dd0d7195c3c0e09a2a6ab3eca354963122d5a0ec16e2b2b81b0ddce6ec0a312c492a96a4fd392f1deb6a1f3318541a3f87e5c9e73ee7edd3b855910f412789e25038108e1eaae04dcfb02b4d958c00c630dc8caa87a40798ce7156d2ade882e68832d39fe8f9bce6a995249a7383013a5093c4af55c3b7232de0f2593d82c30b8dabd0784455037f25f6bb66a6d0d8f72bc7be0dee2d0a8af44bb4e143257d873268d331722c3253ea5c004e72daf04c875e2054f2b4b2bca2979fd046a1e835600045edf2f159d851a540a91a1ab8fbcb64594d21942bbaa2160535d32496ba7ce4a76c6bdeb9bb4c5cab7bed1ae26564058d0be125803d7019b83b3953c4b0cc1f8299c4edcf6a5faa4765092412d368b277689900e71fb5d47581057adaa2dd494e0f66dc1aa16f3741973b0d9ffa1728aeafab84b777394a7afae0f8eabaa6b740f1c60ca26469f0c9356ec880ad6f4dc01b99bd14d7a4bb8afc97662a9e68b0155e4cdf3caa3402819ac6ce562c8fe06edb50a31cfd7a',
ad: '',
}),
salt_enc: '',
enc_driver_key: null,
};
const userVerifyA = {
@@ -419,13 +412,12 @@ collaboratorsList.total = collaboratorsList.collaborators.length + emailsAndGrou
export default function setup(app: Application) {
// User
app.get('/v1/user/profile', (_req, res) => {
console.log('GET *');
res.status(200).send(user);
app.get('/v3/users/me', (_req, res) => {
res.status(200).send(v3User);
});
app.get('/auth/whoami', (_req, res) => {
res.status(200).send(whoami);
app.get('/v3/users/me/encryption-keys', (_req, res) => {
res.status(200).send(v3EncryptionKeys);
});
// Vault related

View File

@@ -120,5 +120,5 @@ test('can send requests', async ({ page, insomnia }) => {
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await page.getByRole('button', { name: 'Cancel Request' }).click();
await page.click('text=Request was cancelled');
await page.getByText('Request was cancelled').click();
});

View File

@@ -15,11 +15,11 @@ test.describe('Cookie editor', () => {
test('create and send a cookie', async ({ page }) => {
// Open cookie editor
await page.click('button:has-text("Cookies")');
await page.getByRole('button', { name: 'Cookies' }).click();
// Edit existing cookie
await page.getByTestId('cookie-test-iteration-0').getByRole('button', { name: 'Edit' }).click();
await page.click('pre[role="presentation"]:has-text("bar")');
await page.locator('pre[role="presentation"]').filter({ hasText: 'bar' }).click();
await page.locator('[data-testid="CookieValue"] >> textarea').nth(1).fill('123');
await page.locator('text=Done').nth(1).click();
await page.getByTestId('cookie-test-iteration-0').click();
@@ -37,11 +37,11 @@ test.describe('Cookie editor', () => {
await page.locator('text=Done').nth(1).click();
await page.getByTestId('cookie-test-iteration-0').click();
await page.click('text=Done');
await page.getByText('Done').click();
// Send http request
await page.getByLabel('Request Collection').getByTestId('example http').press('Enter');
await page.click('[data-testid="request-pane"] button:has-text("Send")');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
// Check in the timeline that the cookie was sent
@@ -50,8 +50,8 @@ test.describe('Cookie editor', () => {
// Send ws request
await page.getByLabel('Request Collection').getByTestId('example websocket').press('Enter');
await page.click('text=ws://localhost:4010');
await page.click('[data-testid="request-pane"] >> text=Connect');
await page.getByText('ws://localhost:4010').click();
await page.getByTestId('request-pane').getByText('Connect').click();
// Check in the timeline that the cookie was sent
await page.getByRole('tab', { name: 'Console' }).click();
@@ -60,7 +60,7 @@ test.describe('Cookie editor', () => {
test('support __Host- prefix', async ({ page }) => {
// Open cookie editor
await page.click('button:has-text("Cookies")');
await page.getByRole('button', { name: 'Cookies' }).click();
// Create a new cookie
await page.getByRole('button', { name: 'Add Cookie' }).click();
@@ -74,11 +74,11 @@ test.describe('Cookie editor', () => {
.locator('text=Raw Cookie String >> input[type="text"]')
.fill('__Host-foo=bar; Expires=Tue, 19 Jan 2038 03:14:07 GMT; Secure; Domain=localhost; Path=/');
await page.locator('text=Done').nth(1).click();
await page.click('text=Done');
await page.getByText('Done').click();
// Send request
await page.getByLabel('Request Collection').getByTestId('example http').press('Enter');
await page.click('[data-testid="request-pane"] button:has-text("Send")');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
// Check in the timeline that the cookie was sent
await page.getByRole('tab', { name: 'Console' }).click();
@@ -87,7 +87,7 @@ test.describe('Cookie editor', () => {
test('cookie list should update when cookie is updated', async ({ page }) => {
// Open cookie editor
await page.click('button:has-text("Cookies")');
await page.getByRole('button', { name: 'Cookies' }).click();
// Set domain to empty
await page.getByTestId('cookie-test-iteration-0').getByRole('button', { name: 'Edit' }).click();

View File

@@ -0,0 +1,101 @@
import { expect } from '@playwright/test';
import { test } from '../../playwright/test';
const mockCredentials = {
email: 'insomnia-test@konghq.com',
gitUsername: 'insomnia-test',
username: 'insomnia',
token: '12345',
baseUrl: 'https://fakeurl.com/',
};
test.describe('Git Sync', () => {
test.describe('with git sync feature flag disabled', () => {
test.beforeEach(async ({ request }) => {
// Disable git sync feature flag for organization
await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/features', {
data: {
features: {
gitSync: {
enabled: false,
},
konnectSync: {
enabled: true,
},
},
},
});
});
test.afterEach(async ({ request }) => {
// Re-enable git sync feature flag for organization
await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/features', {
data: {
features: {
gitSync: {
enabled: true,
},
konnectSync: {
enabled: true,
},
},
},
});
});
test('should disable git sync usage', async ({ page }) => {
await page.getByTestId('settings-button').click();
await page.getByRole('tab', { name: 'Credentials' }).click();
await page.getByRole('button', { name: 'Create Git Credential' }).click();
await page.getByText('Access Token').click();
await page.getByRole('textbox', { name: 'Author Email' }).fill(mockCredentials.email);
await page.getByRole('textbox', { name: 'Author Name' }).fill(mockCredentials.gitUsername);
await page.getByRole('textbox', { name: 'Username', exact: true }).fill(mockCredentials.username);
await page.getByRole('textbox', { name: 'Git Access Token' }).fill(mockCredentials.token);
await page.getByRole('textbox', { name: 'Repository base URL' }).fill(mockCredentials.baseUrl);
await page.getByRole('button', { name: 'Save Credential' }).click();
await page.getByRole('button', { name: 'Modal Close Button' }).click();
await page.getByRole('button', { name: 'Create new Project' }).click();
await page.getByLabel('Project Type Item: git').click();
await expect.soft(page.getByLabel('Git Sync Feature Disabled Banner')).toBeVisible();
await expect.soft(page.getByLabel('Git Setup Form')).toBeHidden();
await expect.soft(page.getByRole('button', { name: 'Scan for files' })).toBeDisabled();
});
});
test.describe('with git storage rule disabled', () => {
test.beforeEach(async ({ request }) => {
// Set storage rule to disable git sync
await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/storage-rule', {
data: {
enableCloudSync: true,
enableGitSync: false,
enableLocalVault: true,
isOverridden: false,
},
});
});
test.afterEach(async ({ request }) => {
// reset the storage rule after test
await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/storage-rule', {
data: {
enableCloudSync: true,
enableGitSync: true,
enableLocalVault: true,
isOverridden: false,
},
});
});
test('disable git sync selection', async ({ page }) => {
await page.getByRole('button', { name: 'Create new Project' }).click();
const banner = page.getByLabel('Project Storage Restriction Banner');
await expect.soft(banner).toBeVisible();
await expect.soft(banner).not.toHaveText('Git Sync');
await expect.soft(page.getByLabel('Project Type: git')).toBeDisabled();
});
});
});

View File

@@ -33,9 +33,8 @@ test.describe('Export', () => {
await insomnia.preferencesPage.switchToPreferenceTab('Data');
await insomnia.preferencesPage.dataTab.exportProjectData(tempDir, 'yaml');
await waitForExportFiles(tempDir, 2);
await insomnia.preferencesPage.closePreferences();
const exportedFiles = getExportedFiles(tempDir);
expect.soft(exportedFiles.length).toBe(2);
expect.soft(exportedFiles).toHaveLength(2);
const fixtureMap: Record<string, string> = {
'Collection-A': FIXTURE_FILES[0],
'Collection-B': FIXTURE_FILES[1],
@@ -78,7 +77,7 @@ test.describe('Export', () => {
await insomnia.preferencesPage.dataTab.exportAllData(tempDir);
await insomnia.preferencesPage.closePreferences();
const exportedFiles = getExportedFiles(tempDir).filter((file: string) => !file.includes('scratchpad'));
expect.soft(exportedFiles.length).toBe(2);
expect.soft(exportedFiles).toHaveLength(2);
const fixtureMap: Record<string, string> = {
'Collection-A': FIXTURE_FILES[0],
'Collection-B': FIXTURE_FILES[1],
@@ -123,8 +122,6 @@ test.describe('Export', () => {
await insomnia.preferencesPage.dataTab.exportProjectData(exportFilePath, 'har');
await waitForExportFiles(tempDir, 1);
await insomnia.preferencesPage.closePreferences();
const exportedContent = readExportedFile(exportFilePath);
const har = JSON.parse(exportedContent);

View File

@@ -1,101 +1,171 @@
import { expect } from '@playwright/test';
import type { InsomniaApp } from '../../playwright/pages';
import { test } from '../../playwright/test';
const mockCredentials = {
email: 'insomnia-test@konghq.com',
gitUsername: 'insomnia-test',
username: 'insomnia',
token: '12345',
baseUrl: 'https://fakeurl.com/',
};
test.describe('Git Sync', () => {
test.describe('with git sync feature flag disabled', () => {
test.beforeEach(async ({ request }) => {
// Disable git sync feature flag for organization
await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/features', {
data: {
features: {
gitSync: {
enabled: false,
},
konnectSync: {
enabled: true,
},
},
},
});
});
test.slow();
test.afterEach(async ({ request }) => {
// Re-enable git sync feature flag for organization
await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/features', {
data: {
features: {
gitSync: {
enabled: true,
},
konnectSync: {
enabled: true,
},
},
},
});
});
test('should disable git sync usage', async ({ page }) => {
await page.getByTestId('settings-button').click();
await page.getByRole('tab', { name: 'Credentials' }).click();
await page.getByRole('button', { name: 'Create Git Credential' }).click();
await page.getByText('Access Token').click();
await page.getByRole('textbox', { name: 'Author Email' }).fill(mockCredentials.email);
await page.getByRole('textbox', { name: 'Author Name' }).fill(mockCredentials.gitUsername);
await page.getByRole('textbox', { name: 'Username', exact: true }).fill(mockCredentials.username);
await page.getByRole('textbox', { name: 'Git Access Token' }).fill(mockCredentials.token);
await page.getByRole('textbox', { name: 'Repository base URL' }).fill(mockCredentials.baseUrl);
await page.getByRole('button', { name: 'Save Credential' }).click();
await page.getByRole('button', { name: 'Modal Close Button' }).click();
await page.getByRole('button', { name: 'Create new Project' }).click();
await page.getByLabel('Project Type Item: git').click();
await expect.soft(page.getByLabel('Git Sync Feature Disabled Banner')).toBeVisible();
await expect.soft(page.getByLabel('Git Setup Form')).toBeHidden();
await expect.soft(page.getByRole('button', { name: 'Scan for files' })).toBeDisabled();
});
test.beforeEach(async ({ insomnia, request }) => {
await request.post('http://127.0.0.1:4010/v1/test-utils/git/setup');
await addAccessTokenGitCredential(insomnia);
await insomnia.projectPage.createGitSyncProject();
});
test.describe('with git storage rule disabled', () => {
test.beforeEach(async ({ request }) => {
// Set storage rule to disable git sync
await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/storage-rule', {
data: {
enableCloudSync: true,
enableGitSync: false,
enableLocalVault: true,
isOverridden: false,
},
});
});
test.afterEach(async ({ request }) => {
await request.delete('http://127.0.0.1:4010/v1/test-utils/git/setup');
});
test.afterEach(async ({ request }) => {
// reset the storage rule after test
await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/storage-rule', {
data: {
enableCloudSync: true,
enableGitSync: true,
enableLocalVault: true,
isOverridden: false,
},
});
});
// Creates a git sync project, opens the Branches modal, creates "branch1",
// and verifies the active branch switches to branch1.
test('Create new branch and switch to it', async ({ page }) => {
await page.getByTestId('git-dropdown').click();
await page.getByRole('menuitemradio', { name: 'Branches' }).click();
await page.getByRole('textbox', { name: 'New branch name:' }).click();
await page.getByRole('textbox', { name: 'New branch name:' }).fill('branch1');
await page.getByRole('button', { name: 'Create', exact: true }).click();
await expect.soft(page.getByText('branch1 *')).toBeVisible();
});
test('disable git sync selection', async ({ page }) => {
await page.getByRole('button', { name: 'Create new Project' }).click();
const banner = page.getByLabel('Project Storage Restriction Banner');
await expect.soft(banner).toBeVisible();
await expect.soft(banner).not.toHaveText('Git Sync');
await expect.soft(page.getByLabel('Project Type: git')).toBeDisabled();
});
// Creates a collection to produce an unstaged change, stages it, commits with message "1",
// then opens History and verifies the commit appears in the log.
test('Commit and check history', async ({ page }) => {
await page.getByRole('button', { name: 'New request collection' }).click();
await page.getByRole('textbox', { name: 'Name', exact: true }).click();
await page.getByRole('textbox', { name: 'Name', exact: true }).press('ControlOrMeta+a');
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Collection 1');
await page.getByRole('textbox', { name: 'File name my_collection' }).click();
await page.getByRole('textbox', { name: 'File name my_collection' }).press('ControlOrMeta+a');
await page.getByRole('textbox', { name: 'File name my_collection' }).fill('collection_1');
await page.getByRole('button', { name: 'Create', exact: true }).click();
await page.getByTestId('git-dropdown').click();
await expect.soft(page.getByRole('menuitemradio', { name: 'Commit' })).toBeVisible();
await page.getByRole('menuitemradio', { name: 'Commit' }).click();
await expect.soft(page.getByLabel('Unstaged changes').locator('span')).toContainText('collection_1.yaml');
await page.locator('button[name="Stage all changes"]').click();
await page.getByRole('textbox', { name: 'Message' }).click();
await page.getByRole('textbox', { name: 'Message' }).fill('1');
await page.getByRole('button', { name: 'Commit', exact: true }).click();
await page.getByTestId('git-dropdown').click();
await page.getByText('History').click();
await expect.soft(page.getByLabel('1', { exact: true }).getByRole('rowheader')).toContainText('1');
});
// Creates branch1, commits a new collection on it, switches back to master,
// merges branch1 into master, and verifies the collection is visible on master.
test('Merge branch and verify changes on the other branch has been merged into current branch', async ({ page }) => {
await page.getByTestId('git-dropdown').click();
await page.getByRole('menuitemradio', { name: 'Branches' }).click();
await page.getByRole('textbox', { name: 'New branch name:' }).click();
await page.getByRole('textbox', { name: 'New branch name:' }).fill('branch1');
await page.getByRole('button', { name: 'Create', exact: true }).click();
await expect.soft(page.getByText('branch1 *')).toBeVisible();
await page.getByTestId('close-git-project-branches-modal').click();
await page.getByTestId('git-project-branches-modal-overlay').waitFor({ state: 'hidden' });
await page.getByRole('button', { name: 'New request collection' }).click();
await page.getByRole('textbox', { name: 'Name', exact: true }).click();
await page.getByRole('textbox', { name: 'Name', exact: true }).press('ControlOrMeta+a');
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('collection 1');
await page.getByRole('textbox', { name: 'File name my_collection' }).click();
await page.getByRole('textbox', { name: 'File name my_collection' }).press('ControlOrMeta+a');
await page.getByRole('textbox', { name: 'File name my_collection' }).fill('collection_1');
await page.getByRole('button', { name: 'Create', exact: true }).click();
await page.getByTestId('project').click();
await page.getByTestId('git-dropdown').click();
await page.getByRole('menuitemradio', { name: 'Commit' }).click();
await page.locator('button[name="Stage all changes"]').click();
await page.getByRole('textbox', { name: 'Message' }).click();
await page.getByRole('textbox', { name: 'Message' }).fill('commit 1');
await page.getByRole('button', { name: 'Commit', exact: true }).click();
await page.getByTestId('git-dropdown').click();
await page.getByRole('menuitemradio', { name: 'master' }).click();
await page.locator('html').click();
await page.getByTestId('git-dropdown').click();
await page.getByRole('menuitemradio', { name: 'Branches' }).click();
await page.getByLabel('branch1').getByRole('button', { name: 'Merge' }).click();
await page.getByRole('button', { name: ' Confirm' }).click();
await page.getByTestId('close-git-project-branches-modal').click();
await page.getByTestId('git-project-branches-modal-overlay').waitFor({ state: 'hidden' });
await expect.soft(page.getByText('collection 1')).toBeVisible();
});
// Creates a collection, commits it, then pushes to the remote git server.
// Verifies the "Push completed" toast appears, confirming a successful push.
test('Push committed changes to remote', async ({ page }) => {
await page.getByRole('button', { name: 'New request collection' }).click();
await page.getByRole('textbox', { name: 'Name', exact: true }).click();
await page.getByRole('textbox', { name: 'Name', exact: true }).press('ControlOrMeta+a');
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Push Test Collection');
await page.getByRole('textbox', { name: 'File name my_collection' }).click();
await page.getByRole('textbox', { name: 'File name my_collection' }).press('ControlOrMeta+a');
await page.getByRole('textbox', { name: 'File name my_collection' }).fill('push_test_collection');
await page.getByRole('button', { name: 'Create', exact: true }).click();
await page.getByTestId('git-dropdown').click();
await page.getByRole('menuitemradio', { name: 'Commit' }).click();
await page.locator('button[name="Stage all changes"]').click();
await page.getByRole('textbox', { name: 'Message' }).fill('push test commit');
await page.getByRole('button', { name: 'Commit', exact: true }).click();
await page.getByTestId('git-dropdown').click();
await page.getByRole('menuitemradio', { name: 'Push' }).click();
await expect.soft(page.getByText('Push completed')).toBeVisible();
});
// Creates "branch-to-delete", checks out master, then deletes the branch via the
// two-step PromptButton (Delete → Confirm). Verifies the branch is removed from the list.
test('Delete a branch', async ({ page }) => {
await page.getByTestId('git-dropdown').click();
await page.getByRole('menuitemradio', { name: 'Branches' }).click();
await page.getByRole('textbox', { name: 'New branch name:' }).click();
await page.getByRole('textbox', { name: 'New branch name:' }).fill('branch-to-delete');
await page.getByRole('button', { name: 'Create', exact: true }).click();
await expect.soft(page.getByText('branch-to-delete *')).toBeVisible();
await page.getByRole('row', { name: 'master' }).getByRole('button', { name: 'Checkout' }).click();
await expect.soft(page.getByText('master *')).toBeVisible();
await page.getByRole('row', { name: 'branch-to-delete' }).getByRole('button', { name: 'Delete' }).click();
await page.getByRole('row', { name: 'branch-to-delete' }).getByRole('button', { name: 'Confirm' }).click();
await expect.soft(page.getByRole('row', { name: 'branch-to-delete' })).toBeHidden();
await page.getByTestId('close-git-project-branches-modal').click();
await page.getByTestId('git-project-branches-modal-overlay').waitFor({ state: 'hidden' });
await expect.soft(page.getByTestId('git-dropdown')).toContainText('master');
});
// Creates a collection to produce an unstaged change, opens the staging modal,
// clicks "Discard all changes" and confirms. Verifies the modal auto-closes,
// indicating all changes were discarded.
test('Discard all unstaged changes', async ({ page }) => {
await page.getByRole('button', { name: 'New request collection' }).click();
await page.getByRole('textbox', { name: 'Name', exact: true }).click();
await page.getByRole('textbox', { name: 'Name', exact: true }).press('ControlOrMeta+a');
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Discard Test Collection');
await page.getByRole('textbox', { name: 'File name my_collection' }).click();
await page.getByRole('textbox', { name: 'File name my_collection' }).press('ControlOrMeta+a');
await page.getByRole('textbox', { name: 'File name my_collection' }).fill('discard_test_collection');
await page.getByRole('button', { name: 'Create', exact: true }).click();
await page.getByTestId('git-dropdown').click();
await page.getByRole('menuitemradio', { name: 'Commit' }).click();
await expect.soft(page.getByLabel('Unstaged changes').locator('span')).toContainText('discard_test_collection.yaml');
await page.locator('button[name="Discard all changes"]').click();
await page.getByTestId('discard-changes-confirm-button').click();
// After discarding all changes the staging modal auto-closes
await expect.soft(page.getByLabel('Unstaged changes')).toBeHidden();
});
});
async function addAccessTokenGitCredential(insomnia: InsomniaApp) {
await insomnia.statusbar.openPreferences();
await insomnia.preferencesPage.switchToPreferenceTab('Credentials');
await insomnia.preferencesPage.credentialsTab.addAccessTokenGitCredential();
await expect.soft(insomnia.page.getByRole('row', { name: 'Custom Git Credential' })).toBeVisible();
await insomnia.preferencesPage.closePreferences();
}

View File

@@ -0,0 +1,40 @@
import { expect } from '@playwright/test';
import { test } from '../../playwright/test';
test('can send ipv6 requests', async ({ page, insomnia }) => {
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
const statusTag = page.locator('[data-testid="response-status-tag"]:visible');
const responseBody = page.getByTestId('response-pane');
await insomnia.projectPage.importFixture('ipv6-collection.yaml');
await page.getByLabel('Request Collection')
.getByTestId('send JSON request').press('Enter');
await expect.soft(page.getByTestId('request-pane')
.getByTestId('OneLineEditor')
.getByText('http://[::1]:4010/pets/1')
).toBeVisible();
await page.getByTestId('request-pane')
.getByRole('button', { name: 'Send' }).click();
await expect.soft(statusTag).toContainText('200 OK');
await expect.soft(responseBody).toContainText('"id": "1"');
await page
.getByLabel('Request Collection')
.getByTestId('sends dummy.csv request and shows rich response')
.press('Enter');
await expect.soft(page.getByTestId('request-pane')
.getByTestId('OneLineEditor')
.getByText('http://[::1]:4010/file/dummy.csv')
).toBeVisible();
await page.getByTestId('request-pane')
.getByRole('button', { name: 'Send' }).click();
await expect.soft(statusTag).toContainText('200 OK');
await page.getByRole('button', { name: 'Preview' }).click();
await page.getByRole('menuitem', { name: 'Raw Data' }).click();
await expect.soft(responseBody).toContainText('a,b,c');
});

View File

@@ -208,6 +208,7 @@ test.describe('pre-request features tests', () => {
}),
};
});
test('run test cases', async ({ page }) => {
for (const tc of testCases) {
console.log(`Running test case: ${tc.name}`);
@@ -230,6 +231,7 @@ test.describe('pre-request features tests', () => {
tc.customVerify(bodyJson);
}
});
test('send request with content type', async ({ page }) => {
await page.getByTestId('settings-button').click();
await page.getByTestId('dataFolders').click();
@@ -660,14 +662,15 @@ test.describe('unhappy paths', () => {
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
// verify
await expect.soft(page.getByTestId('response-pane')).toContainText('my custom error');
await expect.soft(page.getByTestId('response-pane')).toContainText(`my custom error`);
await page.getByRole('tab', { name: 'Scripts' }).click();
await page.getByTestId('CodeEditor').getByRole('textbox').press('ControlOrMeta+a');
await page.keyboard.press('Backspace');
await editor.fill(`insomnia.INVALID_FIELD.set('', '')`);
await page.getByRole('tab', { name: 'Body' }).click();
// CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
// send
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
@@ -678,3 +681,165 @@ test.describe('unhappy paths', () => {
.toContainText(`Cannot read properties of undefined (reading 'set')`);
});
});
test.describe('sandbox features', () => {
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
test.beforeEach(async ({ app, page }) => {
const text = await loadFixture('pre-request-collection.yaml');
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
await page.getByLabel('Import').click();
await page.locator('[data-test-id="import-from-clipboard"]').click();
await page.getByRole('button', { name: 'Scan' }).click();
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
});
// Blocked Roots / Scopes group: 'this' is blocked.
test('blocked roots / scopes group', async ({ page }) => {
await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
await page.getByRole('tab', { name: 'Scripts' }).click();
const editor = page.getByTestId('CodeEditor').getByRole('textbox');
// enter script that accesses a property on 'this'.
await editor.fill(`insomnia.environment.set('result', String(this?.process));`);
// CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
// verify blocked-root error
await expect
.soft(page.getByTestId('response-pane'))
.toContainText("The script was blocked because it used 'this'.");
// navigate to Settings → Scripting, disable the "Scopes" blocked roots group
await page.getByTestId('settings-button').click();
await page.locator('text=Insomnia Preferences').first().click();
await page.getByRole('tab', { name: 'Scripting' }).click();
const scopesSwitch = page.locator('div:has(> h4:has-text("Scopes")) label[data-react-aria-pressable]');
await scopesSwitch.scrollIntoViewIfNeeded();
await scopesSwitch.click();
await page.locator('.app').press('Escape');
// re-send — no sandbox error; this === undefined.
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect
.soft(page.getByTestId('response-pane'))
.not.toContainText("The script was blocked because it used 'this'.");
await expect.soft(page.locator('[data-testid="response-status-tag"]:visible')).toContainText('200 OK');
});
// Blocked Properties / Prototype Mutation group: 'prototype' is blocked.
test('blocked properties / prototype mutation group', async ({ page }) => {
await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
// enter script that accesses Object.prototype.
await page.getByRole('tab', { name: 'Scripts' }).click();
const editor = page.getByTestId('CodeEditor').getByRole('textbox');
await editor.fill(`insomnia.environment.set('result', typeof Object.prototype.toString);`);
// CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
// verify blocked-property error
await expect
.soft(page.getByTestId('response-pane'))
.toContainText("The script was blocked because it used the property 'prototype'.");
// navigate to Settings → Scripting, disable the "Prototype Mutation" blocked properties group
await page.getByTestId('settings-button').click();
await page.locator('text=Insomnia Preferences').first().click();
await page.getByRole('tab', { name: 'Scripting' }).click();
const protoMutationSwitch = page.locator(
'div:has(> h4:has-text("Prototype Mutation")) label[data-react-aria-pressable]',
);
await protoMutationSwitch.scrollIntoViewIfNeeded();
await protoMutationSwitch.click();
await expect.soft(protoMutationSwitch).not.toHaveAttribute('data-selected');
await page.locator('.app').press('Escape');
// re-send — prototype access now allowed; Object.prototype.toString is a function
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect
.soft(page.getByTestId('response-pane'))
.not.toContainText("The script was blocked because it used the property 'prototype'.");
await expect.soft(page.locator('[data-testid="response-status-tag"]:visible')).toContainText('200 OK');
});
// Mask Rules / Runtime APIs group: 'Function' is masked to undefined at runtime.
test('Mask Rules / Runtime APIs group.', async ({ page }) => {
await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
// enter script that uses the Function constructor, only masked at runtime.
await page.getByRole('tab', { name: 'Scripts' }).click();
const editor = page.getByTestId('CodeEditor').getByRole('textbox');
await editor.fill(`const f = new Function('return 42'); insomnia.environment.set('result', f());`);
// CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
// send — Function masked to undefined → V8 uses the identifier name: "Function is not a constructor"
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect.soft(page.getByTestId('response-pane')).toContainText('Function is not a constructor');
// navigate to Settings → Scripting, disable the "Runtime APIs" mask group
await page.getByTestId('settings-button').click();
await page.locator('text=Insomnia Preferences').first().click();
await page.getByRole('tab', { name: 'Scripting' }).click();
const runtimeApisSwitch = page.locator('div:has(> h4:has-text("Runtime APIs")) label[data-react-aria-pressable]');
await runtimeApisSwitch.scrollIntoViewIfNeeded();
await runtimeApisSwitch.click();
await expect.soft(runtimeApisSwitch).not.toHaveAttribute('data-selected');
await page.locator('.app').press('Escape');
// re-send — Function is now the real constructor; script returns 42
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect.soft(page.getByTestId('response-pane')).not.toContainText('Function is not a constructor');
await expect.soft(page.locator('[data-testid="response-status-tag"]:visible')).toContainText('200 OK');
});
test('Layered security / unblocked properties resolve undefined', async ({ page }) => {
await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
// enter script that accesses a property on 'process'.
await page.getByRole('tab', { name: 'Scripts' }).click();
const editor = page.getByTestId('CodeEditor').getByRole('textbox');
await editor.fill(`insomnia.environment.set('result', String(process?.version));`);
// CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
// verify blocked-root error
await expect
.soft(page.getByTestId('response-pane'))
.toContainText("The script was blocked because it used 'process'.");
// navigate to Settings → Scripting, disable only the "Node.js Internals" BLOCKED ROOTS group.
await page.getByTestId('settings-button').click();
await page.locator('text=Insomnia Preferences').first().click();
await page.getByRole('tab', { name: 'Scripting' }).click();
const nodeInternalsSwitch = page.locator(
'xpath=//h4[normalize-space(text())="Node.js Internals"]/following-sibling::div[1]//label[@data-react-aria-pressable]',
);
await nodeInternalsSwitch.scrollIntoViewIfNeeded();
await nodeInternalsSwitch.click();
await expect.soft(nodeInternalsSwitch).not.toHaveAttribute('data-selected');
await page.locator('.app').press('Escape');
// process?.version === undefined.
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect
.soft(page.getByTestId('response-pane'))
.not.toContainText("The script was blocked because it used 'process'.");
await expect.soft(page.locator('[data-testid="response-status-tag"]:visible')).toContainText('200 OK');
});
});

View File

@@ -2,6 +2,8 @@
This document breaks the renderer `nodeIntegration: false` migration into deliverable slices that can move in parallel without creating excessive merge conflict risk.
Update: PR 1 through PR 3 are merged. The remaining work is re-scoped around single feature flows instead of broad subsystem buckets so each PR can land with tighter review, clearer ownership, and faster iteration.
The plan assumes the current guardrails are already in place:
- renderer import analyzer in `vite.config.ts`
@@ -15,6 +17,9 @@ The plan assumes the current guardrails are already in place:
2. If a PR removes offenders, update `config/renderer-node-import-baseline.json` in the same PR.
3. Prefer moving privileged behavior behind existing preload or `window.main` APIs before inventing new bridge surface.
4. Do not combine route cleanup with subsystem redesign unless the route is blocked on the subsystem boundary.
5. Remaining candidates should stay scoped to one user-visible feature flow or one tightly bounded privileged service.
6. Each PR should carry an explicit test automation plan before implementation starts.
7. If a feature depends on sync or storage behavior, land the sync/storage boundary first instead of mixing the dependency into the same PR.
## Reviewer Lanes
@@ -26,23 +31,32 @@ The plan assumes the current guardrails are already in place:
## Parallelization Summary
Can start immediately in parallel:
Already merged:
- PR 1: Route path-only cleanup
- PR 2: Route fs-backed cleanup
- PR 3: Shared browser-safe helper cleanup
Can start in parallel with the above if staffed separately:
Next PR candidates:
- PR 4: Renderer-to-main boundary extraction
- Candidate: Sync storage boundary foundation
- Candidate: Import parsing and persistence boundary
- Candidate: gRPC proto asset boundary **(quick win candidate)**
- Candidate: Plugin discovery boundary
- Candidate: Templating bootstrap boundary
- Candidate: OAuth token crypto cleanup **(quick win candidate)**
- Candidate: Response archival and compression boundary
- Candidate: Script executor boundary
Should usually wait until PR 4 is clear enough to avoid duplicated refactors:
Dependencies:
- PR 5: Network and gRPC boundary pass
- PR 6: Sync and storage boundary pass
- PR 7: Plugin and templating boundary pass
- Import depends on sync storage because import creates or updates persisted workspace state.
- Plugin discovery and templating should stay split so one does not become a catch-all runtime PR.
- gRPC proto asset work can likely move ahead once the sync-storage bridge pattern is clear.
- OAuth cleanup looks like the smallest remaining isolated boundary and is a good candidate for a fast iteration.
- Response archival and `script-executor.ts` should stay separate from the feature-scoped candidates unless a later PR proves they are truly part of the same flow.
## PR Board
## Candidate Backlog
## PR 0: Guardrails and Baseline
@@ -69,6 +83,8 @@ Suggested reviewers:
## PR 1: Route Path-Only Cleanup
Status: merged
Purpose:
- Remove route-local `node:path` usage where existing `window.path` is already sufficient.
@@ -108,10 +124,12 @@ Concurrent with:
- PR 2
- PR 3
- PR 4
- later dependency-clearing candidate
## PR 2: Route FS-Backed Cleanup
Status: merged
Purpose:
- Remove route-level `node:fs` and remaining `node:path` usage that touches downloads or file reads.
@@ -153,6 +171,8 @@ Concurrent with:
## PR 3: Shared Browser-Safe Helper Cleanup
Status: merged
Purpose:
- Remove Node builtin usage from helper modules that should be safe to load in the renderer.
@@ -199,35 +219,121 @@ Concurrent with:
- PR 1
- PR 2
- PR 4
- later dependency-clearing candidate
## PR 4: Renderer-to-Main Boundary Extraction
## Candidate: Sync Storage Boundary Foundation
Status: candidate
Purpose:
- Stop renderer code from pulling `src/main` implementations into the client graph when only pure helpers or types are needed.
- Move local project sync storage so renderer code stops owning filesystem, compression, and VCS-path details.
Single feature scope:
- Local project persistence for sync-backed workspaces.
Primary files:
- `src/sync/store/drivers/file-system-driver.ts`
- `src/sync/store/drivers/graceful-rename.ts`
- `src/sync/store/hooks/compress.ts`
- `src/sync/store/index.ts`
- `src/sync/vcs/util.ts`
- `src/sync/vcs/vcs.ts`
- `src/sync/vcs/create-vcs.ts`
Implementation notes:
- Introduce a narrow storage-oriented bridge for read, write, rename, compression, and VCS-adjacent path work.
- Avoid mixing import, plugin, or network behavior into this PR.
- Move the vcs class entirely to main, simplify it down so its clear what internal state it has, expose its functions over IPC.
- create an event listener in the renderer to handle events from the conflictHandler function passed into vcs.
Expected risk: high
Quick win: no
Suggested reviewers:
- Sync/storage
- Electron/runtime
Baseline entries to remove:
- `src/sync/store/drivers/file-system-driver.ts -> fs/promises`
- `src/sync/store/drivers/file-system-driver.ts -> path`
- `src/sync/store/drivers/graceful-rename.ts -> fs/promises`
- `src/sync/store/hooks/compress.ts -> zlib`
- `src/sync/store/index.ts -> path`
- `src/sync/vcs/util.ts -> crypto`
- `src/sync/vcs/vcs.ts -> crypto`
- `src/sync/vcs/vcs.ts -> path`
Dependencies:
- none
Test automation plan:
- Extend `src/sync/store/hooks/__tests__/compress.test.ts` and related sync store tests to cover the new privileged boundary behavior.
- Add focused unit coverage for any new main-process sync bridge or IPC handlers.
- Add a renderer-side contract test that proves sync store calls delegate through the bridge instead of importing Node-backed code directly.
- Keep smoke coverage limited to one sync-backed roundtrip so this PR stays quick to iterate.
Enables:
- Import parsing and persistence boundary
- gRPC proto asset boundary
- Plugin discovery boundary
- Templating bootstrap boundary
Out of scope:
- import parsing or import persistence
- gRPC proto temp-file handling
- plugin discovery or templating runtime changes
## Candidate: Import Parsing and Persistence Boundary
Status: candidate
Purpose:
- Make import a self-contained feature flow that relies on the sync boundary instead of importing `src/main` helpers or privileged file access into renderer-reachable modules.
Single feature scope:
- Scan, parse, and persist imported resources.
Primary files:
- `src/common/import.ts`
- `src/routes/import.scan.tsx`
- `src/routes/import.resources.tsx`
- `src/ui/components/modals/import-modal/import-modal.tsx`
- `src/main/importers/convert.ts`
- `src/main/importers/importers/curl.ts`
- `src/main/importers/importers/openapi-3.ts`
- `src/main/importers/importers/swagger-2.ts`
- `src/main/network/parse-header-strings.ts`
- `src/main/secure-read-file.ts`
Likely implementation:
Implementation notes:
- Move pure helper logic into shared modules outside `src/main`.
- Leave privileged code in `src/main`.
- Update renderer imports to target shared modules or types only.
- Keep importer execution and privileged file reads behind explicit main-process entrypoints.
- Extract pure importer helpers into shared modules only where they are genuinely renderer-safe.
- Make the import flow consume the sync/storage candidate instead of mixing storage work into this candidate.
Expected risk: medium
Expected risk: high
Quick win: no
Suggested reviewers:
- Electron/runtime
- Router/UI
- Network/gRPC
- Sync/storage
- Electron/runtime
Baseline entries to remove:
@@ -242,40 +348,52 @@ Baseline entries to remove:
Dependencies:
- none
- Sync storage boundary foundation
Concurrent with:
Test automation plan:
- PR 1
- PR 3
- Extend `src/common/__tests__/import.test.ts` to cover scan and persist paths after the boundary cleanup.
- Add route-level coverage for `src/routes/import.scan.tsx` and `src/routes/import.resources.tsx` where the bridge contract changes.
- Keep one UI-level import smoke path for a representative source such as curl or file import.
- Update the renderer-node-import baseline in the same PR once the import offenders are removed.
Blocks or informs:
Enables:
- PR 5
- import follow-up polish, if needed
## PR 5: Network and gRPC Privileged Boundary Pass
Out of scope:
- generic OAuth cleanup
- gRPC proto temp files
- plugin loading and templating
## Candidate: gRPC Proto Asset Boundary
Status: candidate
Purpose:
- Move filesystem and privileged request execution concerns behind explicit APIs instead of direct renderer imports.
- Isolate the gRPC proto file preparation flow behind a privileged boundary without expanding the PR into broader network or response persistence cleanup.
Single feature scope:
- Preparing proto directories and temp proto files for gRPC request execution.
Primary files:
- `src/network/network.ts`
- `src/network/grpc/proto-directory-loader.tsx`
- `src/network/grpc/write-proto-file.ts`
- `src/network/o-auth-1/get-token.ts`
- `src/network/o-auth-2/get-token.ts`
- `src/network/o-auth-2/utils.ts`
- `src/network/url-matches-cert-host.ts`
- `src/models/helpers/response-operations.ts`
- any minimal preload or IPC additions needed to support the proto write path
Likely implementation:
Implementation notes:
- Separate pure parsing and formatting code from file/crypto operations.
- Push file reads, writes, temp file creation, and request execution details behind `window.main` or a dedicated network bridge.
- Move proto temp-file creation and path work to main.
- Keep request assembly in the renderer, but remove direct `fs`, `os`, and `path` ownership from the gRPC preparation path.
- Do not mix in generic request execution, OAuth, or response archival work.
Expected risk: high
Expected risk: medium
Quick win: yes
Suggested reviewers:
@@ -289,79 +407,34 @@ Baseline entries to remove:
- `src/network/grpc/write-proto-file.ts -> fs`
- `src/network/grpc/write-proto-file.ts -> os`
- `src/network/grpc/write-proto-file.ts -> path`
- `src/network/network.ts -> fs`
- `src/network/network.ts -> path`
- `src/network/o-auth-1/get-token.ts -> crypto`
- `src/network/o-auth-2/get-token.ts -> crypto`
- `src/network/o-auth-2/get-token.ts -> querystring`
- `src/network/o-auth-2/utils.ts -> crypto`
- `src/network/url-matches-cert-host.ts -> url`
- `src/models/helpers/response-operations.ts -> fs`
- `src/models/helpers/response-operations.ts -> zlib`
Dependencies:
- recommended after PR 4
- Sync storage boundary foundation patterns should be available first
Concurrent with:
Test automation plan:
- PR 6
- PR 7
- Extend or relocate unit coverage for `write-proto-file` so the privileged file-write path stays directly tested.
- Add a contract test proving the renderer gRPC flow uses the new bridge instead of direct Node imports.
- Keep one gRPC-focused smoke or integration path that confirms proto-backed requests still resolve correctly.
## PR 6: Sync and Storage Boundary Pass
Out of scope:
- OAuth token helpers
- generic network file IO
- response body archival helpers
## Candidate: Plugin Discovery Boundary
Status: candidate
Purpose:
- Isolate local project storage, sync, compression, and file rename flows behind explicit privileged services.
- Move plugin discovery and plugin file access onto a privileged boundary without coupling the work to templating bootstrap or unrelated runtime refactors.
Primary files:
Single feature scope:
- `src/sync/store/drivers/file-system-driver.ts`
- `src/sync/store/drivers/graceful-rename.ts`
- `src/sync/store/hooks/compress.ts`
- `src/sync/store/index.ts`
- `src/sync/vcs/util.ts`
- `src/sync/vcs/vcs.ts`
- `src/script-executor.ts`
Likely implementation:
- Move filesystem operations and compression into a storage backend boundary.
- Keep renderer-facing sync orchestration on the safe side of that boundary.
Expected risk: high
Suggested reviewers:
- Sync/storage
- Electron/runtime
Baseline entries to remove:
- `src/script-executor.ts -> fs/promises`
- `src/sync/store/drivers/file-system-driver.ts -> fs/promises`
- `src/sync/store/drivers/file-system-driver.ts -> path`
- `src/sync/store/drivers/graceful-rename.ts -> fs/promises`
- `src/sync/store/hooks/compress.ts -> zlib`
- `src/sync/store/index.ts -> path`
- `src/sync/vcs/util.ts -> crypto`
- `src/sync/vcs/vcs.ts -> crypto`
- `src/sync/vcs/vcs.ts -> path`
Dependencies:
- none, though shared boundary patterns from PR 4 help
Concurrent with:
- PR 5
- PR 7
## PR 7: Plugin and Templating Boundary Pass
Purpose:
- Redesign plugin and templating runtime boundaries so privileged module loading and filesystem traversal do not live in renderer-reachable code.
- Plugin discovery, metadata loading, and plugin file access.
Primary files:
@@ -369,16 +442,18 @@ Primary files:
- `src/plugins/create.ts`
- `src/plugins/index.ts`
- `src/utils/plugin.ts`
- `src/templating/base-extension.ts`
- any preload or IPC additions needed for plugin discovery and metadata handoff
Likely implementation:
Implementation notes:
- Decide what plugin discovery and loading must remain privileged.
- Extract pure metadata and UI-facing types from runtime loading logic.
- Push filesystem-backed plugin operations behind explicit APIs.
- Keep plugin metadata and UI-facing types renderer-safe.
- Move plugin discovery and plugin file traversal to main.
- Avoid adding templating bootstrap, plugin install, package management, or unrelated runtime redesign to this candidate.
Expected risk: high
Quick win: no
Suggested reviewers:
- Plugins/templating
@@ -393,19 +468,237 @@ Baseline entries to remove:
- `src/plugins/index.ts -> path`
- `src/utils/plugin.ts -> fs`
- `src/utils/plugin.ts -> path`
Dependencies:
- Sync storage boundary foundation should land first
- Prefer after import and gRPC candidate patterns are established so plugin work can reuse the same bridge shape
Test automation plan:
- Extend plugin discovery or plugin creation unit tests around the new privileged entrypoints.
- Add a renderer contract test for plugin metadata loading so the UI path remains fast and explicit.
- Keep one plugin-focused smoke path to prove discovery still works end to end.
Out of scope:
- templating bootstrap
- package installation and lifecycle management
- generic network cleanup
- import persistence changes
## Candidate: Templating Bootstrap Boundary
Status: candidate
Purpose:
- Move templating bootstrap helpers off the renderer path without coupling the work to plugin discovery or broader templating redesign.
Single feature scope:
- Templating startup and privileged helper access.
Primary files:
- `src/templating/base-extension.ts`
- any preload or IPC additions needed for templating bootstrap helpers
Implementation notes:
- Keep template evaluation behavior unchanged while moving privileged helper access behind explicit main-process entrypoints.
- Avoid folding plugin discovery or plugin package management into this candidate.
- Prefer to reuse any plugin-side metadata bridge rather than inventing parallel surfaces if the contracts line up cleanly.
Expected risk: medium
Quick win: maybe
Suggested reviewers:
- Plugins/templating
- Electron/runtime
Baseline entries to remove:
- `src/templating/base-extension.ts -> crypto`
- `src/templating/base-extension.ts -> os`
Dependencies:
- none, but coordinate if PR 5 touches shared templating execution
- Prefer after plugin discovery boundary if templating still relies on shared plugin bootstrap behavior
Concurrent with:
Test automation plan:
- PR 5
- PR 6
- Extend templating-focused unit tests around the new privileged entrypoints.
- Add a renderer contract test for templating bootstrap helpers.
- Keep one templating smoke path to prove the startup flow still works end to end.
## PR 8: Baseline Ratchet Follow-Ups
Out of scope:
- plugin discovery
- package installation and lifecycle management
- generic network cleanup
## Candidate: OAuth Token Crypto Cleanup
Status: candidate
Purpose:
- Remove isolated OAuth crypto usage from renderer-reachable code without pulling in unrelated network or storage work.
Single feature scope:
- OAuth token helper crypto operations.
Primary files:
- `src/network/o-auth-1/get-token.ts`
- `src/network/o-auth-2/get-token.ts`
- `src/network/o-auth-2/utils.ts`
Implementation notes:
- Treat this as a small privileged-helper cleanup, not a broad request-execution refactor.
- Prefer a narrow bridge for hashing, PKCE, and token helper crypto rather than a generic network bridge.
- Keep request sending and response handling out of scope.
Expected risk: low to medium
Quick win: yes
Suggested reviewers:
- Network/gRPC
- Electron/runtime
Baseline entries to remove:
- `src/network/o-auth-1/get-token.ts -> crypto`
- `src/network/o-auth-2/get-token.ts -> crypto`
- `src/network/o-auth-2/utils.ts -> crypto`
Dependencies:
- none
Test automation plan:
- Extend OAuth unit tests around PKCE and token helper behavior.
- Add a renderer contract test for any new crypto bridge surface.
- Keep this candidate free of smoke-test expansion unless an existing auth smoke path already covers the flow.
Out of scope:
- generic request execution
- response archival
- import or sync persistence
## Candidate: Response Archival and Compression Boundary
Status: candidate
Purpose:
- Isolate response body archival and compression so renderer-safe helpers stop owning file and compression concerns.
Single feature scope:
- Response archival, file writes, and compression helpers.
Primary files:
- `src/models/helpers/response-operations.ts`
Implementation notes:
- Split pure response metadata shaping from privileged file-write and compression behavior.
- Avoid bundling this candidate with sync compression just because both touch compression.
- Reuse existing preload APIs if they are already close to the needed shape.
Expected risk: medium
Quick win: maybe
Suggested reviewers:
- Network/gRPC
- Electron/runtime
Baseline entries to remove:
- `src/models/helpers/response-operations.ts -> fs`
- `src/models/helpers/response-operations.ts -> zlib`
Dependencies:
- none, though sync-storage patterns may provide a good template
Test automation plan:
- Extend unit tests for response archival helpers around the boundary split.
- Add a renderer contract test for any file-write bridge used by response exports.
- Keep one focused integration path for exporting or persisting a response body.
Out of scope:
- sync store compression
- generic request execution
- plugin or templating work
## Candidate: Script Executor Boundary
Status: candidate
Purpose:
- Move `script-executor.ts` file access behind a privileged boundary without broadening the work into general scripting or plugin changes.
Single feature scope:
- Script execution file access.
Primary files:
- `src/script-executor.ts`
Implementation notes:
- Keep the candidate narrowly focused on the file-access boundary.
- Do not combine this with plugin loading or broader execution-runtime redesign.
- Reuse existing bridge patterns from sync or response file operations where possible.
Expected risk: medium
Quick win: maybe
Suggested reviewers:
- Electron/runtime
- Sync/storage
Baseline entries to remove:
- `src/script-executor.ts -> fs/promises`
Dependencies:
- none
Test automation plan:
- Extend targeted script-executor tests around the file-access boundary.
- Add a renderer contract test if a new bridge surface is introduced.
- Avoid growing this into a smoke-heavy candidate unless an existing script-execution smoke path already exists.
Out of scope:
- plugin runtime redesign
- sync store persistence
- response archival
## Candidate: Baseline Ratchet Follow-Ups
Purpose:
@@ -416,7 +709,7 @@ Primary files:
- `config/renderer-node-import-baseline.json`
- `.reports/renderer-node-imports.json`
Likely implementation:
Implementation notes:
- Re-run `npm run update:renderer-node-import-baseline` after each offender-removing PR.
- Confirm the baseline only drops entries already removed from the analyzer output.
@@ -435,29 +728,35 @@ Concurrent with:
- usually folded into the offender-removing PR rather than done separately
## Suggested Merge Order
## Suggested Candidate Sequencing
1. PR 1
2. PR 2
3. PR 3
4. PR 4
5. PR 5
6. PR 6
7. PR 7
1. Sync storage boundary foundation
2. Import parsing and persistence boundary
3. gRPC proto asset boundary
4. Plugin discovery boundary
5. Templating bootstrap boundary
6. OAuth token crypto cleanup
7. Response archival and compression boundary
8. Script executor boundary
Notes:
- PR 1, PR 2, and PR 3 should produce the fastest visible reduction in route and helper debt.
- PR 4 is the best candidate for early architectural work because it reduces follow-on churn in PR 5 through PR 7.
- PR 5 through PR 7 are intentionally split by subsystem so they can be assigned to different owners.
- Sync storage remains the dependency-clearing candidate because import should not move until sync storage is on the privileged side of the boundary.
- gRPC proto asset work and OAuth token crypto cleanup look like the clearest quick-win candidates.
- Plugin discovery and templating are intentionally separate candidates. If they naturally converge later, that should be a conscious decision rather than the default plan.
- If a candidate starts collecting unrelated offenders, split it again instead of broadening it.
## Ownership Template
For each PR, capture the following in the PR description:
- Purpose
- Single feature scope
- Implementation notes
- Files in scope
- Baseline entries expected to be removed
- Test automation plan
- Any new preload or IPC surface added
- Any deliberate deferrals to later PRs
@@ -469,3 +768,4 @@ This migration is complete when:
2. The baseline file is empty or reduced to intentionally permitted entries.
3. Lint restrictions can be tightened by removing temporary offender exclusions.
4. The main BrowserWindow runs with `nodeIntegration: false` without renderer regressions.
5. Security audit of changes is complete, including the writeResponseBodyToFile preload function.

View File

@@ -16,74 +16,6 @@
"importer": "../insomnia-testing/src/run/run.ts",
"builtin": "path"
},
{
"importer": "src/common/misc.ts",
"builtin": "path"
},
{
"importer": "src/common/misc.ts",
"builtin": "zlib"
},
{
"importer": "src/common/significant-diff-detection.ts",
"builtin": "path"
},
{
"importer": "src/main/importers/importers/curl.ts",
"builtin": "url"
},
{
"importer": "src/main/importers/importers/openapi-3.ts",
"builtin": "crypto"
},
{
"importer": "src/main/importers/importers/openapi-3.ts",
"builtin": "url"
},
{
"importer": "src/main/importers/importers/swagger-2.ts",
"builtin": "crypto"
},
{
"importer": "src/main/network/libcurl-promise.ts",
"builtin": "fs"
},
{
"importer": "src/main/network/libcurl-promise.ts",
"builtin": "path"
},
{
"importer": "src/main/network/libcurl-promise.ts",
"builtin": "url"
},
{
"importer": "src/main/network/multipart.ts",
"builtin": "fs"
},
{
"importer": "src/main/network/multipart.ts",
"builtin": "os"
},
{
"importer": "src/main/network/multipart.ts",
"builtin": "path"
},
{
"importer": "src/main/network/parse-header-strings.ts",
"builtin": "url"
},
{
"importer": "src/main/secure-read-file.ts",
"builtin": "fs"
},
{
"importer": "src/main/secure-read-file.ts",
"builtin": "os"
},
{
"importer": "src/main/secure-read-file.ts",
"builtin": "path"
},
{
"importer": "src/models/helpers/response-operations.ts",
"builtin": "fs"
@@ -92,26 +24,6 @@
"importer": "src/models/helpers/response-operations.ts",
"builtin": "zlib"
},
{
"importer": "src/network/grpc/proto-directory-loader.tsx",
"builtin": "fs"
},
{
"importer": "src/network/grpc/proto-directory-loader.tsx",
"builtin": "path"
},
{
"importer": "src/network/grpc/write-proto-file.ts",
"builtin": "fs"
},
{
"importer": "src/network/grpc/write-proto-file.ts",
"builtin": "os"
},
{
"importer": "src/network/grpc/write-proto-file.ts",
"builtin": "path"
},
{
"importer": "src/network/network.ts",
"builtin": "fs"
@@ -120,22 +32,6 @@
"importer": "src/network/network.ts",
"builtin": "path"
},
{
"importer": "src/network/o-auth-1/get-token.ts",
"builtin": "crypto"
},
{
"importer": "src/network/o-auth-2/get-token.ts",
"builtin": "crypto"
},
{
"importer": "src/network/o-auth-2/get-token.ts",
"builtin": "querystring"
},
{
"importer": "src/network/o-auth-2/utils.ts",
"builtin": "crypto"
},
{
"importer": "src/network/url-matches-cert-host.ts",
"builtin": "url"
@@ -144,6 +40,10 @@
"importer": "src/plugins/context/response.ts",
"builtin": "fs"
},
{
"importer": "src/plugins/context/response.ts",
"builtin": "zlib"
},
{
"importer": "src/plugins/create.ts",
"builtin": "fs"
@@ -160,57 +60,21 @@
"importer": "src/plugins/index.ts",
"builtin": "path"
},
{
"importer": "src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx",
"builtin": "fs"
},
{
"importer": "src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx",
"builtin": "path"
},
{
"importer": "src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx",
"builtin": "fs"
},
{
"importer": "src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx",
"builtin": "path"
},
{
"importer": "src/script-executor.ts",
"builtin": "fs/promises"
},
{
"importer": "src/sync/store/drivers/file-system-driver.ts",
"builtin": "fs/promises"
"importer": "src/scripting/require-interceptor.ts",
"builtin": "buffer"
},
{
"importer": "src/sync/store/drivers/file-system-driver.ts",
"builtin": "path"
"importer": "src/scripting/require-interceptor.ts",
"builtin": "timers"
},
{
"importer": "src/sync/store/drivers/graceful-rename.ts",
"builtin": "fs/promises"
},
{
"importer": "src/sync/store/hooks/compress.ts",
"builtin": "zlib"
},
{
"importer": "src/sync/store/index.ts",
"builtin": "path"
},
{
"importer": "src/sync/vcs/util.ts",
"builtin": "crypto"
},
{
"importer": "src/sync/vcs/vcs.ts",
"builtin": "crypto"
},
{
"importer": "src/sync/vcs/vcs.ts",
"builtin": "path"
"importer": "src/scripting/require-interceptor.ts",
"builtin": "util"
},
{
"importer": "src/templating/base-extension.ts",
@@ -227,10 +91,6 @@
{
"importer": "src/utils/plugin.ts",
"builtin": "path"
},
{
"importer": "src/utils/url/querystring.ts",
"builtin": "url"
}
]
}

View File

@@ -43,7 +43,6 @@
"dependencies": {
"@apideck/better-ajv-errors": "^0.3.6",
"@apidevtools/swagger-parser": "10.1.1",
"ajv": "^8.17.1",
"@bufbuild/protobuf": "^1.10.0",
"@connectrpc/connect": "^1.6.1",
"@connectrpc/connect-node": "^1.6.1",
@@ -54,7 +53,7 @@
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^3.0.2",
"@getinsomnia/node-libcurl": "3.2.1",
"@getinsomnia/node-libcurl": "3.2.2",
"@getinsomnia/srp-js": "1.0.0-alpha.1",
"@grpc/grpc-js": "^1.13.3",
"@grpc/proto-loader": "^0.7.13",
@@ -67,13 +66,16 @@
"@seald-io/nedb": "^4.1.1",
"@segment/analytics-node": "2.2.1",
"@sentry/electron": "^6.5.0",
"@stoplight/spectral-core": "^1.20.0",
"@stoplight/spectral-core": "^1.22.0",
"@stoplight/spectral-formats": "^1.8.2",
"@stoplight/spectral-ruleset-bundler": "1.6.3",
"@stoplight/spectral-rulesets": "^1.22.0",
"@stoplight/spectral-ruleset-bundler": "1.7.0",
"@stoplight/spectral-rulesets": "^1.22.1",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-virtual": "3.13.12",
"@xmldom/xmldom": "^0.9.8",
"acorn": "^8.16.0",
"acorn-walk": "^8.3.5",
"ajv": "^8.17.1",
"apiconnect-wsdl": "2.0.36",
"aws4": "^1.13.2",
"blakejs": "^1.2.1",
@@ -92,10 +94,11 @@
"decompress": "^4.2.1",
"deep-equal": "2.2.3",
"diff-match-patch-ts": "^0.6.0",
"dompurify": "^3.2.5",
"dompurify": "^3.4.1",
"electron-context-menu": "^3.6.1",
"electron-updater": "^6.6.2",
"fastq": "^1.19.1",
"fflate": "^0.8.2",
"fuzzysort": "^1.9.0",
"graphql": "^16.10.0",
"graphql-ws": "^5.16.2",
@@ -144,7 +147,7 @@
"tinykeys": "^3.0.0",
"tough-cookie": "^4.1.4",
"tweetnacl": "^1.0.3",
"undici": "^7.16.0",
"undici": "^7.25.0",
"uuid": "^9.0.1",
"vkbeautify": "^0.99.3",
"ws": "^8.18.1",
@@ -207,7 +210,7 @@
"vite": "^7.1.3"
},
"optionalDependencies": {
"@kong/insomnia-plugin-ai": "^1.0.9",
"@kong/insomnia-plugin-ai": "^1.0.11",
"@kong/insomnia-plugin-external-vault": "0.1.4-dev.20251224090833"
},
"dev": {

View File

@@ -5,8 +5,8 @@ import { servicesNodeImpl } from '~/insomnia-data/node';
import { nodeLibcurlMock } from './src/__mocks__/@getinsomnia/node-libcurl';
import { electronMock } from './src/__mocks__/electron';
import { v4Mock } from './src/__mocks__/uuid';
import { mainDatabase } from './src/main/database.main';
import { v4Mock } from './src/models/__mocks__/uuid';
await initDatabase(mainDatabase, { inMemoryOnly: true }, true);
await initServices(servicesNodeImpl);

View File

@@ -48,12 +48,6 @@ vi.mock('electron', () => ({
},
}));
vi.mock('../models', () => ({
settings: {
get: vi.fn(() => Promise.resolve({})),
},
}));
// Mock the entire install-plugin module
vi.mock('../main/install-plugin', async () => {
const actual = await vi.importActual('../main/install-plugin');

View File

@@ -0,0 +1,240 @@
import * as insomniaApi from 'insomnia-api';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as crypt from '../crypt';
import {
absorbKey,
getCurrentSessionId,
getPrivateKey,
getUserSession,
isLoggedIn,
logout,
setSessionData,
} from '../session';
vi.mock('insomnia-api', () => ({
getUserProfile: vi.fn(),
getEncryptionKeys: vi.fn(),
logout: vi.fn(),
}));
vi.mock('../crypt', () => ({
decryptAES: vi.fn(),
}));
interface MockWindowMain {
loginStateChange: ReturnType<typeof vi.fn>;
}
const getWindowMain = () => (window as unknown as { main: MockWindowMain }).main;
// Fixtures
const SESSION_ID = 'test-session-id';
const RAW_KEY = 'raw-key-material';
const MOCK_PUBLIC_KEY = { kty: 'RSA', n: 'abc', e: 'AQAB' };
const MOCK_ENC_PRIVATE_KEY = { iv: 'iv1', t: 't1', d: 'd1', ad: 'ad1' };
const MOCK_ENC_SYMMETRIC_KEY = { iv: 'iv2', t: 't2', d: 'd2', ad: 'ad2' };
const MOCK_SYMMETRIC_KEY = { kty: 'oct', k: 'sym-key' };
const mockEncryptionKeys = {
public_key: JSON.stringify(MOCK_PUBLIC_KEY),
enc_private_key: JSON.stringify(MOCK_ENC_PRIVATE_KEY),
enc_symmetric_key: JSON.stringify(MOCK_ENC_SYMMETRIC_KEY),
salt_enc: 'salt',
enc_driver_key: '',
};
const mockUserProfile = {
id: 'account-123',
created_at: new Date('2026-01-01T00:00:00Z'),
email: 'test@example.com',
first_name: 'Jane',
last_name: 'Doe',
picture: '',
emails: [],
encryption_enabled: false,
is_externally_provisioned: false,
};
beforeEach(() => {
vi.mocked(insomniaApi.getUserProfile).mockResolvedValue(mockUserProfile);
vi.mocked(insomniaApi.getEncryptionKeys).mockResolvedValue(mockEncryptionKeys);
vi.mocked(crypt.decryptAES).mockReturnValue(JSON.stringify(MOCK_SYMMETRIC_KEY));
vi.stubGlobal('window', { main: { loginStateChange: vi.fn() } });
});
describe('absorbKey', () => {
it('fetches profile and encryption keys with the provided sessionId', async () => {
await absorbKey(SESSION_ID, RAW_KEY);
expect(insomniaApi.getUserProfile).toHaveBeenCalledWith({ sessionId: SESSION_ID });
expect(insomniaApi.getEncryptionKeys).toHaveBeenCalledWith({ sessionId: SESSION_ID });
});
it('decrypts the symmetric key using the provided raw key and encSymmetricKey', async () => {
await absorbKey(SESSION_ID, RAW_KEY);
expect(crypt.decryptAES).toHaveBeenCalledWith(RAW_KEY, MOCK_ENC_SYMMETRIC_KEY);
});
it('stores session data with mapped fields from profile and encryption keys', async () => {
await absorbKey(SESSION_ID, RAW_KEY);
const session = await getUserSession();
expect(session.id).toBe(SESSION_ID);
expect(session.accountId).toBe(mockUserProfile.id);
expect(session.email).toBe(mockUserProfile.email);
expect(session.firstName).toBe(mockUserProfile.first_name);
expect(session.lastName).toBe(mockUserProfile.last_name);
expect(session.symmetricKey).toEqual(MOCK_SYMMETRIC_KEY);
expect(session.publicKey).toEqual(MOCK_PUBLIC_KEY);
expect(session.encPrivateKey).toEqual(MOCK_ENC_PRIVATE_KEY);
});
it('triggers loginStateChange after storing session', async () => {
await absorbKey(SESSION_ID, RAW_KEY);
expect(getWindowMain().loginStateChange).toHaveBeenCalledOnce();
});
it('falls back to current session id when none is provided', async () => {
// First establish a session so getCurrentSessionId returns something
await setSessionData(
SESSION_ID,
'acct',
'A',
'B',
'a@b.com',
{} as JsonWebKey,
{} as JsonWebKey,
{} as crypt.AESMessage,
);
await absorbKey('', RAW_KEY);
expect(insomniaApi.getUserProfile).toHaveBeenCalledWith({ sessionId: SESSION_ID });
expect(insomniaApi.getEncryptionKeys).toHaveBeenCalledWith({ sessionId: SESSION_ID });
});
});
describe('getPrivateKey', () => {
it('decrypts and returns the private key from session, and throws when keys are missing', async () => {
const mockPrivateKey = { kty: 'RSA', d: 'private' };
vi.mocked(crypt.decryptAES).mockReturnValue(JSON.stringify(mockPrivateKey));
await setSessionData(
SESSION_ID,
'acct',
'A',
'B',
'a@b.com',
MOCK_SYMMETRIC_KEY as JsonWebKey,
MOCK_PUBLIC_KEY as JsonWebKey,
MOCK_ENC_PRIVATE_KEY as crypt.AESMessage,
);
const privateKey = await getPrivateKey();
expect(crypt.decryptAES).toHaveBeenCalledWith(MOCK_SYMMETRIC_KEY, MOCK_ENC_PRIVATE_KEY);
expect(privateKey).toEqual(mockPrivateKey);
await setSessionData(
'',
'',
'',
'',
'',
null as unknown as JsonWebKey,
{} as JsonWebKey,
null as unknown as crypt.AESMessage,
);
await expect(getPrivateKey()).rejects.toThrow("Can't get private key: session is missing keys.");
});
});
describe('isLoggedIn', () => {
it('returns true when a session id exists', async () => {
await setSessionData(
SESSION_ID,
'acct',
'A',
'B',
'a@b.com',
{} as JsonWebKey,
{} as JsonWebKey,
{} as crypt.AESMessage,
);
expect(await isLoggedIn()).toBe(true);
});
});
describe('logout', () => {
it('calls the logout API with the current session id', async () => {
await setSessionData(
SESSION_ID,
'acct',
'A',
'B',
'a@b.com',
{} as JsonWebKey,
{} as JsonWebKey,
{} as crypt.AESMessage,
);
await logout();
expect(insomniaApi.logout).toHaveBeenCalledWith({ sessionId: SESSION_ID });
});
it('triggers loginStateChange', async () => {
await setSessionData(
SESSION_ID,
'acct',
'A',
'B',
'a@b.com',
{} as JsonWebKey,
{} as JsonWebKey,
{} as crypt.AESMessage,
);
await logout();
expect(getWindowMain().loginStateChange).toHaveBeenCalledOnce();
});
it('does not throw if the API call fails', async () => {
vi.mocked(insomniaApi.logout).mockRejectedValue(new Error('network error'));
await setSessionData(
SESSION_ID,
'acct',
'A',
'B',
'a@b.com',
{} as JsonWebKey,
{} as JsonWebKey,
{} as crypt.AESMessage,
);
await expect(logout()).resolves.not.toThrow();
});
});
describe('getCurrentSessionId', () => {
it('returns the current session id', async () => {
await setSessionData(
SESSION_ID,
'acct',
'A',
'B',
'a@b.com',
{} as JsonWebKey,
{} as JsonWebKey,
{} as crypt.AESMessage,
);
expect(await getCurrentSessionId()).toBe(SESSION_ID);
});
});

View File

@@ -232,7 +232,7 @@ export async function generateAES256Key() {
function _hexToB64Url(h: string) {
const bytes = forge.util.hexToBytes(h);
return window.btoa(bytes).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
return btoa(bytes).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
function _b64UrlToBigInt(s: string) {
@@ -243,5 +243,5 @@ function _b64UrlToBigInt(s: string) {
function _b64UrlToHex(s: string) {
const b64 = s.replace(/-/g, '+').replace(/_/g, '/');
return forge.util.bytesToHex(window.atob(b64));
return forge.util.bytesToHex(atob(b64));
}

View File

@@ -1,4 +1,4 @@
import { logout as logoutAPI, whoami } from 'insomnia-api';
import { getEncryptionKeys, getUserProfile, logout as logoutAPI } from 'insomnia-api';
import type { GitRepository, Project, WorkspaceMeta } from '~/insomnia-data';
import { models, services } from '~/insomnia-data';
@@ -21,9 +21,17 @@ export interface SessionData {
/** Creates a session from a sessionId and derived symmetric key. */
export async function absorbKey(sessionId: string, key: string) {
// Get and store some extra info (salts and keys)
const { publicKey, encPrivateKey, encSymmetricKey, email, accountId, firstName, lastName } = await whoami({
sessionId: sessionId || (await getCurrentSessionId()),
});
const sessionIdResolved = sessionId || (await getCurrentSessionId());
const [profile, keys] = await Promise.all([
getUserProfile({ sessionId: sessionIdResolved }),
getEncryptionKeys({ sessionId: sessionIdResolved }),
]);
const {
public_key: publicKey,
enc_private_key: encPrivateKey,
enc_symmetric_key: encSymmetricKey,
} = keys;
const { email, id: accountId, first_name: firstName, last_name: lastName } = profile;
const symmetricKeyStr = crypt.decryptAES(key, JSON.parse(encSymmetricKey));
// Store the information for later
@@ -217,7 +225,8 @@ async function _removeAllCredentials() {
*
*/
async function _removeGitRepository(repo: GitRepository) {
const projects = await database.find<Project>(models.project.type, { gitRepositoryId: repo._id });
const queryIds = models.project.getQueryableGitRepositoryIds(repo._id);
const projects = await database.find<Project>(models.project.type, { gitRepositoryId: { $in: queryIds } });
for (const p of projects) {
await services.project.update(p, { gitRepositoryId: models.project.EMPTY_GIT_PROJECT_ID });
}

View File

@@ -9,6 +9,7 @@ interface Props {
onClose?: () => void;
title?: React.ReactNode;
closable?: boolean;
isDismissable?: boolean;
className?: string;
}
@@ -18,6 +19,7 @@ export const Modal: React.FC<React.PropsWithChildren<Props>> = ({
className,
title,
closable,
isDismissable,
children,
}) => {
return (
@@ -26,7 +28,7 @@ export const Modal: React.FC<React.PropsWithChildren<Props>> = ({
onOpenChange={isOpen => {
!isOpen && onClose?.();
}}
isDismissable
isDismissable={isDismissable}
className="fixed top-0 left-0 z-10 flex h-(--visual-viewport-height) w-full items-center justify-center bg-black/30"
>
<RAModal

View File

@@ -1,4 +1,7 @@
import { type BaseModel, request, requestGroup, workspace } from '../../models';
import type { BaseModel } from '~/insomnia-data';
import { models } from '~/insomnia-data';
const { workspace, requestGroup, request } = models;
export const data: Record<string, Partial<BaseModel>[]> = {
[workspace.type]: [

View File

@@ -108,4 +108,13 @@ describe('Test electron storage()', () => {
expect(fs.readFileSync(path.join(basePath, 'foo'), 'utf8')).toEqual('"bar3"');
expect(fs.readFileSync(path.join(basePath, 'another'), 'utf8')).toEqual('10');
});
it.each(['', '.', '..', 'foo/bar', 'foo\\bar', 'foo\0bar'])('rejects invalid key %j', key => {
const basePath = `/tmp/insomnia-electronstorage-${Math.random()}`;
const electronStorage = new ElectronStorage(basePath);
expect(() => electronStorage.getItem(key)).toThrowError('Invalid electron storage key');
expect(() => electronStorage.setItem(key, 'value')).toThrowError('Invalid electron storage key');
expect(() => electronStorage.deleteItem(key)).toThrowError('Invalid electron storage key');
});
});

View File

@@ -3,10 +3,9 @@ import path from 'node:path';
import { beforeEach, describe, expect, it } from 'vitest';
import type { Cookie, Request, Response } from '~/insomnia-data';
import { services } from '~/insomnia-data';
import { models, services } from '~/insomnia-data';
import { database as db } from '../../common/database';
import * as models from '../../models';
import { exportHar, exportHarResponse, exportHarWithRequest } from '../har';
import { getRenderedRequestAndContext } from '../render';

View File

@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { parse } from 'yaml';
import { EnvironmentKvPairDataType, EnvironmentType, services } from '~/insomnia-data';
@@ -172,6 +172,36 @@ describe('importRaw()', () => {
});
});
it('should call syncNewWorkspaceIfNeeded after importing to a new workspace', async () => {
const fixturePath = path.join(__dirname, '..', '__fixtures__', 'curl', 'complex-input.sh');
const content = fs.readFileSync(fixturePath, 'utf8').toString();
const projectToImportTo = await services.project.create();
const syncNewWorkspaceIfNeeded = vi.fn();
const scanResult = await importUtil.scanResources([
{
contentStr: content,
},
]);
expect(scanResult[0].type?.id).toBe('curl');
expect(scanResult[0].errors.length).toBe(0);
await importUtil.importResourcesToProject({
projectId: projectToImportTo._id,
syncNewWorkspaceIfNeeded,
});
expect(syncNewWorkspaceIfNeeded).toHaveBeenCalledTimes(1);
expect(syncNewWorkspaceIfNeeded).toHaveBeenCalledWith(
expect.objectContaining({
parentId: projectToImportTo._id,
scope: 'collection',
}),
);
});
it('should import a curl request to an existing workspace', async () => {
const fixturePath = path.join(__dirname, '..', '__fixtures__', 'curl', 'complex-input.sh');
const content = fs.readFileSync(fixturePath, 'utf8').toString();

View File

@@ -1,6 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import { chunkArray } from '../../sync/vcs/vcs';
import {
debounce,
filterHeaders,
@@ -204,33 +203,6 @@ describe('fuzzyMatchAll()', () => {
expect(fuzzyMatchAll('wrong this ou', ['testing', 'this', 'out'])).toEqual(null);
});
});
describe('chunkArray()', () => {
it('works with exact divisor', () => {
const chunks = chunkArray([1, 2, 3, 4, 5, 6], 3);
expect(chunks).toEqual([
[1, 2, 3],
[4, 5, 6],
]);
});
it('works with weird divisor', () => {
const chunks = chunkArray([1, 2, 3, 4, 5, 6], 4);
expect(chunks).toEqual([
[1, 2, 3, 4],
[5, 6],
]);
});
it('works with empty', () => {
const chunks = chunkArray([], 4);
expect(chunks).toEqual([]);
});
it('works with less than one chunk', () => {
const chunks = chunkArray([1, 2], 4);
expect(chunks).toEqual([[1, 2]]);
});
});
describe('isNotNullOrUndefined', () => {
it('should return correctly', () => {

View File

@@ -2,9 +2,9 @@ import { createBuilder } from '@develohpanda/fluent-builder';
import { beforeEach, describe, expect, it } from 'vitest';
import type { Environment, Workspace } from '~/insomnia-data';
import { models, services } from '~/insomnia-data';
import { services } from '~/insomnia-data';
import { environmentModelSchema, requestGroupModelSchema } from '../../models/__schemas__/model-schemas';
import { environmentModelSchema, requestGroupModelSchema } from '../../sync/__schemas__/model-schemas';
import * as renderUtils from '../render';
const envBuilder = createBuilder(environmentModelSchema);

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import { grpcRequest, request, requestGroup } from '../../models';
import { models } from '~/insomnia-data';
import {
METHOD_DELETE,
METHOD_GET,
@@ -18,6 +19,8 @@ import {
sortMethodMap,
} from '../sorting';
const { request, requestGroup, grpcRequest } = models;
describe('Sorting methods', () => {
it('defaults to ascending metaSortKey aka descending but flipped (* -1)', () => {
const unsorted = [

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import * as models from '../../models';
import { models } from '~/insomnia-data';
import { getWorkspaceLabel } from '../get-workspace-label';
import { strings } from '../strings';

View File

@@ -0,0 +1,40 @@
import { gunzipSync, gzipSync, strFromU8, strToU8 } from 'fflate';
const bytesToBase64 = (bytes: Uint8Array) => {
if (typeof Buffer !== 'undefined') {
return Buffer.from(bytes).toString('base64');
}
let binary = '';
for (const byte of bytes) {
binary += String.fromCodePoint(byte);
}
return btoa(binary);
};
const base64ToBytes = (input: string) => {
if (typeof Buffer !== 'undefined') {
return Uint8Array.from(Buffer.from(input, 'base64'));
}
const binary = atob(input);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index++) {
bytes[index] = binary.codePointAt(index)!;
}
return bytes;
};
export function compressObject(obj: any) {
return bytesToBase64(gzipSync(strToU8(JSON.stringify(obj))));
}
export function decompressObject<ObjectType>(input: string | null): ObjectType | null {
if (typeof input !== 'string') {
return null;
}
return JSON.parse(strFromU8(gunzipSync(base64ToBytes(input)))) as ObjectType;
}

View File

@@ -12,7 +12,8 @@ const env = process[ENV];
export const INSOMNIA_GITLAB_REDIRECT_URI = env.INSOMNIA_GITLAB_REDIRECT_URI;
export const INSOMNIA_GITLAB_CLIENT_ID = env.INSOMNIA_GITLAB_CLIENT_ID;
export const INSOMNIA_GITLAB_API_URL = env.INSOMNIA_GITLAB_API_URL;
export const PLAYWRIGHT = env.PLAYWRIGHT;
export const PLAYWRIGHT_TEST = env.PLAYWRIGHT_TEST;
export const OAUTH_WINDOW_SESSION_ID_KEY = 'current-oauth-session-id';
// App Stuff
export const getSkipOnboarding = () => env.INSOMNIA_SKIP_ONBOARDING;
@@ -33,12 +34,12 @@ export const getAppBundlePlugins = () => appConfig.bundlePlugins;
export const getAppEnvironment = () => process.env.INSOMNIA_ENV || 'production';
export const isDevelopment = () => getAppEnvironment() === 'development';
export const getSegmentWriteKey = () =>
appConfig.segmentWriteKeys[isDevelopment() || env.PLAYWRIGHT ? 'development' : 'production'];
appConfig.segmentWriteKeys[isDevelopment() || env.PLAYWRIGHT_TEST ? 'development' : 'production'];
export const getSentryDsn = () => appConfig.sentryDsn;
export const getCioWriteKey = () =>
appConfig.cio[isDevelopment() || env.PLAYWRIGHT ? 'development' : 'production'].writeKey;
appConfig.cio[isDevelopment() || env.PLAYWRIGHT_TEST ? 'development' : 'production'].writeKey;
export const getCioSiteId = () =>
appConfig.cio[isDevelopment() || env.PLAYWRIGHT ? 'development' : 'production'].siteId;
appConfig.cio[isDevelopment() || env.PLAYWRIGHT_TEST ? 'development' : 'production'].siteId;
export const getAppBuildDate = () => new Date(process.env.BUILD_DATE ?? '').toLocaleDateString();
export const getBrowserUserAgent = () =>
@@ -112,6 +113,7 @@ export const getMockServiceBinURL = (mockServer: MockServer, path: string) => {
export const getAIServiceURL = () => env.INSOMNIA_AI_URL || 'https://ai-helper.insomnia.rest';
export const getKonnectApiBaseURL = () => env.KONNECT_API_URL || 'https://global.api.konghq.com';
export const isKonnectSyncEnabled = () => !!env.KONNECT_SYNC_ENABLED;
// App website
export const getAppWebsiteBaseURL = () => env.INSOMNIA_APP_WEBSITE_URL || 'https://app.insomnia.rest';
@@ -256,6 +258,55 @@ export type AuthTypes =
export const HAWK_ALGORITHM_SHA256 = 'sha256';
export const HAWK_ALGORITHM_SHA1 = 'sha1';
//oauth 1
export type OAuth1SignatureMethod = 'HMAC-SHA1' | 'RSA-SHA1' | 'HMAC-SHA256' | 'PLAINTEXT';
export const SIGNATURE_METHOD_HMAC_SHA1: OAuth1SignatureMethod = 'HMAC-SHA1';
export const SIGNATURE_METHOD_HMAC_SHA256: OAuth1SignatureMethod = 'HMAC-SHA256';
export const SIGNATURE_METHOD_RSA_SHA1: OAuth1SignatureMethod = 'RSA-SHA1';
export const SIGNATURE_METHOD_PLAINTEXT: OAuth1SignatureMethod = 'PLAINTEXT';
//oauth 2
export const GRANT_TYPE_AUTHORIZATION_CODE = 'authorization_code';
export const GRANT_TYPE_IMPLICIT = 'implicit';
export const GRANT_TYPE_PASSWORD = 'password';
export const GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials';
export const GRANT_TYPE_REFRESH = 'refresh_token';
export const GRANT_TYPE_MCP_AUTH_FLOW = 'mcp_auth_flow';
export type AuthKeys =
| 'access_token'
| 'id_token'
| 'client_id'
| 'client_secret'
| 'audience'
| 'resource'
| 'code_challenge'
| 'code_challenge_method'
| 'code_verifier'
| 'code'
| 'nonce'
| 'error'
| 'error_description'
| 'error_uri'
| 'expires_in'
| 'grant_type'
| 'password'
| 'redirect_uri'
| 'refresh_token'
| 'response_type'
| 'scope'
| 'state'
| 'token_type'
| 'username'
| 'xError'
| 'xResponseId';
export const PKCE_CHALLENGE_S256 = 'S256';
export const PKCE_CHALLENGE_PLAIN = 'plain';
export type OAuth2AuthorizationStatusType = 'none' | 'getting_code' | 'getting_token';
// json-order constants
export const JSON_ORDER_PREFIX = '&';
export const JSON_ORDER_SEPARATOR = '~|';

View File

@@ -2,13 +2,9 @@ import clone from 'clone';
import type * as Har from 'har-format';
import { Cookie as ToughCookie } from 'tough-cookie';
import type { Environment, Request, RequestGroup, Response, Workspace } from '~/insomnia-data';
import { services } from '~/insomnia-data';
import { getBodyBuffer } from '~/models/helpers/response-operations';
import type { BaseModel, Environment, Request, RequestGroup, Response, Workspace } from '~/insomnia-data';
import { models, services } from '~/insomnia-data';
import type { BaseModel } from '../models';
import * as models from '../models';
import { getAuthHeader } from '../network/authentication';
import * as plugins from '../plugins';
import * as pluginApp from '../plugins/context/app';
import * as pluginRequest from '../plugins/context/request';
@@ -307,6 +303,10 @@ export async function exportHarWithRenderedRequest(renderedRequest: RenderedRequ
// Set auth header if we have it
if (!hasAuthHeader(renderedRequest.headers)) {
const getAuthHeader =
process.type === 'renderer'
? window.main.getAuthHeader
: (await import('../main/network/get-auth-header')).getAuthHeader;
const header = await getAuthHeader(renderedRequest, url);
if (header) {
@@ -403,7 +403,7 @@ function mapCookie(cookie: ToughCookie) {
}
async function getResponseContent(response: Response) {
let body = await getBodyBuffer(response);
let body = await services.helpers.getResponseBodyBuffer(response);
if (body === null) {
body = Buffer.alloc(0);

View File

@@ -2,7 +2,9 @@ import orderedJSON from 'json-order';
import { z, type ZodError } from 'zod/v4';
import type {
AllTypes,
ApiSpec,
BaseModel,
CookieJar,
Environment,
EnvironmentKvPairData,
@@ -16,21 +18,18 @@ import type {
WebSocketRequest,
Workspace,
} from '~/insomnia-data';
import { services } from '~/insomnia-data';
import { insecureReadFile } from '~/main/secure-read-file';
import { models, services } from '~/insomnia-data';
import type { InsomniaImporter } from '../main/importers/convert';
import type { ImportEntry } from '../main/importers/entities';
import { pathWithParamsAsPathParameters } from '../main/importers/importers/openapi-3';
import { id as postmanEnvImporterId } from '../main/importers/importers/postman-env';
import * as models from '../models/index';
import { type AllTypes, type BaseModel, getModel } from '../models/index';
import { invariant } from '../utils/invariant';
import { parseApiSpec, type ParsedApiSpec } from './api-specs';
import { JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from './constants';
import { database as db } from './database';
import { tryImportV5Data } from './insomnia-v5';
import { generateId } from './misc';
import { pathWithParamsAsPathParameters } from './path-with-params';
const { isRequest } = models.request;
const { isApiSpec } = models.apiSpec;
@@ -93,8 +92,12 @@ export async function fetchImportContentFromURI({ uri }: { uri: string }) {
return content;
} else if (uri.match(/^(file):\/\//)) {
const path = uri.replace(/^(file):\/\//, '');
// allow reading the file as it is chosen by user
return insecureReadFile(path);
const readFileProcessFork = async (path: string) =>
process.type === 'renderer'
? window.main.insecureReadFile({ path })
: (await import('../main/secure-read-file')).insecureReadFile(path);
return readFileProcessFork(path);
}
// Treat everything else as raw text
const content = decodeURIComponent(uri);
@@ -204,9 +207,9 @@ export async function scanResources(importEntries: ImportEntry[]): Promise<ScanR
},
};
} else {
const processFork =
const convertProcessFork =
process.type === 'renderer' ? window.main.parseImport : (await import('../main/importers/convert')).convert;
result = (await processFork(importEntry)) as unknown as ConvertResult;
result = (await convertProcessFork(importEntry)) as unknown as ConvertResult;
}
} catch (err: unknown) {
if (v5Error) {
@@ -531,7 +534,7 @@ export const importResourcesToWorkspace = async ({
const subEnvironments = resources.filter(models.environment.isEnvironment).filter(isSubEnvironmentResource) || [];
for (const environment of subEnvironments) {
const model = getModel(environment.type);
const model = models.getModel(environment.type);
model && ResourceIdMap.set(environment._id, generateId(model.prefix));
await services.environment.create({
...environment,
@@ -542,13 +545,13 @@ export const importResourcesToWorkspace = async ({
// Create new ids for each resource below optionalResources
for (const resource of optionalResources) {
const model = getModel(resource.type);
const model = models.getModel(resource.type);
model && ResourceIdMap.set(resource._id, generateId(model.prefix));
}
// Preserve optionalResource relationships
for (const resource of optionalResources) {
const model = getModel(resource.type);
const model = models.getModel(resource.type);
if (model) {
const rewritten = models.rewriteReferences(resource, ResourceIdMap);
const objectToWrite = {
@@ -626,12 +629,12 @@ export const importResourcesToNewWorkspace = async ({
);
for (const resource of resourcesWithoutWorkspaceAndApiSpec) {
const model = getModel(resource.type);
const model = models.getModel(resource.type);
model && ResourceIdMap.set(resource._id, generateId(model.prefix));
}
for (const resource of resourcesWithoutWorkspaceAndApiSpec) {
const model = getModel(resource.type);
const model = models.getModel(resource.type);
if (model) {
const newParentId = ResourceIdMap.get(resource.parentId);

View File

@@ -1,6 +1,6 @@
import { type FetchConfig, ResponseFailError } from 'insomnia-api';
import { getApiBaseURL, getClientString, INSOMNIA_FETCH_TIME_OUT, PLAYWRIGHT } from './constants';
import { getApiBaseURL, getClientString, INSOMNIA_FETCH_TIME_OUT, PLAYWRIGHT_TEST } from './constants';
import { generateId } from './misc';
// Adds headers, retries and opens deep links returned from the api
@@ -27,7 +27,7 @@ export async function insomniaFetch<T = void>({
...(sessionId ? { 'X-Session-Id': sessionId } : {}),
...(data ? { 'Content-Type': 'application/json' } : {}),
...(organizationId ? { 'X-Insomnia-Org-Id': organizationId } : {}),
...(PLAYWRIGHT ? { 'X-Mockbin-Test': 'true' } : {}),
...(PLAYWRIGHT_TEST ? { 'X-Mockbin-Test': 'true' } : {}),
},
...(data ? { body: JSON.stringify(data) } : {}),
signal: AbortSignal.timeout(timeout),

View File

@@ -19,6 +19,7 @@ import { migrateToLatestYaml } from '~/common/insomnia-schema-migrations';
import { INSOMNIA_SCHEMA_VERSION } from '~/common/insomnia-schema-migrations/schema-version';
import type {
ApiSpec,
BaseModel,
CookieJar,
Environment,
EnvironmentKvPairData,
@@ -38,11 +39,10 @@ import type {
Workspace,
WorkspaceScope,
} from '~/insomnia-data';
import { services } from '~/insomnia-data';
import { models, services } from '~/insomnia-data';
import { maskVaultEnvironmentData } from '~/utils/environment-utils';
import { invariant } from '~/utils/invariant';
import * as models from '../models';
import { database } from './database';
import {
type Insomnia_GRPCRequest,
@@ -62,7 +62,7 @@ import {
* Type helper that adds the export type field to any BaseModel
* This is used to ensure all exported models have the correct _type field for v5 format
*/
type WithExportType<T extends models.BaseModel> = T & { _type: AllExportTypes };
type WithExportType<T extends BaseModel> = T & { _type: AllExportTypes };
/**
* Maps request headers from internal format to v5 export format

View File

@@ -1,10 +1,8 @@
import path from 'node:path';
import zlib from 'node:zlib';
import fuzzysort from 'fuzzysort';
import { v4 as uuidv4 } from 'uuid';
import { DEBOUNCE_MILLIS } from './constants';
export { compressObject, decompressObject } from './compression';
const ESCAPE_REGEX_MATCH = /[-[\]/{}()*+?.\\^$|]/g;
@@ -147,20 +145,6 @@ export function fnOrString(v: string | ((...args: any[]) => any), ...args: any[]
return v(...args);
}
export function compressObject(obj: any) {
const compressed = zlib.gzipSync(JSON.stringify(obj));
return compressed.toString('base64');
}
export function decompressObject<ObjectType>(input: string | null): ObjectType | null {
if (typeof input !== 'string') {
return null;
}
const jsonBuffer = zlib.gunzipSync(Buffer.from(input, 'base64'));
return JSON.parse(jsonBuffer.toString('utf8')) as ObjectType;
}
/**
* Escape a dynamic string for use inside of a regular expression
* @param str - string to escape
@@ -266,34 +250,6 @@ export function unescapeForwardSlash(str: string): string {
});
}
export const normalizeFolderPath = (p: string) => {
const normalized = path.normalize(p);
// Preserve filesystem roots as-is (e.g. "/" on POSIX, "C:\" on Windows)
if (normalized === path.parse(normalized).root) {
return normalized;
}
return normalized.replace(/[/\\]+$/, '');
};
export type FolderValidationResult =
| { ok: true; normalizedValue: string }
| { ok: false; error: string };
export function validateFolderInput(input: string, existing: string[]): FolderValidationResult {
const trimmed = input.trim();
if (trimmed === '') {
return { ok: false, error: 'Enter a folder path to add.' };
}
const normalized = normalizeFolderPath(trimmed);
if (trimmed !== normalized) {
return { ok: false, error: `Invalid folder path format. Did you mean "${normalized}"?` };
}
if (existing.some(v => normalizeFolderPath(v) === normalized)) {
return { ok: false, error: 'Duplicate folders are not allowed.' };
}
return { ok: true, normalizedValue: normalized };
}
export const SECURITY_SETTINGS_PATH_LABEL = "Insomnia Preferences → General → Security";
export function cannotAccessPathError(accessingPath: string): string {

View File

@@ -0,0 +1,54 @@
import { getOrganizationStorageRule, type StorageRules } from 'insomnia-api';
import { models, services } from '~/insomnia-data';
import { invariant } from '~/utils/invariant';
const inMemoryStorageRuleCache: Map<string, StorageRules> = new Map<string, StorageRules>();
export const DEFAULT_STORAGE_RULES = {
enableCloudSync: true,
enableLocalVault: true,
enableGitSync: true,
isOverridden: false,
};
export async function fetchAndCacheOrganizationStorageRule(
organizationId: string | undefined,
forceFetch = false,
): Promise<StorageRules> {
invariant(organizationId, 'Organization ID is required');
if (models.organization.isScratchpadOrganizationId(organizationId)) {
return {
enableCloudSync: false,
enableLocalVault: true,
enableGitSync: false,
isOverridden: false,
};
}
if (!forceFetch) {
const storageRules = inMemoryStorageRuleCache.get(organizationId);
if (storageRules) {
return storageRules;
}
}
const { id: sessionId } = await services.userSession.getOrCreate();
return await getOrganizationStorageRule({
organizationId,
sessionId,
}).then(
res => {
if (res) {
inMemoryStorageRuleCache.set(organizationId, res);
}
return res || DEFAULT_STORAGE_RULES;
},
err => {
console.log('[storageRule] Failed to load storage rules', err.message);
return DEFAULT_STORAGE_RULES;
},
);
}

View File

@@ -0,0 +1,3 @@
const VARIABLE_SEARCH_VALUE = /{([^}]+)}/g;
export const pathWithParamsAsPathParameters = (path?: string) => path?.replace(VARIABLE_SEARCH_VALUE, ':$1') ?? '';

View File

@@ -14,7 +14,6 @@ import {
type WorkspaceMeta,
type WorkspaceScope,
} from '~/insomnia-data';
import { VCSInstance } from '~/sync/vcs/insomnia-sync';
export interface InsomniaFile {
id: string;
@@ -34,6 +33,10 @@ export interface InsomniaFile {
hasUncommittedChanges?: boolean;
hasUnpushedChanges?: boolean;
gitFilePath?: string | null;
fileIssue?: {
kind: 'conflict' | 'parse-error';
message: string;
};
}
const lockGenerator = () => {
@@ -208,8 +211,7 @@ export const getAllRemoteBackendProjectsByProjectId = async ({
teamProjectId: string;
organizationId: string;
}) => {
const vcs = VCSInstance();
return vcs.remoteBackendProjects({ teamId: organizationId, teamProjectId });
return window.main.sync.remoteBackendProjects({ teamId: organizationId, teamProjectId });
};
export const getUnsyncedRemoteWorkspaces = (remoteFiles: InsomniaFile[], workspaces: Workspace[]) =>
@@ -230,12 +232,10 @@ export async function getAllRemoteFiles({ projectId, organizationId }: { project
`remoteId: ${remoteId}`,
);
const vcs = VCSInstance();
const [allPulledBackendProjectsForRemoteId, allFetchedRemoteBackendProjectsForRemoteId] = await Promise.all([
vcs.localBackendProjects().then(projects => projects.filter(p => p.id === remoteId)),
window.main.sync.localBackendProjects().then(projects => projects.filter(p => p.id === remoteId)),
// Remote backend projects are fetched from the backend since they are not stored locally
vcs.remoteBackendProjects({ teamId: organizationId, teamProjectId: remoteId }),
window.main.sync.remoteBackendProjects({ teamId: organizationId, teamProjectId: remoteId }),
]);
console.log(
`[getAllRemoteFiles] found allPulledBackendProjectsForRemoteId: ${allPulledBackendProjectsForRemoteId.length} and allFetchedRemoteBackendProjectsForRemoteId: ${allFetchedRemoteBackendProjectsForRemoteId.length} for remoteId: ${remoteId}`,

View File

@@ -13,9 +13,8 @@ import type {
WebSocketRequest,
Workspace,
} from '~/insomnia-data';
import { services } from '~/insomnia-data';
import { models, services } from '~/insomnia-data';
import * as models from '../models';
import { getOrInheritAuthentication, getOrInheritHeaders } from '../network/network';
import * as templating from '../templating';
import { RenderError } from '../templating/render-error';

View File

@@ -1,12 +1,10 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import type { Environment, Settings, UserUploadEnvironment } from '~/insomnia-data';
import type { BaseModel, Environment, Settings, UserUploadEnvironment } from '~/insomnia-data';
import { database, initDatabase, services } from '~/insomnia-data';
import { createNedbDatabase } from '~/insomnia-data/node';
import { getBodyBuffer } from '~/models/helpers/response-operations';
import type { BaseModel } from '../models';
import {
defaultSendActionRuntime,
fetchRequestData,
@@ -140,7 +138,7 @@ export async function getSendRequestCallbackMemDb(
(acc, { name, value }) => ({ ...acc, [name.toLowerCase() || '']: value || '' }),
{},
);
const bodyBuffer = (await getBodyBuffer(res)) as Buffer;
const bodyBuffer = (await services.helpers.getResponseBodyBuffer(res)) as Buffer;
const data = bodyBuffer ? bodyBuffer.toString('utf8') : undefined;
const testResults = [

View File

@@ -163,4 +163,16 @@ export interface Settings {
saveVaultKeyToOSSecretManager: boolean;
vaultSecretCacheDuration: number;
dataFolders: string[];
// AST and shadowing check.
scriptSandboxEnabled: boolean;
// Wraps the user script in 'use strict', preventing accidental globals and making `this` undefined.
scriptStrictModeEnabled: boolean;
// Names of security rules that have been individually disabled.
disabledSecurityRules: string[];
// AST blocked-property names that have been individually disabled.
disabledBlockedProperties: string[];
// AST blocked-root names that have been individually disabled.
disabledBlockedRoots: string[];
/** Custom npm registry URL for plugin installation (e.g., corporate mirror). Empty string uses the default https://registry.npmjs.org/. */
npmRegistryUrl: string;
}

View File

@@ -1,5 +1,3 @@
import path from 'node:path';
import { isMap, isScalar, isSeq, LineCounter, parse, type ParsedNode, parseDocument } from 'yaml';
import { normalizeScripts } from '~/common/insomnia-schema-migrations/v5.1';
@@ -158,7 +156,7 @@ export function hasSignificantChanges(
config: Partial<IntelligentDiffConfig> = {},
): boolean {
// Non-YAML files → raw string comparison
if (path.extname(filePath) !== '.yaml') {
if (!filePath.toLowerCase().endsWith('.yaml')) {
return originalContent !== modifiedContent;
}

View File

@@ -9,12 +9,13 @@ import { HydratedRouter } from 'react-router/dom';
import { insomniaFetch } from '~/common/insomnia-fetch';
import { initDatabase, initServices, services } from '~/insomnia-data';
import { database as clientDatabase } from '~/ui/database.client';
import { clearOAuthWindowSessionId } from '~/ui/spawn-oauth-window';
import { migrateFromLocalStorage, type SessionData, setSessionData, setVaultSessionData } from './account/session';
import { getInsomniaSession, getInsomniaVaultKey, getInsomniaVaultSalt, getSkipOnboarding } from './common/constants';
import { initNewOAuthSession } from './network/o-auth-2/get-token';
import { init as initPlugins } from './plugins';
import { applyColorScheme } from './plugins/misc';
import { registerSyncMergeConflictListener } from './sync/vcs/insomnia-sync';
import { HtmlElementWrapper } from './ui/components/html-element-wrapper';
import { showModal } from './ui/components/modals';
import { AlertModal } from './ui/components/modals/alert-modal';
@@ -42,6 +43,7 @@ configureFetch(options => insomniaFetch({ ...options }));
await initPlugins();
await migrateFromLocalStorage();
registerSyncMergeConflictListener();
try {
window.showAlert = options => showModal(AlertModal, options);
@@ -129,7 +131,7 @@ if (insomniaSession) {
const appSettings = await services.settings.getOrCreate();
if (appSettings.clearOAuth2SessionOnRestart) {
initNewOAuthSession();
await clearOAuthWindowSessionId();
}
applyColorScheme(appSettings);

View File

@@ -3,6 +3,7 @@ import * as fs from 'node:fs';
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron';
import type { Compression } from '~/insomnia-data';
import { servicesProxy } from '~/ui/renderer-services-proxy';
import {
asyncTasksAllSettled,
@@ -13,7 +14,7 @@ import {
stopMonitorAsyncTasks,
} from '../../insomnia-scripting-environment/src/objects';
// this will also import lots of node_modules into the preload script, consider moving this file insomnia-scripting-environment
import { requireInterceptor } from './require-interceptor';
import { requireInterceptor } from './scripting/require-interceptor';
export interface HiddenBrowserWindowToMainBridgeAPI {
requireInterceptor: (module: string) => any;
@@ -63,7 +64,9 @@ const bridge: HiddenBrowserWindowToMainBridgeAPI = {
if (process.contextIsolated) {
contextBridge.exposeInMainWorld('bridge', bridge);
contextBridge.exposeInMainWorld('Promise', ProxiedPromise);
contextBridge.exposeInMainWorld('_dataServices', servicesProxy);
} else {
window.bridge = bridge;
window.Promise = ProxiedPromise;
window._dataServices = servicesProxy;
}

View File

@@ -1,29 +1,34 @@
import * as Sentry from '@sentry/electron/renderer';
import * as _ from 'es-toolkit/compat';
import { SENTRY_OPTIONS } from 'insomnia/src/common/sentry';
import {
initInsomniaObject,
InsomniaObject,
waitForAllTestsDone,
} from '../../insomnia-scripting-environment/src/objects';
import {
getNewConsole,
mergeClientCertificates,
mergeCookieJar,
mergeRequests,
mergeSettings,
type RequestContext,
} from '../../insomnia-scripting-environment/src/objects';
import { initServices } from '~/insomnia-data';
import type { RequestContext } from '../../insomnia-scripting-environment/src/objects';
import { runScript } from './scripting/run-script';
import { type ScriptSecurityPolicy } from './scripting/sandbox';
export interface HiddenBrowserWindowBridgeAPI {
runScript: (options: { script: string; context: RequestContext }) => Promise<RequestContext>;
runScript: (options: {
script: string;
context: RequestContext;
securityPolicy?: ScriptSecurityPolicy;
}) => Promise<RequestContext>;
}
Sentry.init({
...SENTRY_OPTIONS,
});
// Initialize services for hidden renderer process
if (!window._dataServices) {
throw new Error(
'window._dataServices is not available. This entrypoint must run in an environment with the preload bridge.',
);
}
initServices(window._dataServices);
// Remove the global services reference after initialization to improve security by preventing unintended access from the global scope.
delete window._dataServices;
window.bridge.onmessage(
async (data: { script: string; context: RequestContext }, callback: ({ error }: { error: string }) => void) => {
window.bridge.setBusy(true);
@@ -38,14 +43,17 @@ window.bridge.onmessage(
const result = await window.bridge.Promise.race([timeoutPromise, runScript(data)]);
callback(result);
} catch (err) {
const errMessage = err.message
? `Error from Pre-request or after-response script:
${err.message}`
: err;
const fullErrMessage = `${errMessage}
${err.stack ? `Stack: ${err.stack}` : ''}`;
const error = err instanceof Error ? err : new Error(String(err));
if ((error as NodeJS.ErrnoException).code === 'SECURITY_POLICY_VIOLATION') {
console.log('[hidden-window] security policy violation:', error.message);
callback({ error: error.message });
return;
}
const errMessage = error.message
? `Error from Pre-request or after-response script:\n${error.message}`
: String(error);
const fullErrMessage = `${errMessage}\n\n${error.stack ? `Stack: ${error.stack}` : ''}`;
console.log('[hidden-window] script error:', errMessage);
Sentry.captureException(errMessage, {
tags: {
source: 'hidden-window',
@@ -57,112 +65,3 @@ ${err.stack ? `Stack: ${err.stack}` : ''}`;
}
},
);
// This function is duplicated in scriptExecutor.ts to run in nodejs
// TODO: consider removing this implementation and using only nodejs scripting
const runScript = async ({ script, context }: { script: string; context: RequestContext }): Promise<RequestContext> => {
const scriptConsole = getNewConsole();
const executionContext = await initInsomniaObject(context, scriptConsole.log);
const AsyncFunction = (async () => {}).constructor;
const executeScript = AsyncFunction(
'insomnia',
'require',
'console',
'_',
'setTimeout',
// disable these as they are not supported in web or existing implementation
'setImmediate',
'queueMicrotask',
'process',
'waitForAllTestsDone',
`
const $ = insomnia;
window.bridge.resetAsyncTasks(); // exclude unnecessary ones
${script};
await waitForAllTestsDone();
window.bridge.stopMonitorAsyncTasks(); // the next one should not be monitored
await window.bridge.asyncTasksAllSettled();
return insomnia;`,
);
const mutatedInsomniaObject = await executeScript(
executionContext,
window.bridge.requireInterceptor,
scriptConsole,
_,
proxiedSetTimeout,
undefined,
undefined,
undefined,
waitForAllTestsDone,
);
if (mutatedInsomniaObject == null || !(mutatedInsomniaObject instanceof InsomniaObject)) {
throw new Error('insomnia object is invalid or script returns earlier than expected.');
}
const mutatedContextObject = mutatedInsomniaObject.toObject();
const updatedRequest = mergeRequests(context.request, mutatedContextObject.request);
const updatedSettings = mergeSettings(context.settings, mutatedContextObject.request);
const updatedCertificates = mergeClientCertificates(
mutatedContextObject.clientCertificates,
mutatedContextObject.request,
);
const updatedCookieJar = mergeCookieJar(context.cookieJar, mutatedContextObject.cookieJar);
return {
...context,
environment: {
id: context.environment.id,
name: context.environment.name,
data: mutatedContextObject.environment,
},
baseEnvironment: {
id: context.baseEnvironment.id,
name: context.baseEnvironment.name,
data: mutatedContextObject.baseEnvironment,
},
iterationData: context.iterationData
? {
name: context.iterationData.name,
data: mutatedContextObject.iterationData,
}
: undefined,
transientVariables: {
name: context.transientVariables?.name || 'transientVariables',
data: mutatedContextObject.variables,
},
request: updatedRequest,
execution: mutatedContextObject.execution,
settings: updatedSettings,
clientCertificates: updatedCertificates,
cookieJar: updatedCookieJar,
globals: context.globals && {
id: context.globals.id,
name: context.globals.name,
data: mutatedContextObject.globals,
},
baseGlobals: context.baseGlobals && {
id: context.baseGlobals.id,
name: context.baseGlobals.name,
data: mutatedContextObject.baseGlobals,
},
requestTestResults: mutatedContextObject.requestTestResults,
logs: scriptConsole.dumpLogsAsArray(),
parentFolders: mutatedContextObject.parentFolders,
};
};
// proxiedSetTimeout has to be here as callback could be an async task
function proxiedSetTimeout(callback: () => void, ms?: number | undefined) {
let resolveHdl: (value: unknown) => void;
new Promise(resolve => {
resolveHdl = resolve;
});
return setTimeout(() => {
callback();
resolveHdl(null);
}, ms);
}

View File

@@ -8,15 +8,15 @@ import contextMenu from 'electron-context-menu';
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
import { configureFetch } from 'insomnia-api';
import { getCurrentSessionId } from '~/account/session';
import { insomniaFetch } from '~/common/insomnia-fetch';
import type { Project, RemoteProject, Stats } from '~/insomnia-data';
import { database, initDatabase, initServices, services } from '~/insomnia-data';
import { database, initDatabase, initServices, models, services } from '~/insomnia-data';
import { servicesNodeImpl } from '~/insomnia-data/node';
import { mainDatabase } from '~/main/database.main';
import { initElectronStorage } from '~/main/electron-storage';
import { runGitCredentialsMigration } from '~/main/git/migrations';
import { registerPathHandlers } from '~/main/ipc/path';
import { registerLLMConfigServiceAPI } from '~/main/llm-config-service';
import { runGitCredentialsMigration } from '~/sync/git/migrations';
import { userDataFolder } from '../config/config.json';
import { getAppVersion, getProductName, isDevelopment } from './common/constants';
@@ -24,8 +24,10 @@ import { isMac } from './common/platform';
import { SegmentEvent, trackSegmentEvent } from './main/analytics';
import { registerInsomniaProtocols } from './main/api.protocol';
import { backupIfNewerVersionAvailable } from './main/backup';
import { registerSyncHandlers } from './main/cloud-sync/ipc';
import { registerGitServiceAPI } from './main/git-service';
import { ipcMainOn, ipcMainOnce, registerElectronHandlers } from './main/ipc/electron';
import { registerElectronStorageHandlers } from './main/ipc/electron-storage';
import { registergRPCHandlers } from './main/ipc/grpc';
import { registerMainHandlers } from './main/ipc/main';
import { registerSecretStorageHandlers } from './main/ipc/secret-storage';
@@ -39,14 +41,16 @@ import { initializeSentry, sentryWatchAnalyticsEnabled } from './main/sentry';
import { checkIfRestartNeeded } from './main/squirrel-startup';
import * as updates from './main/updates';
import * as windowUtils from './main/window-utils';
import * as models from './models/index';
// Override the Electron userData path
// This makes Chromium use this folder for eg localStorage
// ensure userData dir change is made before configure sentry SDK (https://docs.sentry.io/platforms/javascript/guides/electron/#app-userdata-directory)
const dataPath =
process.env.INSOMNIA_DATA_PATH ||
path.join(app.getPath('userData'), '../', isDevelopment() ? 'insomnia-app' : userDataFolder);
app.setPath('userData', dataPath);
initElectronStorage(dataPath);
initializeLogging();
@@ -89,6 +93,8 @@ app.on('ready', async () => {
registerCurlHandlers();
registerMcpHandlers();
registerSecretStorageHandlers();
registerElectronStorageHandlers();
registerSyncHandlers();
/**
* There's no option that prevents Electron from fetching spellcheck dictionaries from Chromium's CDN and passing a non-resolving URL is the only known way to prevent it from fetching.
@@ -121,7 +127,6 @@ app.on('ready', async () => {
await backupIfNewerVersionAvailable();
sentryWatchAnalyticsEnabled();
watchProxySettings();
windowUtils.init();
await runGitCredentialsMigration();
@@ -217,7 +222,7 @@ const _launchApp = async () => {
}
});
// Disable deep linking in playwright e2e tests in order to run multiple tests in parallel
if (!process.env.PLAYWRIGHT) {
if (!process.env.PLAYWRIGHT_TEST) {
// Deep linking logic - https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
@@ -251,15 +256,6 @@ const _launchApp = async () => {
} else {
window = windowUtils.createWindowsAndReturnMain();
}
// Block imports when not logged in
const isImportDeeplink = url.includes('://app/import');
const isLoggedIn = (await getCurrentSessionId()) ? true : false;
const shouldShowLoginPrompt = isImportDeeplink && !isLoggedIn;
if (shouldShowLoginPrompt) {
const title = encodeURIComponent('You must be logged in to open this link');
const message = encodeURIComponent('Please log in and try again.');
return window.webContents.send('shell:open', `insomnia://app/alert?title=${title}&message=${message}`);
}
return window.webContents.send('shell:open', url);
};

View File

@@ -1,10 +1,15 @@
import { contextBridge, ipcRenderer, webUtils as webUtilities } from 'electron';
import type { Services } from '~/insomnia-data';
import type { AuthTypeOAuth2, OAuth2Token, RequestHeader } from '~/insomnia-data';
import { invokeWithNormalizedError } from '~/main/ipc/invoke';
import type { LLMBackend, LLMConfig, LLMConfigServiceAPI } from '~/main/llm-config-service';
import type { GenerateMcpSamplingResponseFunction } from '~/plugins/types';
import { isUserAbortResolveMergeConflictError, UserAbortResolveMergeConflictError } from '~/sync/vcs/errors';
import { servicesProxy } from '~/ui/renderer-services-proxy';
import type { SyncBridgeAPI } from './main/cloud-sync/ipc';
import type { GitServiceAPI } from './main/git-service';
import type { electronStorageBridgeAPI } from './main/ipc/electron-storage';
import type { gRPCBridgeAPI } from './main/ipc/grpc';
import type { secretStorageBridgeAPI } from './main/ipc/secret-storage';
import type { AIFeatureNames } from './main/llm-config-service';
@@ -12,80 +17,81 @@ import type { CurlBridgeAPI } from './main/network/curl';
import type { McpBridgeAPI } from './main/network/mcp';
import type { SocketIOBridgeAPI } from './main/network/socket-io';
import type { WebSocketBridgeAPI } from './main/network/websocket';
import type { RenderedRequest } from './templating/types';
import { invariant } from './utils/invariant';
const ports = new Map<'hiddenWindowPort', MessagePort>();
const webSocket: WebSocketBridgeAPI = {
open: options => ipcRenderer.invoke('webSocket.open', options),
open: options => invokeWithNormalizedError('webSocket.open', options),
close: options => ipcRenderer.send('webSocket.close', options),
closeAll: () => ipcRenderer.send('webSocket.closeAll'),
readyState: {
getCurrent: options => ipcRenderer.invoke('webSocket.readyState', options),
getCurrent: options => invokeWithNormalizedError('webSocket.readyState', options),
},
event: {
findMany: options => ipcRenderer.invoke('webSocket.event.findMany', options),
send: options => ipcRenderer.invoke('webSocket.event.send', options),
findMany: options => invokeWithNormalizedError('webSocket.event.findMany', options),
send: options => invokeWithNormalizedError('webSocket.event.send', options),
},
};
const curl: CurlBridgeAPI = {
open: options => ipcRenderer.invoke('curl.open', options),
open: options => invokeWithNormalizedError('curl.open', options),
close: options => ipcRenderer.send('curl.close', options),
closeAll: () => ipcRenderer.send('curl.closeAll'),
readyState: {
getCurrent: options => ipcRenderer.invoke('curl.readyState', options),
getCurrent: options => invokeWithNormalizedError('curl.readyState', options),
},
event: {
findMany: options => ipcRenderer.invoke('curl.event.findMany', options),
findMany: options => invokeWithNormalizedError('curl.event.findMany', options),
},
};
const socketIO: SocketIOBridgeAPI = {
open: options => ipcRenderer.invoke('socketIO.open', options),
open: options => invokeWithNormalizedError('socketIO.open', options),
readyState: {
getCurrent: options => ipcRenderer.invoke('socketIO.readyState', options),
getCurrent: options => invokeWithNormalizedError('socketIO.readyState', options),
},
close: options => ipcRenderer.send('socketIO.close', options),
closeAll: () => ipcRenderer.send('socketIO.closeAll'),
event: {
findMany: options => ipcRenderer.invoke('socketIO.event.findMany', options),
send: options => ipcRenderer.invoke('socketIO.event.send', options),
findMany: options => invokeWithNormalizedError('socketIO.event.findMany', options),
send: options => invokeWithNormalizedError('socketIO.event.send', options),
on: options => ipcRenderer.send('socketIO.event.on', options),
off: options => ipcRenderer.send('socketIO.event.off', options),
},
};
const mcp: McpBridgeAPI = {
connect: options => ipcRenderer.invoke('mcp.connect', options),
close: options => ipcRenderer.invoke('mcp.close', options),
connect: options => invokeWithNormalizedError('mcp.connect', options),
close: options => invokeWithNormalizedError('mcp.close', options),
closeAll: () => ipcRenderer.send('mcp.closeAll'),
authConfirmation: confirmed => ipcRenderer.send('mcp.authConfirmed', confirmed),
primitive: {
listTools: options => ipcRenderer.invoke('mcp.primitive.listTools', options),
callTool: options => ipcRenderer.invoke('mcp.primitive.callTool', options),
listResources: options => ipcRenderer.invoke('mcp.primitive.listResources', options),
listResourceTemplates: options => ipcRenderer.invoke('mcp.primitive.listResourceTemplates', options),
readResource: options => ipcRenderer.invoke('mcp.primitive.readResource', options),
subscribeResource: options => ipcRenderer.invoke('mcp.primitive.subscribeResource', options),
unsubscribeResource: options => ipcRenderer.invoke('mcp.primitive.unsubscribeResource', options),
listPrompts: options => ipcRenderer.invoke('mcp.primitive.listPrompts', options),
getPrompt: options => ipcRenderer.invoke('mcp.primitive.getPrompt', options),
listTools: options => invokeWithNormalizedError('mcp.primitive.listTools', options),
callTool: options => invokeWithNormalizedError('mcp.primitive.callTool', options),
listResources: options => invokeWithNormalizedError('mcp.primitive.listResources', options),
listResourceTemplates: options => invokeWithNormalizedError('mcp.primitive.listResourceTemplates', options),
readResource: options => invokeWithNormalizedError('mcp.primitive.readResource', options),
subscribeResource: options => invokeWithNormalizedError('mcp.primitive.subscribeResource', options),
unsubscribeResource: options => invokeWithNormalizedError('mcp.primitive.unsubscribeResource', options),
listPrompts: options => invokeWithNormalizedError('mcp.primitive.listPrompts', options),
getPrompt: options => invokeWithNormalizedError('mcp.primitive.getPrompt', options),
},
notification: {
rootListChange: options => ipcRenderer.invoke('mcp.notification.rootListChange', options),
rootListChange: options => invokeWithNormalizedError('mcp.notification.rootListChange', options),
},
readyState: {
getCurrent: options => ipcRenderer.invoke('mcp.readyState', options),
getCurrent: options => invokeWithNormalizedError('mcp.readyState', options),
},
client: {
responseElicitationRequest: options => ipcRenderer.send('mcp.client.responseElicitationRequest', options),
responseSamplingRequest: options => ipcRenderer.send('mcp.client.responseSamplingRequest', options),
hasRequestResponded: options => ipcRenderer.invoke('mcp.client.hasRequestResponded', options),
cancelRequest: options => ipcRenderer.invoke('mcp.client.cancelRequest', options),
hasRequestResponded: options => invokeWithNormalizedError('mcp.client.hasRequestResponded', options),
cancelRequest: options => invokeWithNormalizedError('mcp.client.cancelRequest', options),
},
event: {
findMany: options => ipcRenderer.invoke('mcp.event.findMany', options),
findNotifications: options => ipcRenderer.invoke('mcp.event.findNotifications', options),
findPendingEvents: options => ipcRenderer.invoke('mcp.event.findPendingEvents', options),
findMany: options => invokeWithNormalizedError('mcp.event.findMany', options),
findNotifications: options => invokeWithNormalizedError('mcp.event.findNotifications', options),
findPendingEvents: options => invokeWithNormalizedError('mcp.event.findPendingEvents', options),
},
};
@@ -95,72 +101,135 @@ const grpc: gRPCBridgeAPI = {
commit: options => ipcRenderer.send('grpc.commit', options),
cancel: options => ipcRenderer.send('grpc.cancel', options),
closeAll: () => ipcRenderer.send('grpc.closeAll'),
loadMethods: options => ipcRenderer.invoke('grpc.loadMethods', options),
loadMethodsFromReflection: options => ipcRenderer.invoke('grpc.loadMethodsFromReflection', options),
loadMethods: options => invokeWithNormalizedError('grpc.loadMethods', options),
loadMethodsFromReflection: options => invokeWithNormalizedError('grpc.loadMethodsFromReflection', options),
writeProtoFile: protoFileId => invokeWithNormalizedError('grpc.writeProtoFile', protoFileId),
};
const secretStorage: secretStorageBridgeAPI = {
setSecret: (key, secret) => ipcRenderer.invoke('secretStorage.setSecret', key, secret),
getSecret: key => ipcRenderer.invoke('secretStorage.getSecret', key),
deleteSecret: key => ipcRenderer.invoke('secretStorage.deleteSecret', key),
encryptString: raw => ipcRenderer.invoke('secretStorage.encryptString', raw),
decryptString: cipherText => ipcRenderer.invoke('secretStorage.decryptString', cipherText),
setSecret: (key, secret) => invokeWithNormalizedError('secretStorage.setSecret', key, secret),
getSecret: key => invokeWithNormalizedError('secretStorage.getSecret', key),
deleteSecret: key => invokeWithNormalizedError('secretStorage.deleteSecret', key),
encryptString: raw => invokeWithNormalizedError('secretStorage.encryptString', raw),
decryptString: cipherText => invokeWithNormalizedError('secretStorage.decryptString', cipherText),
};
const electronStorage: electronStorageBridgeAPI = {
getItem: key => invokeWithNormalizedError('electronStorage.getItem', key),
setItem: (key, value) => invokeWithNormalizedError('electronStorage.setItem', key, value),
};
const invokeSyncMethod = async <T>(methodName: string, ...args: unknown[]) => {
try {
return (await invokeWithNormalizedError('sync.invoke', methodName, ...args)) as T;
} catch (error) {
if (isUserAbortResolveMergeConflictError(error)) {
throw new UserAbortResolveMergeConflictError(
'message' in error && typeof error.message === 'string' ? error.message : undefined,
);
}
throw error;
}
};
const sync: SyncBridgeAPI = {
archiveProject: () => invokeSyncMethod('archiveProject'),
checkout: (...args) => invokeSyncMethod('checkout', ...args),
compareRemoteBranch: () => invokeSyncMethod('compareRemoteBranch'),
fork: (...args) => invokeSyncMethod('fork', ...args),
getActiveBackendProject: () => invokeSyncMethod('getActiveBackendProject'),
getBranchNames: () => invokeSyncMethod('getBranchNames'),
getCurrentBranchName: () => invokeSyncMethod('getCurrentBranchName'),
getHistory: (...args) => invokeSyncMethod('getHistory', ...args),
getHistoryCount: () => invokeSyncMethod('getHistoryCount'),
getRemoteBranchNames: () => invokeSyncMethod('getRemoteBranchNames'),
getVersion: () => invokeSyncMethod('getVersion'),
hasBackendProject: () => invokeSyncMethod('hasBackendProject'),
localBackendProjects: () => invokeSyncMethod('localBackendProjects'),
merge: (...args) => invokeSyncMethod('merge', ...args),
pull: (...args) => invokeSyncMethod('pull', ...args),
pullRemoteBackendProject: options => invokeWithNormalizedError('sync.pullRemoteBackendProject', options),
push: (...args) => invokeSyncMethod('push', ...args),
remoteBackendProjects: (...args) => invokeSyncMethod('remoteBackendProjects', ...args),
removeBackendProjectsForRoot: (...args) => invokeSyncMethod('removeBackendProjectsForRoot', ...args),
removeBranch: (...args) => invokeSyncMethod('removeBranch', ...args),
removeRemoteBranch: (...args) => invokeSyncMethod('removeRemoteBranch', ...args),
rollback: (...args) => invokeSyncMethod('rollback', ...args),
rollbackToLatest: (...args) => invokeSyncMethod('rollbackToLatest', ...args),
resolveConflict: options => ipcRenderer.send('sync.resolveConflict', options),
cancelConflict: options => ipcRenderer.send('sync.cancelConflict', options),
stage: (...args) => invokeSyncMethod('stage', ...args),
status: (...args) => invokeSyncMethod('status', ...args),
switchAndCreateBackendProjectIfNotExist: (...args) =>
invokeSyncMethod('switchAndCreateBackendProjectIfNotExist', ...args),
takeSnapshot: (...args) => invokeSyncMethod('takeSnapshot', ...args),
unstage: (...args) => invokeSyncMethod('unstage', ...args),
on: (channel, listener) => {
ipcRenderer.on(channel, listener);
return () => ipcRenderer.removeListener(channel, listener);
},
};
const git: GitServiceAPI = {
loadGitRepository: options => ipcRenderer.invoke('git.loadGitRepository', options),
getGitBranches: options => ipcRenderer.invoke('git.getGitBranches', options),
fetchGitRemoteBranches: options => ipcRenderer.invoke('git.fetchGitRemoteBranches', options),
validateGitRepositoryCredentials: options => ipcRenderer.invoke('git.validateGitRepositoryCredentials', options),
validateGitCredentialById: options => ipcRenderer.invoke('git.validateGitCredentialById', options),
gitFetchAction: options => ipcRenderer.invoke('git.gitFetchAction', options),
gitLogLoader: options => ipcRenderer.invoke('git.gitLogLoader', options),
gitChangesLoader: options => ipcRenderer.invoke('git.gitChangesLoader', options),
canPushLoader: options => ipcRenderer.invoke('git.canPushLoader', options),
cloneGitRepo: options => ipcRenderer.invoke('git.cloneGitRepo', options),
initGitRepoClone: options => ipcRenderer.invoke('git.initGitRepoClone', options),
updateGitRepo: options => ipcRenderer.invoke('git.updateGitRepo', options),
resetGitRepo: options => ipcRenderer.invoke('git.resetGitRepo', options),
commitToGitRepo: options => ipcRenderer.invoke('git.commitToGitRepo', options),
commitAndPushToGitRepo: options => ipcRenderer.invoke('git.commitAndPushToGitRepo', options),
createNewGitBranch: options => ipcRenderer.invoke('git.createNewGitBranch', options),
checkoutGitBranch: options => ipcRenderer.invoke('git.checkoutGitBranch', options),
mergeGitBranch: options => ipcRenderer.invoke('git.mergeGitBranch', options),
deleteGitBranch: options => ipcRenderer.invoke('git.deleteGitBranch', options),
pushToGitRemote: options => ipcRenderer.invoke('git.pushToGitRemote', options),
pullFromGitRemote: options => ipcRenderer.invoke('git.pullFromGitRemote', options),
continueMerge: options => ipcRenderer.invoke('git.continueMerge', options),
discardChanges: options => ipcRenderer.invoke('git.discardChanges', options),
abortMerge: () => ipcRenderer.invoke('git.abortMerge'),
gitStatus: options => ipcRenderer.invoke('git.gitStatus', options),
diff: () => ipcRenderer.invoke('git.diff'),
multipleCommitToGitRepo: options => ipcRenderer.invoke('git.multipleCommitToGitRepo', options),
stageChanges: options => ipcRenderer.invoke('git.stageChanges', options),
unstageChanges: options => ipcRenderer.invoke('git.unstageChanges', options),
diffFileLoader: options => ipcRenderer.invoke('git.diffFileLoader', options),
getRepositoryDirectoryTree: options => ipcRenderer.invoke('git.getRepositoryDirectoryTree', options),
migrateLegacyInsomniaFolderToFile: options => ipcRenderer.invoke('git.migrateLegacyInsomniaFolderToFile', options),
loadGitRepository: options => invokeWithNormalizedError('git.loadGitRepository', options),
getGitBranches: options => invokeWithNormalizedError('git.getGitBranches', options),
fetchGitRemoteBranches: options => invokeWithNormalizedError('git.fetchGitRemoteBranches', options),
getProjectGitFileIssues: options => invokeWithNormalizedError('git.getProjectGitFileIssues', options),
validateGitRepositoryCredentials: options =>
invokeWithNormalizedError('git.validateGitRepositoryCredentials', options),
validateGitCredentialById: options => invokeWithNormalizedError('git.validateGitCredentialById', options),
gitFetchAction: options => invokeWithNormalizedError('git.gitFetchAction', options),
gitLogLoader: options => invokeWithNormalizedError('git.gitLogLoader', options),
gitChangesLoader: options => invokeWithNormalizedError('git.gitChangesLoader', options),
canPushLoader: options => invokeWithNormalizedError('git.canPushLoader', options),
cloneGitRepo: options => invokeWithNormalizedError('git.cloneGitRepo', options),
initGitRepoClone: options => invokeWithNormalizedError('git.initGitRepoClone', options),
updateGitRepo: options => invokeWithNormalizedError('git.updateGitRepo', options),
resetGitRepo: options => invokeWithNormalizedError('git.resetGitRepo', options),
commitToGitRepo: options => invokeWithNormalizedError('git.commitToGitRepo', options),
commitAndPushToGitRepo: options => invokeWithNormalizedError('git.commitAndPushToGitRepo', options),
createNewGitBranch: options => invokeWithNormalizedError('git.createNewGitBranch', options),
checkoutGitBranch: options => invokeWithNormalizedError('git.checkoutGitBranch', options),
mergeGitBranch: options => invokeWithNormalizedError('git.mergeGitBranch', options),
deleteGitBranch: options => invokeWithNormalizedError('git.deleteGitBranch', options),
pushToGitRemote: options => invokeWithNormalizedError('git.pushToGitRemote', options),
pullFromGitRemote: options => invokeWithNormalizedError('git.pullFromGitRemote', options),
continueMerge: options => invokeWithNormalizedError('git.continueMerge', options),
discardChanges: options => invokeWithNormalizedError('git.discardChanges', options),
abortMerge: options => invokeWithNormalizedError('git.abortMerge', options),
gitStatus: options => invokeWithNormalizedError('git.gitStatus', options),
diff: () => invokeWithNormalizedError('git.diff'),
multipleCommitToGitRepo: options => invokeWithNormalizedError('git.multipleCommitToGitRepo', options),
stageChanges: options => invokeWithNormalizedError('git.stageChanges', options),
unstageChanges: options => invokeWithNormalizedError('git.unstageChanges', options),
diffFileLoader: options => invokeWithNormalizedError('git.diffFileLoader', options),
getRepositoryDirectoryTree: options => invokeWithNormalizedError('git.getRepositoryDirectoryTree', options),
migrateLegacyInsomniaFolderToFile: options =>
invokeWithNormalizedError('git.migrateLegacyInsomniaFolderToFile', options),
listGitProviders: () => ipcRenderer.invoke('git.listGitProviders'),
initSignInToGitProvider: options => ipcRenderer.invoke('git.initSignInToGitProvider', options),
completeSignInToGitProvider: options => ipcRenderer.invoke('git.completeSignInToGitProvider', options),
getGitProviderRepositories: options => ipcRenderer.invoke('git.getGitProviderRepositories', options),
getGitProviderEmails: options => ipcRenderer.invoke('git.getGitProviderEmails', options),
getCurrentBranchByRepositoryId: options => ipcRenderer.invoke('git.getCurrentBranchByRepositoryId', options),
listGitProviders: () => invokeWithNormalizedError('git.listGitProviders'),
initSignInToGitProvider: options => invokeWithNormalizedError('git.initSignInToGitProvider', options),
completeSignInToGitProvider: options => invokeWithNormalizedError('git.completeSignInToGitProvider', options),
getGitProviderRepositories: options => invokeWithNormalizedError('git.getGitProviderRepositories', options),
getGitProviderEmails: options => invokeWithNormalizedError('git.getGitProviderEmails', options),
getCurrentBranchByRepositoryId: options => invokeWithNormalizedError('git.getCurrentBranchByRepositoryId', options),
getBranchRemoteInfo: options => invokeWithNormalizedError('git.getBranchRemoteInfo', options),
runAllGitRepoMigrations: () => invokeWithNormalizedError('git.runAllGitRepoMigrations'),
};
const llm: LLMConfigServiceAPI = {
getActiveBackend: () => ipcRenderer.invoke('llm.getActiveBackend'),
setActiveBackend: (backend: LLMBackend) => ipcRenderer.invoke('llm.setActiveBackend', backend),
clearActiveBackend: () => ipcRenderer.invoke('llm.clearActiveBackend'),
getBackendConfig: (backend: LLMBackend) => ipcRenderer.invoke('llm.getBackendConfig', backend),
getActiveBackend: () => invokeWithNormalizedError('llm.getActiveBackend'),
setActiveBackend: (backend: LLMBackend) => invokeWithNormalizedError('llm.setActiveBackend', backend),
clearActiveBackend: () => invokeWithNormalizedError('llm.clearActiveBackend'),
getBackendConfig: (backend: LLMBackend) => invokeWithNormalizedError('llm.getBackendConfig', backend),
updateBackendConfig: (backend: LLMBackend, config: Partial<LLMConfig>) =>
ipcRenderer.invoke('llm.updateBackendConfig', backend, config),
getAllConfigurations: () => ipcRenderer.invoke('llm.getAllConfigurations'),
getCurrentConfig: () => ipcRenderer.invoke('llm.getCurrentConfig'),
getAIFeatureEnabled: (feature: AIFeatureNames) => ipcRenderer.invoke('llm.getAIFeatureEnabled', feature),
invokeWithNormalizedError('llm.updateBackendConfig', backend, config),
getAllConfigurations: () => invokeWithNormalizedError('llm.getAllConfigurations'),
getCurrentConfig: () => invokeWithNormalizedError('llm.getCurrentConfig'),
getAIFeatureEnabled: (feature: AIFeatureNames) => invokeWithNormalizedError('llm.getAIFeatureEnabled', feature),
setAIFeatureEnabled: (feature: AIFeatureNames, enabled: boolean) =>
ipcRenderer.invoke('llm.setAIFeatureEnabled', feature, enabled),
invokeWithNormalizedError('llm.setAIFeatureEnabled', feature, enabled),
};
const main: Window['main'] = {
@@ -168,33 +237,44 @@ const main: Window['main'] = {
addExecutionStep: options => ipcRenderer.send('addExecutionStep', options),
completeExecutionStep: options => ipcRenderer.send('completeExecutionStep', options),
updateLatestStepName: options => ipcRenderer.send('updateLatestStepName', options),
getExecution: options => ipcRenderer.invoke('getExecution', options),
getExecution: options => invokeWithNormalizedError('getExecution', options),
loginStateChange: () => ipcRenderer.send('loginStateChange'),
restart: () => ipcRenderer.send('restart'),
openInBrowser: options => ipcRenderer.send('openInBrowser', options),
openDeepLink: options => ipcRenderer.send('openDeepLink', options),
halfSecondAfterAppStart: () => ipcRenderer.send('halfSecondAfterAppStart'),
manualUpdateCheck: () => ipcRenderer.send('manualUpdateCheck'),
backup: () => ipcRenderer.invoke('backup'),
restoreBackup: options => ipcRenderer.invoke('restoreBackup', options),
authorizeUserInWindow: options => ipcRenderer.invoke('authorizeUserInWindow', options),
authorizeUserInDefaultBrowser: options => ipcRenderer.invoke('authorizeUserInDefaultBrowser', options),
onDefaultBrowserOAuthRedirect: options => ipcRenderer.invoke('onDefaultBrowserOAuthRedirect', options),
cancelAuthorizationInDefaultBrowser: options => ipcRenderer.invoke('cancelAuthorizationInDefaultBrowser', options),
backup: () => invokeWithNormalizedError('backup'),
restoreBackup: options => invokeWithNormalizedError('restoreBackup', options),
authorizeUserInWindow: options => invokeWithNormalizedError('authorizeUserInWindow', options),
authorizeUserInDefaultBrowser: options => invokeWithNormalizedError('authorizeUserInDefaultBrowser', options),
onDefaultBrowserOAuthRedirect: options => invokeWithNormalizedError('onDefaultBrowserOAuthRedirect', options),
cancelAuthorizationInDefaultBrowser: options =>
invokeWithNormalizedError('cancelAuthorizationInDefaultBrowser', options),
setMenuBarVisibility: options => ipcRenderer.send('setMenuBarVisibility', options),
multipartBufferToArray: options => ipcRenderer.invoke('multipartBufferToArray', options),
multipartBufferToArray: options => invokeWithNormalizedError('multipartBufferToArray', options),
installPlugin: (lookupName: string, allowScopedPackageNames = false) =>
ipcRenderer.invoke('installPlugin', lookupName, allowScopedPackageNames),
curlRequest: options => ipcRenderer.invoke('curlRequest', options),
invokeWithNormalizedError('installPlugin', lookupName, allowScopedPackageNames),
initializeWorkspaceBackendProject: options => invokeWithNormalizedError('initializeWorkspaceBackendProject', options),
curlRequest: options => invokeWithNormalizedError('curlRequest', options),
cancelCurlRequest: options => ipcRenderer.send('cancelCurlRequest', options),
writeFile: options => ipcRenderer.invoke('writeFile', options),
insecureReadFile: options => ipcRenderer.invoke('insecureReadFile', options),
insecureReadFileWithEncoding: options => ipcRenderer.invoke('insecureReadFileWithEncoding', options),
secureReadFile: options => ipcRenderer.invoke('secureReadFile', options),
parseImport: (...args) => ipcRenderer.invoke('parseImport', ...args),
readDir: options => ipcRenderer.invoke('readDir', options),
readOrCreateDataDir: options => ipcRenderer.invoke('readOrCreateDataDir', options),
lintSpec: options => ipcRenderer.invoke('lintSpec', options),
writeFile: options => invokeWithNormalizedError('writeFile', options),
writeResponseBodyToFile: options => invokeWithNormalizedError('writeResponseBodyToFile', options),
getAuthHeader: (renderedRequest: RenderedRequest, url: string): Promise<RequestHeader | undefined> =>
invokeWithNormalizedError('getAuthHeader', renderedRequest, url),
getOAuth2Token: (
requestId: string,
authentication: AuthTypeOAuth2,
forceRefresh?: boolean,
): Promise<OAuth2Token | undefined> =>
invokeWithNormalizedError('getOAuth2Token', requestId, authentication, forceRefresh),
insecureReadFile: options => invokeWithNormalizedError('insecureReadFile', options),
insecureReadFileWithEncoding: options => invokeWithNormalizedError('insecureReadFileWithEncoding', options),
secureReadFile: options => invokeWithNormalizedError('secureReadFile', options),
parseImport: (...args) => invokeWithNormalizedError('parseImport', ...args),
readDir: options => invokeWithNormalizedError('readDir', options),
readOrCreateDataDir: options => invokeWithNormalizedError('readOrCreateDataDir', options),
lintSpec: options => invokeWithNormalizedError('lintSpec', options),
on: (channel, listener) => {
ipcRenderer.on(channel, listener);
return () => ipcRenderer.removeListener(channel, listener);
@@ -207,6 +287,8 @@ const main: Window['main'] = {
grpc,
curl,
secretStorage,
electronStorage,
sync,
trackSegmentEvent: options => ipcRenderer.send('trackSegmentEvent', options),
trackPageView: options => ipcRenderer.send('trackPageView', options),
setCurrentOrganizationId: organizationId => ipcRenderer.send('analytics.setOrganizationId', organizationId),
@@ -214,14 +296,14 @@ const main: Window['main'] = {
showContextMenu: options => ipcRenderer.send('showContextMenu', options),
database: {
caCertificate: {
create: options => ipcRenderer.invoke('database.caCertificate.create', options),
create: options => invokeWithNormalizedError('database.caCertificate.create', options),
},
},
hiddenBrowserWindow: {
runScript: options =>
new Promise(async (resolve, reject) => {
const isPortAlive = ports.get('hiddenWindowPort') !== undefined;
await ipcRenderer.invoke('open-channel-to-hidden-browser-window', isPortAlive);
await invokeWithNormalizedError('open-channel-to-hidden-browser-window', isPortAlive);
const port = ports.get('hiddenWindowPort');
invariant(port, 'hiddenWindowPort is undefined');
@@ -238,8 +320,9 @@ const main: Window['main'] = {
}),
},
extractJsonFileFromPostmanDataDumpArchive: archivePath =>
ipcRenderer.invoke('extractJsonFileFromPostmanDataDumpArchive', archivePath),
getLocalStorageDataFromFileOrigin: () => ipcRenderer.invoke('getLocalStorageDataFromFileOrigin'),
invokeWithNormalizedError('extractJsonFileFromPostmanDataDumpArchive', archivePath),
syncNewWorkspaceIfNeeded: options => invokeWithNormalizedError('syncNewWorkspaceIfNeeded', options),
getLocalStorageDataFromFileOrigin: () => invokeWithNormalizedError('getLocalStorageDataFromFileOrigin'),
generateMockRouteDataFromSpec: (
openApiSpec: string | undefined,
specUrl: string | undefined,
@@ -248,7 +331,7 @@ const main: Window['main'] = {
useDynamicMockResponses: boolean,
mockServerAdditionalFiles: string[],
) =>
ipcRenderer.invoke(
invokeWithNormalizedError(
'generateMockRouteDataFromSpec',
openApiSpec,
specUrl,
@@ -258,15 +341,15 @@ const main: Window['main'] = {
mockServerAdditionalFiles,
),
generateCommitsFromDiff: (input: { diff: string; recent_commits: string }) =>
ipcRenderer.invoke('generateCommitsFromDiff', input),
invokeWithNormalizedError('generateCommitsFromDiff', input),
generateMcpSamplingResponse: (parameters: Parameters<GenerateMcpSamplingResponseFunction>[0]) =>
ipcRenderer.invoke('generateMcpSamplingResponse', parameters),
invokeWithNormalizedError('generateMcpSamplingResponse', parameters),
};
ipcRenderer.on('hidden-browser-window-response-listener', event => {
const [port] = event.ports;
ports.set('hiddenWindowPort', port);
ipcRenderer.invoke('main-window-script-port-ready');
invokeWithNormalizedError('main-window-script-port-ready');
});
const path: Window['path'] = {
dirname: (p: string) => ipcRenderer.sendSync('path.dirname', p),
@@ -275,8 +358,8 @@ const path: Window['path'] = {
resolve: (...paths: string[]) => ipcRenderer.sendSync('path.resolve', ...paths),
};
const dialog: Window['dialog'] = {
showOpenDialog: options => ipcRenderer.invoke('showOpenDialog', options),
showSaveDialog: options => ipcRenderer.invoke('showSaveDialog', options),
showOpenDialog: options => invokeWithNormalizedError('showOpenDialog', options),
showSaveDialog: options => invokeWithNormalizedError('showSaveDialog', options),
};
const app: Window['app'] = {
getPath: options => ipcRenderer.sendSync('getPath', options),
@@ -289,7 +372,7 @@ const app: Window['app'] = {
};
const shell: Window['shell'] = {
showItemInFolder: options => ipcRenderer.send('showItemInFolder', options),
openPath: options => ipcRenderer.invoke('openPath', options),
openPath: options => invokeWithNormalizedError('openPath', options),
};
const clipboard: Window['clipboard'] = {
readText: () => ipcRenderer.sendSync('readText'),
@@ -300,22 +383,9 @@ const webUtils: Window['webUtils'] = {
getPathForFile: (file: File) => webUtilities.getPathForFile(file),
};
const database: Window['database'] = {
invoke: (fnName, ...args) => ipcRenderer.invoke('database.invoke', fnName, ...args),
invoke: (fnName, ...args) => invokeWithNormalizedError('database.invoke', fnName, ...args),
};
const servicesProxy = new Proxy({} as Services, {
get(_target, serviceName: string) {
return new Proxy(
{},
{
get(_target, methodName: string) {
return (...args: unknown[]) => ipcRenderer.invoke('services.invoke', serviceName, methodName, ...args);
},
},
);
},
});
if (process.contextIsolated) {
contextBridge.exposeInMainWorld('main', main);
contextBridge.exposeInMainWorld('dialog', dialog);

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import { type BaseGitCredentialsV2, models, services } from '~/insomnia-data';
import type { BaseGitCredentialsV2 } from '~/insomnia-data';
import { models, services } from '~/insomnia-data';
const { init, isGitCredentialsV2, supportsRenewal } = models.gitCredentials;

View File

@@ -1,8 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { services } from '~/insomnia-data';
import * as models from '../index';
import { models, services } from '~/insomnia-data';
describe('init()', () => {
it('contains all required fields', async () => {

View File

@@ -1,8 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { services } from '~/insomnia-data';
import * as models from '../index';
import { models, services } from '~/insomnia-data';
describe('init()', () => {
it('contains all required fields', async () => {

View File

@@ -1,7 +1,8 @@
import { describe, expect, it } from 'vitest';
import { getModel, mustGetModel } from '../index';
import * as models from '../index';
import { models } from '~/insomnia-data';
const { getModel, mustGetModel } = models;
describe('index', () => {
describe('getModel()', () => {

View File

@@ -1,8 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { services } from '~/insomnia-data';
import * as models from '../index';
import { models, services } from '~/insomnia-data';
describe('init()', () => {
it('contains all required fields', async () => {

View File

@@ -2,8 +2,6 @@ import { describe, expect, it } from 'vitest';
import { services } from '~/insomnia-data';
import * as models from '../index';
describe('create()', () => {
it('fails when missing parentId', async () => {
expect(() =>

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