diff --git a/.github/ISSUE_TEMPLATE/BugReport.yaml b/.github/ISSUE_TEMPLATE/BugReport.yaml
index 4b6da7871..f525bb7e6 100644
--- a/.github/ISSUE_TEMPLATE/BugReport.yaml
+++ b/.github/ISSUE_TEMPLATE/BugReport.yaml
@@ -6,26 +6,58 @@ body:
attributes:
value: |
Thanks for taking the time to fill out this bug report!
+
+ Before submitting, please make sure you've searched existing issues:
+ 👉 [Search existing issues](https://github.com/usebruno/bruno/issues?q=is%3Aissue)
+
- type: checkboxes
attributes:
label: 'I have checked the following:'
options:
- - label: I use the newest version of bruno.
- required: true
- - label: I've searched existing issues and found nothing related to my issue.
+ - label: "I have searched existing issues and found nothing related to my issue."
required: true
+
+ - type: checkboxes
+ attributes:
+ label: 'This bug is:'
+ options:
+ - label: making Bruno unusable for me
+ required: false
+ - label: slowing me down but I'm able to continue working
+ required: false
+ - label: annoying
+ required: false
+
+ - type: input
+ attributes:
+ label: Bruno version
+ description: Please specify the version of Bruno you are using in which the issue occurs.
+ placeholder: 1.38.1
+ validations:
+ required: true
+
+ - type: input
+ attributes:
+ label: Operating System
+ description: Information about the operating system the issue occurs on.
+ placeholder: Windows 11 26100.3037 / macOS 15.1 (24B83) / Linux 6.13.1
+ validations:
+ required: true
+
- type: textarea
attributes:
label: Describe the bug
- description: A clear and concise description of the bug.
+ description: A clear and concise description of the bug and how it's effecting your work along with steps to reproduce.
validations:
required: true
+
- type: textarea
attributes:
label: .bru file to reproduce the bug
- description: Attach your .bru file here that can reqroduce the problem.
+ description: Attach your .bru file here that can reproduce the problem.
validations:
required: false
+
- type: textarea
attributes:
label: Screenshots/Live demo link
diff --git a/.github/ISSUE_TEMPLATE/FeatureRequest.yaml b/.github/ISSUE_TEMPLATE/FeatureRequest.yaml
index 3a3997beb..161e56e9c 100644
--- a/.github/ISSUE_TEMPLATE/FeatureRequest.yaml
+++ b/.github/ISSUE_TEMPLATE/FeatureRequest.yaml
@@ -8,13 +8,23 @@ body:
options:
- label: I've searched existing issues and found nothing related to my issue.
required: true
+ - type: checkboxes
+ attributes:
+ label: 'This feature'
+ options:
+ - label: blocks me from using Bruno
+ required: false
+ - label: would improve my quality of life in Bruno
+ required: false
+ - label: is something I've never seen an API client do before
+ required: false
- type: markdown
attributes:
value: |
Suggest an idea for this project.
- type: textarea
attributes:
- label: Describe the feature you want to add
+ label: Describe the feature you want to add, and how it would change your usage of Bruno
description: A clear and concise description of the feature you want to be added.
validations:
required: true
@@ -23,4 +33,4 @@ body:
label: Mockups or Images of the feature
description: Add some images to support your feature.
validations:
- required: true
+ required: false
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..4aea207fb
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,31 @@
+version: 2
+
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: weekly
+
+ - package-ecosystem: npm
+ directory: "/"
+ schedule:
+ interval: weekly
+ groups:
+ bruno-dependencies:
+ patterns:
+ - "*usebruno*"
+ babel-dependencies:
+ patterns:
+ - "*babel*"
+ fortawesome-dependencies:
+ patterns:
+ - "*fortawesome*"
+ electron-dependencies:
+ patterns:
+ - "*electron*"
+ rollup-dependencies:
+ patterns:
+ - "*rollup*"
+ jest-dependencies:
+ patterns:
+ - "*jest*"
diff --git a/.github/workflows/npm-bru-cli.yml b/.github/workflows/npm-bru-cli.yml
index a1d1021ed..b489f1e43 100644
--- a/.github/workflows/npm-bru-cli.yml
+++ b/.github/workflows/npm-bru-cli.yml
@@ -26,7 +26,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -43,7 +43,7 @@ jobs:
bru run --env Prod --output junit.xml --format junit
- name: Publish Test Report
- uses: dorny/test-reporter@v1
+ uses: dorny/test-reporter@v2
if: success() || failure()
with:
name: Test Report
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index aec3d68a0..d218fc65a 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -5,14 +5,13 @@ on:
pull_request:
branches: [main]
-permissions:
- contents: read
-
jobs:
unit-test:
name: Unit Tests
timeout-minutes: 60
runs-on: ubuntu-latest
+ permissions:
+ contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -52,6 +51,10 @@ jobs:
cli-test:
name: CLI Tests
runs-on: ubuntu-latest
+ permissions:
+ checks: write
+ pull-requests: write
+ contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
diff --git a/contributing.md b/contributing.md
index dbfb9b5eb..582d48be2 100644
--- a/contributing.md
+++ b/contributing.md
@@ -86,11 +86,11 @@ find . -type f -name "package-lock.json" -delete
### Testing
```bash
-# bruno-schema
+# run bruno-schema tests
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# run tests over all workspaces
+npm test --workspaces --if-present
```
### Raising Pull Requests
diff --git a/docs/contributing/contributing_bn.md b/docs/contributing/contributing_bn.md
index a5d765a1e..f6a38515e 100644
--- a/docs/contributing/contributing_bn.md
+++ b/docs/contributing/contributing_bn.md
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### Testing (পরীক্ষা)
```bash
-# bruno-schema
+# ব্রুনো-স্কিমা পরীক্ষা চালান
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# সমস্ত কর্মক্ষেত্রে পরীক্ষা চালান
+npm test --workspaces --if-present
```
### Raising Pull Request (পুল অনুরোধ উত্থাপন)
diff --git a/docs/contributing/contributing_cn.md b/docs/contributing/contributing_cn.md
index f355feba6..a4f27b61f 100644
--- a/docs/contributing/contributing_cn.md
+++ b/docs/contributing/contributing_cn.md
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### 测试
```bash
-# bruno-schema
+# 运行 bruno-schema 测试
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# 在所有工作区上运行测试
+npm test --workspaces --if-present
```
### 提交 Pull Request
diff --git a/docs/contributing/contributing_de.md b/docs/contributing/contributing_de.md
index 2983e8d26..017e07d6a 100644
--- a/docs/contributing/contributing_de.md
+++ b/docs/contributing/contributing_de.md
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
### Testen
```bash
-# bruno-schema
+# Führen Sie Bruno-Schema-Tests aus
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# Führen Sie Tests für alle Arbeitsbereiche durch
+npm test --workspaces --if-present
```
diff --git a/docs/contributing/contributing_es.md b/docs/contributing/contributing_es.md
index f84090a10..f080a765c 100644
--- a/docs/contributing/contributing_es.md
+++ b/docs/contributing/contributing_es.md
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### Pruebas
```bash
-# bruno-schema
+# ejecutar pruebas de esquema bruno
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# ejecutar pruebas en todos los espacios de trabajo
+npm test --workspaces --if-present
```
### Crea un Pull Request
diff --git a/docs/contributing/contributing_fr.md b/docs/contributing/contributing_fr.md
index 9b0eacda1..8e5df051c 100644
--- a/docs/contributing/contributing_fr.md
+++ b/docs/contributing/contributing_fr.md
@@ -73,11 +73,11 @@ find . -type f -name "package-lock.json" -delete
### Tests
```bash
-# bruno-schema
+# exécuter des tests de schéma bruno
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# exécuter des tests sur tous les espaces de travail
+npm test --workspaces --if-present
```
### Ouvrir une Pull Request
diff --git a/docs/contributing/contributing_hi.md b/docs/contributing/contributing_hi.md
index 38f39c5ec..6af26f1d2 100644
--- a/docs/contributing/contributing_hi.md
+++ b/docs/contributing/contributing_hi.md
@@ -65,11 +65,11 @@ find . -type f -name "package-lock.json" -delete
### परिक्षण
```bash
-# bruno-schema
+# ब्रूनो-स्कीमा परीक्षण चलाएँ
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# सभी कार्यस्थानों पर परीक्षण चलाएँ
+npm test --workspaces --if-present
```
### पुल अनुरोध प्रक्रिया
diff --git a/docs/contributing/contributing_it.md b/docs/contributing/contributing_it.md
index 18b7518f1..f38c02300 100644
--- a/docs/contributing/contributing_it.md
+++ b/docs/contributing/contributing_it.md
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
### Tests
```bash
-# bruno-schema
+# esegui i test dello schema bruno
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# esegui test su tutti gli spazi di lavoro
+npm test --workspaces --if-present
```
diff --git a/docs/contributing/contributing_ja.md b/docs/contributing/contributing_ja.md
index da2197883..19da3dfda 100644
--- a/docs/contributing/contributing_ja.md
+++ b/docs/contributing/contributing_ja.md
@@ -65,11 +65,11 @@ find . -type f -name "package-lock.json" -delete
### テストを動かすには
```bash
-# bruno-schema
+# ブルーノスキーマのテストを実行します
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# すべてのワークスペースでテストを実行します
+npm test --workspaces --if-present
```
### プルリクエストの手順
diff --git a/docs/contributing/contributing_kr.md b/docs/contributing/contributing_kr.md
index 70e5a12f2..f5a100510 100644
--- a/docs/contributing/contributing_kr.md
+++ b/docs/contributing/contributing_kr.md
@@ -66,11 +66,11 @@ find . -type f -name "package-lock.json" -delete
### 테스팅
```bash
-# bruno-schema
+# bruno-schema 테스트 실행
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# 모든 작업 공간에서 테스트 실행
+npm test --workspaces --if-present
```
### Pull Requests 요청
diff --git a/docs/contributing/contributing_nl.md b/docs/contributing/contributing_nl.md
index 8c8d9402e..707e342c4 100644
--- a/docs/contributing/contributing_nl.md
+++ b/docs/contributing/contributing_nl.md
@@ -65,11 +65,11 @@ find . -type f -name "package-lock.json" -delete
### Testen
```bash
-# bruno-schema
+# voer bruno-schema tests uit
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# voer tests uit over alle werkruimten
+npm test --workspaces --if-present
```
### Pull Requests indienen
diff --git a/docs/contributing/contributing_pl.md b/docs/contributing/contributing_pl.md
index 085407db5..be600d5a5 100644
--- a/docs/contributing/contributing_pl.md
+++ b/docs/contributing/contributing_pl.md
@@ -71,11 +71,11 @@ find . -type f -name "package-lock.json" -delete
### Testowanie
```bash
-# bruno-schema
+# uruchom testy bruno-schema
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# uruchom testy we wszystkich przestrzeniach roboczych
+npm test --workspaces --if-present
```
### Tworzenie Pull Request
diff --git a/docs/contributing/contributing_pt_br.md b/docs/contributing/contributing_pt_br.md
index 513dfcc3c..3a290c85b 100644
--- a/docs/contributing/contributing_pt_br.md
+++ b/docs/contributing/contributing_pt_br.md
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### Testando
```bash
-# bruno-schema
+# executar testes do bruno-schema
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# executar testes em todos os ambientes de trabalho
+npm test --workspaces --if-present
```
### Envio de Pull Request
diff --git a/docs/contributing/contributing_ro.md b/docs/contributing/contributing_ro.md
index ee2102615..17c211e5b 100644
--- a/docs/contributing/contributing_ro.md
+++ b/docs/contributing/contributing_ro.md
@@ -64,11 +64,11 @@ find . -type f -name "package-lock.json" -delete
### Testarea
```shell
-# bruno-schema
+# executați teste bruno-schema
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# executați teste peste toate spațiile de lucru
+npm test --workspaces --if-present
```
### Crearea unui Pull Request
diff --git a/docs/contributing/contributing_ru.md b/docs/contributing/contributing_ru.md
index ab9679534..8b9f343c4 100644
--- a/docs/contributing/contributing_ru.md
+++ b/docs/contributing/contributing_ru.md
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
### Тестирование
```bash
-# bruno-schema
+# запустите тесты bruno-schema
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# запустите тесты во всех рабочих пространствах
+npm test --workspaces --if-present
```
diff --git a/docs/contributing/contributing_sk.md b/docs/contributing/contributing_sk.md
index e2c4aabe7..10d2ce0f5 100644
--- a/docs/contributing/contributing_sk.md
+++ b/docs/contributing/contributing_sk.md
@@ -67,11 +67,11 @@ find . -type f -name "package-lock.json" -delete
### Testovanie
````bash
-# bruno-schema
+# spustiť bruno-schema testy
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# spustiť testy vo všetkých pracovných priestoroch
+npm test --workspaces --if-present
```
### Vyrobenie Pull Request
diff --git a/docs/contributing/contributing_tr.md b/docs/contributing/contributing_tr.md
index 26531cabd..b80a085a2 100644
--- a/docs/contributing/contributing_tr.md
+++ b/docs/contributing/contributing_tr.md
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### Test
```bash
-# bruno-schema
+# bruno-schema testlerini çalıştır
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# tüm çalışma alanlarında testleri çalıştır
+npm test --workspaces --if-present
```
### Pull Request Oluşturma
diff --git a/docs/contributing/contributing_ua.md b/docs/contributing/contributing_ua.md
index 88f9ab595..2b89bdc36 100644
--- a/docs/contributing/contributing_ua.md
+++ b/docs/contributing/contributing_ua.md
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
### Тестування
```bash
-# bruno-schema
+# запустити тести bruno-schema
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# запустити тести у всіх робочих просторах
+npm test --workspaces --if-present
```
diff --git a/docs/contributing/contributing_zhtw.md b/docs/contributing/contributing_zhtw.md
index f061adb4a..e2ad6ea9a 100644
--- a/docs/contributing/contributing_zhtw.md
+++ b/docs/contributing/contributing_zhtw.md
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### 測試
```bash
-# bruno-schema
+# 執行布魯諾架構測試
npm test --workspace=packages/bruno-schema
-# bruno-lang
-npm test --workspace=packages/bruno-lang
+# 對所有工作區執行測試
+npm test --workspaces --if-present
```
### 發送 Pull Request
diff --git a/docs/publishing/publishin_nl.md b/docs/publishing/publishing_nl.md
similarity index 100%
rename from docs/publishing/publishin_nl.md
rename to docs/publishing/publishing_nl.md
diff --git a/docs/readme/readme_ua.md b/docs/readme/readme_ua.md
index 7fae0d6f5..a8a4bfd5a 100644
--- a/docs/readme/readme_ua.md
+++ b/docs/readme/readme_ua.md
@@ -29,13 +29,13 @@
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
-Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.
+Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статусy кво, запровадженого інструментами на кшталт Postman.
Bruno зберігає ваші колекції напряму у теці на вашому диску. Він використовує текстову мову розмітки Bru для збереження інформації про ваші API запити.
Ви можете використовувати git або будь-яку іншу систему контролю версій щоб спільно працювати над вашими колекціями API запитів.
-Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Взнати більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)
+Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Дізнатись більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)

@@ -69,13 +69,13 @@ Bruno є повністю автономним. Немає жодних план
### Поділитись відгуками 📣
-Якщо Bruno допоміг вам у вашій роботі і вашим командам, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)
+Якщо Bruno допоміг у роботі вам або вашій команді, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)
### Зробити свій внесок 👩💻🧑💻
Я радий що ви бажаєте покращити Bruno. Будь ласка переглянте [інструкцію по контрибуції](../contributing/contributing_ua.md)
-Навіть якщо ви не можете зробити свій внесок пишучи програмний код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
+Навіть якщо ви не можете зробити свій внесок пишучи код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
### Автори
diff --git a/package-lock.json b/package-lock.json
index 9773e05c0..1d3d36378 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,7 +30,7 @@
"pretty-quick": "^3.1.3",
"randomstring": "^1.2.2",
"rimraf": "^6.0.1",
- "ts-jest": "^29.0.5"
+ "ts-jest": "^29.2.6"
}
},
"node_modules/@alloc/quick-lru": {
@@ -184,58 +184,6 @@
"node": ">=14.0.0"
}
},
- "node_modules/@aws-sdk/client-cognito-identity": {
- "version": "3.658.1",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.658.1.tgz",
- "integrity": "sha512-MCYLKmNy0FlNT9TvXfOxj0jh+ZQq+G9qEy/VZqu3JsQSgiFvFRdzgzcbQ9gQx7fZrDC/TPdABOTh483zI4cu9g==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-crypto/sha256-browser": "5.2.0",
- "@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/client-sso-oidc": "3.658.1",
- "@aws-sdk/client-sts": "3.658.1",
- "@aws-sdk/core": "3.658.1",
- "@aws-sdk/credential-provider-node": "3.658.1",
- "@aws-sdk/middleware-host-header": "3.654.0",
- "@aws-sdk/middleware-logger": "3.654.0",
- "@aws-sdk/middleware-recursion-detection": "3.654.0",
- "@aws-sdk/middleware-user-agent": "3.654.0",
- "@aws-sdk/region-config-resolver": "3.654.0",
- "@aws-sdk/types": "3.654.0",
- "@aws-sdk/util-endpoints": "3.654.0",
- "@aws-sdk/util-user-agent-browser": "3.654.0",
- "@aws-sdk/util-user-agent-node": "3.654.0",
- "@smithy/config-resolver": "^3.0.8",
- "@smithy/core": "^2.4.6",
- "@smithy/fetch-http-handler": "^3.2.8",
- "@smithy/hash-node": "^3.0.6",
- "@smithy/invalid-dependency": "^3.0.6",
- "@smithy/middleware-content-length": "^3.0.8",
- "@smithy/middleware-endpoint": "^3.1.3",
- "@smithy/middleware-retry": "^3.0.21",
- "@smithy/middleware-serde": "^3.0.6",
- "@smithy/middleware-stack": "^3.0.6",
- "@smithy/node-config-provider": "^3.1.7",
- "@smithy/node-http-handler": "^3.2.3",
- "@smithy/protocol-http": "^4.1.3",
- "@smithy/smithy-client": "^3.3.5",
- "@smithy/types": "^3.4.2",
- "@smithy/url-parser": "^3.0.6",
- "@smithy/util-base64": "^3.0.0",
- "@smithy/util-body-length-browser": "^3.0.0",
- "@smithy/util-body-length-node": "^3.0.0",
- "@smithy/util-defaults-mode-browser": "^3.0.21",
- "@smithy/util-defaults-mode-node": "^3.0.21",
- "@smithy/util-endpoints": "^2.1.2",
- "@smithy/util-middleware": "^3.0.6",
- "@smithy/util-retry": "^3.0.6",
- "@smithy/util-utf8": "^3.0.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
"node_modules/@aws-sdk/client-sso": {
"version": "3.658.1",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.658.1.tgz",
@@ -410,22 +358,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@aws-sdk/credential-provider-cognito-identity": {
- "version": "3.658.1",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.658.1.tgz",
- "integrity": "sha512-JY4rZ4e2emL7PNHCU7F/BQV8PpQGEBZLkEoPD55RO4CitaIhlVZRpUCGLih+0Hw4MOnTUqJdfQBM+qZk6G+Now==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/client-cognito-identity": "3.658.1",
- "@aws-sdk/types": "3.654.0",
- "@smithy/property-provider": "^3.1.6",
- "@smithy/types": "^3.4.2",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
"node_modules/@aws-sdk/credential-provider-env": {
"version": "3.654.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.654.0.tgz",
@@ -561,33 +493,6 @@
"@aws-sdk/client-sts": "^3.654.0"
}
},
- "node_modules/@aws-sdk/credential-providers": {
- "version": "3.658.1",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.658.1.tgz",
- "integrity": "sha512-lfXA6kZS6GHyi/67EbfrKdLoqHR6j7G35eFwaqxyNkfMhNBpAF0eZK3SYiwnzdR9+Wb/enTFawYiFbG5R+dQzA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/client-cognito-identity": "3.658.1",
- "@aws-sdk/client-sso": "3.658.1",
- "@aws-sdk/client-sts": "3.658.1",
- "@aws-sdk/credential-provider-cognito-identity": "3.658.1",
- "@aws-sdk/credential-provider-env": "3.654.0",
- "@aws-sdk/credential-provider-http": "3.658.1",
- "@aws-sdk/credential-provider-ini": "3.658.1",
- "@aws-sdk/credential-provider-node": "3.658.1",
- "@aws-sdk/credential-provider-process": "3.654.0",
- "@aws-sdk/credential-provider-sso": "3.658.1",
- "@aws-sdk/credential-provider-web-identity": "3.654.0",
- "@aws-sdk/types": "3.654.0",
- "@smithy/credential-provider-imds": "^3.2.3",
- "@smithy/property-provider": "^3.1.6",
- "@smithy/types": "^3.4.2",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
"node_modules/@aws-sdk/middleware-host-header": {
"version": "3.654.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.654.0.tgz",
@@ -648,6 +553,785 @@
"node": ">=16.0.0"
}
},
+ "node_modules/@aws-sdk/nested-clients": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.750.0.tgz",
+ "integrity": "sha512-OH68BRF0rt9nDloq4zsfeHI0G21lj11a66qosaljtEP66PWm7tQ06feKbFkXHT5E1K3QhJW3nVyK8v2fEBY5fg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/middleware-host-header": "3.734.0",
+ "@aws-sdk/middleware-logger": "3.734.0",
+ "@aws-sdk/middleware-recursion-detection": "3.734.0",
+ "@aws-sdk/middleware-user-agent": "3.750.0",
+ "@aws-sdk/region-config-resolver": "3.734.0",
+ "@aws-sdk/types": "3.734.0",
+ "@aws-sdk/util-endpoints": "3.743.0",
+ "@aws-sdk/util-user-agent-browser": "3.734.0",
+ "@aws-sdk/util-user-agent-node": "3.750.0",
+ "@smithy/config-resolver": "^4.0.1",
+ "@smithy/core": "^3.1.4",
+ "@smithy/fetch-http-handler": "^5.0.1",
+ "@smithy/hash-node": "^4.0.1",
+ "@smithy/invalid-dependency": "^4.0.1",
+ "@smithy/middleware-content-length": "^4.0.1",
+ "@smithy/middleware-endpoint": "^4.0.5",
+ "@smithy/middleware-retry": "^4.0.6",
+ "@smithy/middleware-serde": "^4.0.2",
+ "@smithy/middleware-stack": "^4.0.1",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/node-http-handler": "^4.0.2",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/smithy-client": "^4.1.5",
+ "@smithy/types": "^4.1.0",
+ "@smithy/url-parser": "^4.0.1",
+ "@smithy/util-base64": "^4.0.0",
+ "@smithy/util-body-length-browser": "^4.0.0",
+ "@smithy/util-body-length-node": "^4.0.0",
+ "@smithy/util-defaults-mode-browser": "^4.0.6",
+ "@smithy/util-defaults-mode-node": "^4.0.6",
+ "@smithy/util-endpoints": "^3.0.1",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-retry": "^4.0.1",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/core": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.750.0.tgz",
+ "integrity": "sha512-bZ5K7N5L4+Pa2epbVpUQqd1XLG2uU8BGs/Sd+2nbgTf+lNQJyIxAg/Qsrjz9MzmY8zzQIeRQEkNmR6yVAfCmmQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/core": "^3.1.4",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/signature-v4": "^5.0.1",
+ "@smithy/smithy-client": "^4.1.5",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "fast-xml-parser": "4.4.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-host-header": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.734.0.tgz",
+ "integrity": "sha512-LW7RRgSOHHBzWZnigNsDIzu3AiwtjeI2X66v+Wn1P1u+eXssy1+up4ZY/h+t2sU4LU36UvEf+jrZti9c6vRnFw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-logger": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.734.0.tgz",
+ "integrity": "sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-recursion-detection": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.734.0.tgz",
+ "integrity": "sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/middleware-user-agent": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.750.0.tgz",
+ "integrity": "sha512-YYcslDsP5+2NZoN3UwuhZGkhAHPSli7HlJHBafBrvjGV/I9f8FuOO1d1ebxGdEP4HyRXUGyh+7Ur4q+Psk0ryw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@aws-sdk/util-endpoints": "3.743.0",
+ "@smithy/core": "^3.1.4",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/region-config-resolver": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.734.0.tgz",
+ "integrity": "sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-config-provider": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/types": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz",
+ "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.743.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.743.0.tgz",
+ "integrity": "sha512-sN1l559zrixeh5x+pttrnd0A3+r34r0tmPkJ/eaaMaAzXqsmKU/xYre9K3FNnsSS1J1k4PEfk/nHDTVUgFYjnw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-endpoints": "^3.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-user-agent-browser": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.734.0.tgz",
+ "integrity": "sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/types": "^4.1.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-user-agent-node": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.750.0.tgz",
+ "integrity": "sha512-84HJj9G9zbrHX2opLk9eHfDceB+UIHVrmflMzWHpsmo9fDuro/flIBqaVDlE021Osj6qIM0SJJcnL6s23j7JEw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-user-agent": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "aws-crt": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws-crt": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/abort-controller": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.1.tgz",
+ "integrity": "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/config-resolver": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.0.1.tgz",
+ "integrity": "sha512-Igfg8lKu3dRVkTSEm98QpZUvKEOa71jDX4vKRcvJVyRc3UgN3j7vFMf0s7xLQhYmKa8kyJGQgUJDOV5V3neVlQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-config-provider": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/core": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.1.5.tgz",
+ "integrity": "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/middleware-serde": "^4.0.2",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-body-length-browser": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-stream": "^4.1.2",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/credential-provider-imds": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.1.tgz",
+ "integrity": "sha512-l/qdInaDq1Zpznpmev/+52QomsJNZ3JkTl5yrTl02V6NBgJOQ4LY0SFw/8zsMwj3tLe8vqiIuwF6nxaEwgf6mg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/url-parser": "^4.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/fetch-http-handler": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.1.tgz",
+ "integrity": "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/querystring-builder": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-base64": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/hash-node": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.1.tgz",
+ "integrity": "sha512-TJ6oZS+3r2Xu4emVse1YPB3Dq3d8RkZDKcPr71Nj/lJsdAP1c7oFzYqEn1IBc915TsgLl2xIJNuxCz+gLbLE0w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-buffer-from": "^4.0.0",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/invalid-dependency": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.1.tgz",
+ "integrity": "sha512-gdudFPf4QRQ5pzj7HEnu6FhKRi61BfH/Gk5Yf6O0KiSbr1LlVhgjThcvjdu658VE6Nve8vaIWB8/fodmS1rBPQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/is-array-buffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz",
+ "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-content-length": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.1.tgz",
+ "integrity": "sha512-OGXo7w5EkB5pPiac7KNzVtfCW2vKBTZNuCctn++TTSOMpe6RZO/n6WEC1AxJINn3+vWLKW49uad3lo/u0WJ9oQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-endpoint": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.0.6.tgz",
+ "integrity": "sha512-ftpmkTHIFqgaFugcjzLZv3kzPEFsBFSnq1JsIkr2mwFzCraZVhQk2gqN51OOeRxqhbPTkRFj39Qd2V91E/mQxg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.1.5",
+ "@smithy/middleware-serde": "^4.0.2",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/url-parser": "^4.0.1",
+ "@smithy/util-middleware": "^4.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-retry": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.0.7.tgz",
+ "integrity": "sha512-58j9XbUPLkqAcV1kHzVX/kAR16GT+j7DUZJqwzsxh1jtz7G82caZiGyyFgUvogVfNTg3TeAOIJepGc8TXF4AVQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/service-error-classification": "^4.0.1",
+ "@smithy/smithy-client": "^4.1.6",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-retry": "^4.0.1",
+ "tslib": "^2.6.2",
+ "uuid": "^9.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-serde": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.2.tgz",
+ "integrity": "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/middleware-stack": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.1.tgz",
+ "integrity": "sha512-dHwDmrtR/ln8UTHpaIavRSzeIk5+YZTBtLnKwDW3G2t6nAupCiQUvNzNoHBpik63fwUaJPtlnMzXbQrNFWssIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-config-provider": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.0.1.tgz",
+ "integrity": "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.3.tgz",
+ "integrity": "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/abort-controller": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/querystring-builder": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/property-provider": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.1.tgz",
+ "integrity": "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/protocol-http": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.0.1.tgz",
+ "integrity": "sha512-TE4cpj49jJNB/oHyh/cRVEgNZaoPaxd4vteJNB0yGidOCVR0jCw/hjPVsT8Q8FRmj8Bd3bFZt8Dh7xGCT+xMBQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/querystring-builder": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.1.tgz",
+ "integrity": "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-uri-escape": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/querystring-parser": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.1.tgz",
+ "integrity": "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/service-error-classification": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.1.tgz",
+ "integrity": "sha512-3JNjBfOWpj/mYfjXJHB4Txc/7E4LVq32bwzE7m28GN79+M1f76XHflUaSUkhOriprPDzev9cX/M+dEB80DNDKA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/shared-ini-file-loader": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.1.tgz",
+ "integrity": "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/signature-v4": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.0.1.tgz",
+ "integrity": "sha512-nCe6fQ+ppm1bQuw5iKoeJ0MJfz2os7Ic3GBjOkLOPtavbD1ONoyE3ygjBfz2ythFWm4YnRm6OxW+8p/m9uCoIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^4.0.0",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-hex-encoding": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-uri-escape": "^4.0.0",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/smithy-client": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.1.6.tgz",
+ "integrity": "sha512-UYDolNg6h2O0L+cJjtgSyKKvEKCOa/8FHYJnBobyeoeWDmNpXjwOAtw16ezyeu1ETuuLEOZbrynK0ZY1Lx9Jbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.1.5",
+ "@smithy/middleware-endpoint": "^4.0.6",
+ "@smithy/middleware-stack": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-stream": "^4.1.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/types": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.1.0.tgz",
+ "integrity": "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/url-parser": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.1.tgz",
+ "integrity": "sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/querystring-parser": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-base64": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz",
+ "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.0.0",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-body-length-browser": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz",
+ "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-body-length-node": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz",
+ "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-buffer-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz",
+ "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-config-provider": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz",
+ "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-defaults-mode-browser": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.7.tgz",
+ "integrity": "sha512-CZgDDrYHLv0RUElOsmZtAnp1pIjwDVCSuZWOPhIOBvG36RDfX1Q9+6lS61xBf+qqvHoqRjHxgINeQz47cYFC2Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/smithy-client": "^4.1.6",
+ "@smithy/types": "^4.1.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-defaults-mode-node": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.7.tgz",
+ "integrity": "sha512-79fQW3hnfCdrfIi1soPbK3zmooRFnLpSx3Vxi6nUlqaaQeC5dm8plt4OTNDNqEEEDkvKghZSaoti684dQFVrGQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/config-resolver": "^4.0.1",
+ "@smithy/credential-provider-imds": "^4.0.1",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/smithy-client": "^4.1.6",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-endpoints": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.1.tgz",
+ "integrity": "sha512-zVdUENQpdtn9jbpD9SCFK4+aSiavRb9BxEtw9ZGUR1TYo6bBHbIoi7VkrFQ0/RwZlzx0wRBaRmPclj8iAoJCLA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-hex-encoding": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz",
+ "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-middleware": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.1.tgz",
+ "integrity": "sha512-HiLAvlcqhbzhuiOa0Lyct5IIlyIz0PQO5dnMlmQ/ubYM46dPInB+3yQGkfxsk6Q24Y0n3/JmcA1v5iEhmOF5mA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-retry": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.1.tgz",
+ "integrity": "sha512-WmRHqNVwn3kI3rKk1LsKcVgPBG6iLTBGC1iYOV3GQegwJ3E8yjzHytPt26VNzOWr1qu0xE03nK0Ug8S7T7oufw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/service-error-classification": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-stream": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.1.2.tgz",
+ "integrity": "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/fetch-http-handler": "^5.0.1",
+ "@smithy/node-http-handler": "^4.0.3",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-base64": "^4.0.0",
+ "@smithy/util-buffer-from": "^4.0.0",
+ "@smithy/util-hex-encoding": "^4.0.0",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-uri-escape": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz",
+ "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/util-utf8": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz",
+ "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@aws-sdk/region-config-resolver": {
"version": "3.654.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.654.0.tgz",
@@ -7191,6 +7875,13 @@
"@types/estree": "*"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/verror": {
"version": "1.10.10",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz",
@@ -8578,7 +9269,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
- "dev": true,
"license": "ISC"
},
"node_modules/boolean": {
@@ -9409,6 +10099,229 @@
"node": "*"
}
},
+ "node_modules/cheerio": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
+ "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
+ "license": "MIT",
+ "dependencies": {
+ "cheerio-select": "^2.1.0",
+ "dom-serializer": "^2.0.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.1.0",
+ "encoding-sniffer": "^0.2.0",
+ "htmlparser2": "^9.1.0",
+ "parse5": "^7.1.2",
+ "parse5-htmlparser2-tree-adapter": "^7.0.0",
+ "parse5-parser-stream": "^7.1.2",
+ "undici": "^6.19.5",
+ "whatwg-mimetype": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18.17"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
+ }
+ },
+ "node_modules/cheerio-select": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
+ "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-select": "^5.1.0",
+ "css-what": "^6.1.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cheerio-select/node_modules/css-select": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
+ "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cheerio-select/node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/cheerio-select/node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/cheerio-select/node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/cheerio-select/node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/cheerio-select/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/cheerio/node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/cheerio/node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/cheerio/node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/cheerio/node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/cheerio/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/cheerio/node_modules/htmlparser2": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
+ "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.1.0",
+ "entities": "^4.5.0"
+ }
+ },
+ "node_modules/cheerio/node_modules/parse5": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
+ "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -10574,7 +11487,6 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
- "dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
@@ -11247,6 +12159,15 @@
"domelementtype": "1"
}
},
+ "node_modules/dompurify": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
+ "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/domutils": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
@@ -11715,6 +12636,19 @@
"iconv-lite": "^0.6.2"
}
},
+ "node_modules/encoding-sniffer": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
+ "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "^0.6.3",
+ "whatwg-encoding": "^3.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
+ }
+ },
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -12073,48 +13007,6 @@
"dev": true,
"license": "Apache-2.0"
},
- "node_modules/express": {
- "version": "4.21.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
- "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
- "license": "MIT",
- "dependencies": {
- "accepts": "~1.3.8",
- "array-flatten": "1.1.1",
- "body-parser": "1.20.3",
- "content-disposition": "0.5.4",
- "content-type": "~1.0.4",
- "cookie": "0.7.1",
- "cookie-signature": "1.0.6",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "finalhandler": "1.3.1",
- "fresh": "0.5.2",
- "http-errors": "2.0.0",
- "merge-descriptors": "1.0.3",
- "methods": "~1.1.2",
- "on-finished": "2.4.1",
- "parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
- "proxy-addr": "~2.0.7",
- "qs": "6.13.0",
- "range-parser": "~1.2.1",
- "safe-buffer": "5.2.1",
- "send": "0.19.0",
- "serve-static": "1.16.2",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "type-is": "~1.6.18",
- "utils-merge": "1.0.1",
- "vary": "~1.1.2"
- },
- "engines": {
- "node": ">= 0.10.0"
- }
- },
"node_modules/express-basic-auth": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz",
@@ -12124,30 +13016,6 @@
"basic-auth": "^2.0.1"
}
},
- "node_modules/express/node_modules/cookie": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
- "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/express/node_modules/qs": {
- "version": "6.13.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
- "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "side-channel": "^1.0.6"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -15669,24 +16537,6 @@
"graceful-fs": "^4.1.6"
}
},
- "node_modules/jsonpath-plus": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.2.0.tgz",
- "integrity": "sha512-T9V+8iNYKFL2n2rF+w02LBOT2JjDnTjioaNFrxRy0Bv1y/hNsqR/EBK7Ojy2ythRHwmz2cRIls+9JitQGZC/sw==",
- "license": "MIT",
- "dependencies": {
- "@jsep-plugin/assignment": "^1.3.0",
- "@jsep-plugin/regex": "^1.0.4",
- "jsep": "^1.4.0"
- },
- "bin": {
- "jsonpath": "bin/jsonpath-cli.js",
- "jsonpath-plus": "bin/jsonpath-cli.js"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
@@ -16911,6 +17761,18 @@
"node": "*"
}
},
+ "node_modules/moment-timezone": {
+ "version": "0.5.47",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.47.tgz",
+ "integrity": "sha512-UbNt/JAWS0m/NJOebR0QMRHBk0hu03r5dx9GK8Cs0AS3I81yDcOc9k+DytPItgVvBP7J6Mf6U2n3BPAacAV9oA==",
+ "license": "MIT",
+ "dependencies": {
+ "moment": "^2.29.4"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/mousetrap": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",
@@ -17366,7 +18228,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
- "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
@@ -17771,6 +18632,106 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/parse5-htmlparser2-tree-adapter": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
+ "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
+ "license": "MIT",
+ "dependencies": {
+ "domhandler": "^5.0.3",
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/parse5-htmlparser2-tree-adapter/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
+ "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-parser-stream": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
+ "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
+ "license": "MIT",
+ "dependencies": {
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-parser-stream/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/parse5-parser-stream/node_modules/parse5": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
+ "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -17877,12 +18838,6 @@
"node": ">=16 || 14 >=14.17"
}
},
- "node_modules/path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
- "license": "MIT"
- },
"node_modules/path/node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
@@ -21159,7 +22114,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
- "dev": true,
"license": "ISC"
},
"node_modules/scheduler": {
@@ -23060,9 +24014,9 @@
"license": "Apache-2.0"
},
"node_modules/ts-jest": {
- "version": "29.2.5",
- "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz",
- "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==",
+ "version": "29.2.6",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz",
+ "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -23073,7 +24027,7 @@
"json5": "^2.2.3",
"lodash.memoize": "^4.1.2",
"make-error": "^1.3.6",
- "semver": "^7.6.3",
+ "semver": "^7.7.1",
"yargs-parser": "^21.1.1"
},
"bin": {
@@ -23109,9 +24063,9 @@
}
},
"node_modules/ts-jest/node_modules/semver": {
- "version": "7.6.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
- "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -23218,6 +24172,15 @@
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"license": "MIT"
},
+ "node_modules/undici": {
+ "version": "6.21.1",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
+ "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.17"
+ }
+ },
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
@@ -23746,6 +24709,27 @@
"node": ">=10.13.0"
}
},
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
@@ -23887,6 +24871,28 @@
"node": ">= 16"
}
},
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xml2js/node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/xmlbuilder": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
@@ -24008,7 +25014,7 @@
},
"packages/bruno-app": {
"name": "@usebruno/app",
- "version": "0.3.0",
+ "version": "1.39.0",
"dependencies": {
"@babel/preset-env": "^7.26.0",
"@fontsource/inter": "^5.0.15",
@@ -24019,11 +25025,11 @@
"@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.7.0",
- "axios": "1.7.5",
"classnames": "^2.3.1",
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",
"cookie": "0.7.1",
+ "dompurify": "^3.2.4",
"escape-html": "^1.0.3",
"file": "^0.2.2",
"file-dialog": "^0.0.8",
@@ -24033,19 +25039,22 @@
"graphiql": "3.7.1",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
- "httpsnippet": "^3.0.6",
+ "httpsnippet": "^3.0.9",
"i18next": "24.1.2",
+ "iconv-lite": "^0.6.3",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",
"jshint": "^2.13.6",
"json5": "^2.2.3",
"jsonc-parser": "^3.2.1",
- "jsonpath-plus": "10.2.0",
+ "jsonpath-plus": "^10.3.0",
"know-your-http-well": "^0.5.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
+ "moment": "^2.30.1",
+ "moment-timezone": "^0.5.47",
"mousetrap": "^1.6.5",
"nanoid": "3.3.8",
"path": "^0.12.7",
@@ -24068,6 +25077,7 @@
"react-redux": "^7.2.9",
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",
+ "semver": "^7.7.1",
"strip-json-comments": "^5.0.1",
"styled-components": "^5.3.3",
"system": "^2.0.1",
@@ -24107,6 +25117,24 @@
"node": ">= 0.6"
}
},
+ "packages/bruno-app/node_modules/jsonpath-plus": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz",
+ "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jsep-plugin/assignment": "^1.3.0",
+ "@jsep-plugin/regex": "^1.0.4",
+ "jsep": "^1.4.0"
+ },
+ "bin": {
+ "jsonpath": "bin/jsonpath-cli.js",
+ "jsonpath-plus": "bin/jsonpath-cli.js"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"packages/bruno-app/node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
@@ -24125,18 +25153,29 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "packages/bruno-app/node_modules/semver": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"packages/bruno-cli": {
"name": "@usebruno/cli",
"version": "1.16.0",
"license": "MIT",
"dependencies": {
- "@aws-sdk/credential-providers": "3.658.1",
+ "@aws-sdk/credential-providers": "3.750.0",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
"@usebruno/vm2": "^3.9.13",
"aws4-axios": "^3.3.0",
- "axios": "1.7.5",
+ "axios": "^1.8.3",
"axios-ntlm": "^1.4.2",
"chai": "^4.3.7",
"chalk": "^3.0.0",
@@ -24157,6 +25196,1044 @@
"bru": "bin/bru.js"
}
},
+ "packages/bruno-cli/node_modules/@aws-sdk/client-cognito-identity": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.750.0.tgz",
+ "integrity": "sha512-ia5+l7U1ludU/YqQAnEMj5DIp1kfMTu14lUOMG3uTIwTcj8OjkCvAe6BuM0OY6zd8enrJYWLqIqxuKPOWw4I7Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/credential-provider-node": "3.750.0",
+ "@aws-sdk/middleware-host-header": "3.734.0",
+ "@aws-sdk/middleware-logger": "3.734.0",
+ "@aws-sdk/middleware-recursion-detection": "3.734.0",
+ "@aws-sdk/middleware-user-agent": "3.750.0",
+ "@aws-sdk/region-config-resolver": "3.734.0",
+ "@aws-sdk/types": "3.734.0",
+ "@aws-sdk/util-endpoints": "3.743.0",
+ "@aws-sdk/util-user-agent-browser": "3.734.0",
+ "@aws-sdk/util-user-agent-node": "3.750.0",
+ "@smithy/config-resolver": "^4.0.1",
+ "@smithy/core": "^3.1.4",
+ "@smithy/fetch-http-handler": "^5.0.1",
+ "@smithy/hash-node": "^4.0.1",
+ "@smithy/invalid-dependency": "^4.0.1",
+ "@smithy/middleware-content-length": "^4.0.1",
+ "@smithy/middleware-endpoint": "^4.0.5",
+ "@smithy/middleware-retry": "^4.0.6",
+ "@smithy/middleware-serde": "^4.0.2",
+ "@smithy/middleware-stack": "^4.0.1",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/node-http-handler": "^4.0.2",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/smithy-client": "^4.1.5",
+ "@smithy/types": "^4.1.0",
+ "@smithy/url-parser": "^4.0.1",
+ "@smithy/util-base64": "^4.0.0",
+ "@smithy/util-body-length-browser": "^4.0.0",
+ "@smithy/util-body-length-node": "^4.0.0",
+ "@smithy/util-defaults-mode-browser": "^4.0.6",
+ "@smithy/util-defaults-mode-node": "^4.0.6",
+ "@smithy/util-endpoints": "^3.0.1",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-retry": "^4.0.1",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/client-sso": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.750.0.tgz",
+ "integrity": "sha512-y0Rx6pTQXw0E61CaptpZF65qNggjqOgymq/RYZU5vWba5DGQ+iqGt8Yq8s+jfBoBBNXshxq8l8Dl5Uq/JTY1wg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/middleware-host-header": "3.734.0",
+ "@aws-sdk/middleware-logger": "3.734.0",
+ "@aws-sdk/middleware-recursion-detection": "3.734.0",
+ "@aws-sdk/middleware-user-agent": "3.750.0",
+ "@aws-sdk/region-config-resolver": "3.734.0",
+ "@aws-sdk/types": "3.734.0",
+ "@aws-sdk/util-endpoints": "3.743.0",
+ "@aws-sdk/util-user-agent-browser": "3.734.0",
+ "@aws-sdk/util-user-agent-node": "3.750.0",
+ "@smithy/config-resolver": "^4.0.1",
+ "@smithy/core": "^3.1.4",
+ "@smithy/fetch-http-handler": "^5.0.1",
+ "@smithy/hash-node": "^4.0.1",
+ "@smithy/invalid-dependency": "^4.0.1",
+ "@smithy/middleware-content-length": "^4.0.1",
+ "@smithy/middleware-endpoint": "^4.0.5",
+ "@smithy/middleware-retry": "^4.0.6",
+ "@smithy/middleware-serde": "^4.0.2",
+ "@smithy/middleware-stack": "^4.0.1",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/node-http-handler": "^4.0.2",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/smithy-client": "^4.1.5",
+ "@smithy/types": "^4.1.0",
+ "@smithy/url-parser": "^4.0.1",
+ "@smithy/util-base64": "^4.0.0",
+ "@smithy/util-body-length-browser": "^4.0.0",
+ "@smithy/util-body-length-node": "^4.0.0",
+ "@smithy/util-defaults-mode-browser": "^4.0.6",
+ "@smithy/util-defaults-mode-node": "^4.0.6",
+ "@smithy/util-endpoints": "^3.0.1",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-retry": "^4.0.1",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/core": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.750.0.tgz",
+ "integrity": "sha512-bZ5K7N5L4+Pa2epbVpUQqd1XLG2uU8BGs/Sd+2nbgTf+lNQJyIxAg/Qsrjz9MzmY8zzQIeRQEkNmR6yVAfCmmQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/core": "^3.1.4",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/signature-v4": "^5.0.1",
+ "@smithy/smithy-client": "^4.1.5",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "fast-xml-parser": "4.4.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/credential-provider-cognito-identity": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.750.0.tgz",
+ "integrity": "sha512-TwBzrxgIWcQk846XFn0A9DHBHbfg4sHR3M2GL4E7NcffEkh7r642ILiLa58VvQjO2nB1tcOOFtRqbZvVOKexUw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-cognito-identity": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.750.0.tgz",
+ "integrity": "sha512-In6bsG0p/P31HcH4DBRKBbcDS/3SHvEPjfXV8ODPWZO/l3/p7IRoYBdQ07C9R+VMZU2D0+/Sc/DWK/TUNDk1+Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.750.0.tgz",
+ "integrity": "sha512-wFB9qqfa20AB0dElsQz5ZlZT5o+a+XzpEpmg0erylmGYqEOvh8NQWfDUVpRmQuGq9VbvW/8cIbxPoNqEbPtuWQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/fetch-http-handler": "^5.0.1",
+ "@smithy/node-http-handler": "^4.0.2",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/smithy-client": "^4.1.5",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-stream": "^4.1.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.750.0.tgz",
+ "integrity": "sha512-2YIZmyEr5RUd3uxXpxOLD9G67Bibm4I/65M6vKFP17jVMUT+R1nL7mKqmhEVO2p+BoeV+bwMyJ/jpTYG368PCg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/credential-provider-env": "3.750.0",
+ "@aws-sdk/credential-provider-http": "3.750.0",
+ "@aws-sdk/credential-provider-process": "3.750.0",
+ "@aws-sdk/credential-provider-sso": "3.750.0",
+ "@aws-sdk/credential-provider-web-identity": "3.750.0",
+ "@aws-sdk/nested-clients": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/credential-provider-imds": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.750.0.tgz",
+ "integrity": "sha512-THWHHAceLwsOiowPEmKyhWVDlEUxH07GHSw5AQFDvNQtGKOQl0HSIFO1mKObT2Q2Vqzji9Bq8H58SO5BFtNPRw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "3.750.0",
+ "@aws-sdk/credential-provider-http": "3.750.0",
+ "@aws-sdk/credential-provider-ini": "3.750.0",
+ "@aws-sdk/credential-provider-process": "3.750.0",
+ "@aws-sdk/credential-provider-sso": "3.750.0",
+ "@aws-sdk/credential-provider-web-identity": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/credential-provider-imds": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.750.0.tgz",
+ "integrity": "sha512-Q78SCH1n0m7tpu36sJwfrUSxI8l611OyysjQeMiIOliVfZICEoHcLHLcLkiR+tnIpZ3rk7d2EQ6R1jwlXnalMQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.750.0.tgz",
+ "integrity": "sha512-FGYrDjXN/FOQVi/t8fHSv8zCk+NEvtFnuc4cZUj5OIbM4vrfFc5VaPyn41Uza3iv6Qq9rZg0QOwWnqK8lNrqUw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-sso": "3.750.0",
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/token-providers": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.750.0.tgz",
+ "integrity": "sha512-Nz8zs3YJ+GOTSrq+LyzbbC1Ffpt7pK38gcOyNZv76pP5MswKTUKNYBJehqwa+i7FcFQHsCk3TdhR8MT1ZR23uA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/nested-clients": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/credential-providers": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.750.0.tgz",
+ "integrity": "sha512-HpJyLHAjcn/IcvsL4WhEIgbzEWfTnn29u8QFNa5Ii9pVtxdeP/DkSthP3SNxLK2jVNcqWL9xago02SiasNOKfw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-cognito-identity": "3.750.0",
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/credential-provider-cognito-identity": "3.750.0",
+ "@aws-sdk/credential-provider-env": "3.750.0",
+ "@aws-sdk/credential-provider-http": "3.750.0",
+ "@aws-sdk/credential-provider-ini": "3.750.0",
+ "@aws-sdk/credential-provider-node": "3.750.0",
+ "@aws-sdk/credential-provider-process": "3.750.0",
+ "@aws-sdk/credential-provider-sso": "3.750.0",
+ "@aws-sdk/credential-provider-web-identity": "3.750.0",
+ "@aws-sdk/nested-clients": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/core": "^3.1.4",
+ "@smithy/credential-provider-imds": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/middleware-host-header": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.734.0.tgz",
+ "integrity": "sha512-LW7RRgSOHHBzWZnigNsDIzu3AiwtjeI2X66v+Wn1P1u+eXssy1+up4ZY/h+t2sU4LU36UvEf+jrZti9c6vRnFw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/middleware-logger": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.734.0.tgz",
+ "integrity": "sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/middleware-recursion-detection": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.734.0.tgz",
+ "integrity": "sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/middleware-user-agent": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.750.0.tgz",
+ "integrity": "sha512-YYcslDsP5+2NZoN3UwuhZGkhAHPSli7HlJHBafBrvjGV/I9f8FuOO1d1ebxGdEP4HyRXUGyh+7Ur4q+Psk0ryw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@aws-sdk/util-endpoints": "3.743.0",
+ "@smithy/core": "^3.1.4",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/region-config-resolver": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.734.0.tgz",
+ "integrity": "sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-config-provider": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/token-providers": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.750.0.tgz",
+ "integrity": "sha512-X/KzqZw41iWolwNdc8e3RMcNSMR364viHv78u6AefXOO5eRM40c4/LuST1jDzq35/LpnqRhL7/MuixOetw+sFw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/nested-clients": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/types": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz",
+ "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.743.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.743.0.tgz",
+ "integrity": "sha512-sN1l559zrixeh5x+pttrnd0A3+r34r0tmPkJ/eaaMaAzXqsmKU/xYre9K3FNnsSS1J1k4PEfk/nHDTVUgFYjnw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-endpoints": "^3.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/util-user-agent-browser": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.734.0.tgz",
+ "integrity": "sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/types": "^4.1.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "packages/bruno-cli/node_modules/@aws-sdk/util-user-agent-node": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.750.0.tgz",
+ "integrity": "sha512-84HJj9G9zbrHX2opLk9eHfDceB+UIHVrmflMzWHpsmo9fDuro/flIBqaVDlE021Osj6qIM0SJJcnL6s23j7JEw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-user-agent": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "aws-crt": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws-crt": {
+ "optional": true
+ }
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/abort-controller": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.1.tgz",
+ "integrity": "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/config-resolver": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.0.1.tgz",
+ "integrity": "sha512-Igfg8lKu3dRVkTSEm98QpZUvKEOa71jDX4vKRcvJVyRc3UgN3j7vFMf0s7xLQhYmKa8kyJGQgUJDOV5V3neVlQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-config-provider": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/core": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.1.5.tgz",
+ "integrity": "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/middleware-serde": "^4.0.2",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-body-length-browser": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-stream": "^4.1.2",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/credential-provider-imds": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.1.tgz",
+ "integrity": "sha512-l/qdInaDq1Zpznpmev/+52QomsJNZ3JkTl5yrTl02V6NBgJOQ4LY0SFw/8zsMwj3tLe8vqiIuwF6nxaEwgf6mg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/url-parser": "^4.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/fetch-http-handler": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.1.tgz",
+ "integrity": "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/querystring-builder": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-base64": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/hash-node": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.1.tgz",
+ "integrity": "sha512-TJ6oZS+3r2Xu4emVse1YPB3Dq3d8RkZDKcPr71Nj/lJsdAP1c7oFzYqEn1IBc915TsgLl2xIJNuxCz+gLbLE0w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-buffer-from": "^4.0.0",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/invalid-dependency": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.1.tgz",
+ "integrity": "sha512-gdudFPf4QRQ5pzj7HEnu6FhKRi61BfH/Gk5Yf6O0KiSbr1LlVhgjThcvjdu658VE6Nve8vaIWB8/fodmS1rBPQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/is-array-buffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz",
+ "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/middleware-content-length": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.1.tgz",
+ "integrity": "sha512-OGXo7w5EkB5pPiac7KNzVtfCW2vKBTZNuCctn++TTSOMpe6RZO/n6WEC1AxJINn3+vWLKW49uad3lo/u0WJ9oQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/middleware-endpoint": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.0.6.tgz",
+ "integrity": "sha512-ftpmkTHIFqgaFugcjzLZv3kzPEFsBFSnq1JsIkr2mwFzCraZVhQk2gqN51OOeRxqhbPTkRFj39Qd2V91E/mQxg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.1.5",
+ "@smithy/middleware-serde": "^4.0.2",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/url-parser": "^4.0.1",
+ "@smithy/util-middleware": "^4.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/middleware-retry": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.0.7.tgz",
+ "integrity": "sha512-58j9XbUPLkqAcV1kHzVX/kAR16GT+j7DUZJqwzsxh1jtz7G82caZiGyyFgUvogVfNTg3TeAOIJepGc8TXF4AVQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/service-error-classification": "^4.0.1",
+ "@smithy/smithy-client": "^4.1.6",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-retry": "^4.0.1",
+ "tslib": "^2.6.2",
+ "uuid": "^9.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/middleware-serde": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.2.tgz",
+ "integrity": "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/middleware-stack": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.1.tgz",
+ "integrity": "sha512-dHwDmrtR/ln8UTHpaIavRSzeIk5+YZTBtLnKwDW3G2t6nAupCiQUvNzNoHBpik63fwUaJPtlnMzXbQrNFWssIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/node-config-provider": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.0.1.tgz",
+ "integrity": "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/node-http-handler": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.3.tgz",
+ "integrity": "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/abort-controller": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/querystring-builder": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/property-provider": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.1.tgz",
+ "integrity": "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/protocol-http": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.0.1.tgz",
+ "integrity": "sha512-TE4cpj49jJNB/oHyh/cRVEgNZaoPaxd4vteJNB0yGidOCVR0jCw/hjPVsT8Q8FRmj8Bd3bFZt8Dh7xGCT+xMBQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/querystring-builder": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.1.tgz",
+ "integrity": "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-uri-escape": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/querystring-parser": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.1.tgz",
+ "integrity": "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/service-error-classification": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.1.tgz",
+ "integrity": "sha512-3JNjBfOWpj/mYfjXJHB4Txc/7E4LVq32bwzE7m28GN79+M1f76XHflUaSUkhOriprPDzev9cX/M+dEB80DNDKA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/shared-ini-file-loader": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.1.tgz",
+ "integrity": "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/signature-v4": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.0.1.tgz",
+ "integrity": "sha512-nCe6fQ+ppm1bQuw5iKoeJ0MJfz2os7Ic3GBjOkLOPtavbD1ONoyE3ygjBfz2ythFWm4YnRm6OxW+8p/m9uCoIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^4.0.0",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-hex-encoding": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-uri-escape": "^4.0.0",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/smithy-client": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.1.6.tgz",
+ "integrity": "sha512-UYDolNg6h2O0L+cJjtgSyKKvEKCOa/8FHYJnBobyeoeWDmNpXjwOAtw16ezyeu1ETuuLEOZbrynK0ZY1Lx9Jbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.1.5",
+ "@smithy/middleware-endpoint": "^4.0.6",
+ "@smithy/middleware-stack": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-stream": "^4.1.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/types": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.1.0.tgz",
+ "integrity": "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/url-parser": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.1.tgz",
+ "integrity": "sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/querystring-parser": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-base64": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz",
+ "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.0.0",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-body-length-browser": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz",
+ "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-body-length-node": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz",
+ "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-buffer-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz",
+ "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-config-provider": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz",
+ "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-defaults-mode-browser": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.7.tgz",
+ "integrity": "sha512-CZgDDrYHLv0RUElOsmZtAnp1pIjwDVCSuZWOPhIOBvG36RDfX1Q9+6lS61xBf+qqvHoqRjHxgINeQz47cYFC2Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/smithy-client": "^4.1.6",
+ "@smithy/types": "^4.1.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-defaults-mode-node": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.7.tgz",
+ "integrity": "sha512-79fQW3hnfCdrfIi1soPbK3zmooRFnLpSx3Vxi6nUlqaaQeC5dm8plt4OTNDNqEEEDkvKghZSaoti684dQFVrGQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/config-resolver": "^4.0.1",
+ "@smithy/credential-provider-imds": "^4.0.1",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/smithy-client": "^4.1.6",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-endpoints": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.1.tgz",
+ "integrity": "sha512-zVdUENQpdtn9jbpD9SCFK4+aSiavRb9BxEtw9ZGUR1TYo6bBHbIoi7VkrFQ0/RwZlzx0wRBaRmPclj8iAoJCLA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-hex-encoding": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz",
+ "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-middleware": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.1.tgz",
+ "integrity": "sha512-HiLAvlcqhbzhuiOa0Lyct5IIlyIz0PQO5dnMlmQ/ubYM46dPInB+3yQGkfxsk6Q24Y0n3/JmcA1v5iEhmOF5mA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-retry": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.1.tgz",
+ "integrity": "sha512-WmRHqNVwn3kI3rKk1LsKcVgPBG6iLTBGC1iYOV3GQegwJ3E8yjzHytPt26VNzOWr1qu0xE03nK0Ug8S7T7oufw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/service-error-classification": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-stream": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.1.2.tgz",
+ "integrity": "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/fetch-http-handler": "^5.0.1",
+ "@smithy/node-http-handler": "^4.0.3",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-base64": "^4.0.0",
+ "@smithy/util-buffer-from": "^4.0.0",
+ "@smithy/util-hex-encoding": "^4.0.0",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-uri-escape": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz",
+ "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/@smithy/util-utf8": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz",
+ "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-cli/node_modules/axios": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
+ "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"packages/bruno-cli/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -24190,7 +26267,7 @@
"name": "bruno",
"version": "v1.38.1",
"dependencies": {
- "@aws-sdk/credential-providers": "3.658.1",
+ "@aws-sdk/credential-providers": "3.750.0",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
@@ -24199,7 +26276,7 @@
"@usebruno/vm2": "^3.9.13",
"about-window": "^1.15.2",
"aws4-axios": "^3.3.0",
- "axios": "1.7.5",
+ "axios": "^1.8.3",
"axios-ntlm": "^1.4.2",
"chai": "^4.3.7",
"chokidar": "^3.5.3",
@@ -24236,6 +26313,1044 @@
"dmg-license": "^1.0.11"
}
},
+ "packages/bruno-electron/node_modules/@aws-sdk/client-cognito-identity": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.750.0.tgz",
+ "integrity": "sha512-ia5+l7U1ludU/YqQAnEMj5DIp1kfMTu14lUOMG3uTIwTcj8OjkCvAe6BuM0OY6zd8enrJYWLqIqxuKPOWw4I7Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/credential-provider-node": "3.750.0",
+ "@aws-sdk/middleware-host-header": "3.734.0",
+ "@aws-sdk/middleware-logger": "3.734.0",
+ "@aws-sdk/middleware-recursion-detection": "3.734.0",
+ "@aws-sdk/middleware-user-agent": "3.750.0",
+ "@aws-sdk/region-config-resolver": "3.734.0",
+ "@aws-sdk/types": "3.734.0",
+ "@aws-sdk/util-endpoints": "3.743.0",
+ "@aws-sdk/util-user-agent-browser": "3.734.0",
+ "@aws-sdk/util-user-agent-node": "3.750.0",
+ "@smithy/config-resolver": "^4.0.1",
+ "@smithy/core": "^3.1.4",
+ "@smithy/fetch-http-handler": "^5.0.1",
+ "@smithy/hash-node": "^4.0.1",
+ "@smithy/invalid-dependency": "^4.0.1",
+ "@smithy/middleware-content-length": "^4.0.1",
+ "@smithy/middleware-endpoint": "^4.0.5",
+ "@smithy/middleware-retry": "^4.0.6",
+ "@smithy/middleware-serde": "^4.0.2",
+ "@smithy/middleware-stack": "^4.0.1",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/node-http-handler": "^4.0.2",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/smithy-client": "^4.1.5",
+ "@smithy/types": "^4.1.0",
+ "@smithy/url-parser": "^4.0.1",
+ "@smithy/util-base64": "^4.0.0",
+ "@smithy/util-body-length-browser": "^4.0.0",
+ "@smithy/util-body-length-node": "^4.0.0",
+ "@smithy/util-defaults-mode-browser": "^4.0.6",
+ "@smithy/util-defaults-mode-node": "^4.0.6",
+ "@smithy/util-endpoints": "^3.0.1",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-retry": "^4.0.1",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/client-sso": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.750.0.tgz",
+ "integrity": "sha512-y0Rx6pTQXw0E61CaptpZF65qNggjqOgymq/RYZU5vWba5DGQ+iqGt8Yq8s+jfBoBBNXshxq8l8Dl5Uq/JTY1wg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/middleware-host-header": "3.734.0",
+ "@aws-sdk/middleware-logger": "3.734.0",
+ "@aws-sdk/middleware-recursion-detection": "3.734.0",
+ "@aws-sdk/middleware-user-agent": "3.750.0",
+ "@aws-sdk/region-config-resolver": "3.734.0",
+ "@aws-sdk/types": "3.734.0",
+ "@aws-sdk/util-endpoints": "3.743.0",
+ "@aws-sdk/util-user-agent-browser": "3.734.0",
+ "@aws-sdk/util-user-agent-node": "3.750.0",
+ "@smithy/config-resolver": "^4.0.1",
+ "@smithy/core": "^3.1.4",
+ "@smithy/fetch-http-handler": "^5.0.1",
+ "@smithy/hash-node": "^4.0.1",
+ "@smithy/invalid-dependency": "^4.0.1",
+ "@smithy/middleware-content-length": "^4.0.1",
+ "@smithy/middleware-endpoint": "^4.0.5",
+ "@smithy/middleware-retry": "^4.0.6",
+ "@smithy/middleware-serde": "^4.0.2",
+ "@smithy/middleware-stack": "^4.0.1",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/node-http-handler": "^4.0.2",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/smithy-client": "^4.1.5",
+ "@smithy/types": "^4.1.0",
+ "@smithy/url-parser": "^4.0.1",
+ "@smithy/util-base64": "^4.0.0",
+ "@smithy/util-body-length-browser": "^4.0.0",
+ "@smithy/util-body-length-node": "^4.0.0",
+ "@smithy/util-defaults-mode-browser": "^4.0.6",
+ "@smithy/util-defaults-mode-node": "^4.0.6",
+ "@smithy/util-endpoints": "^3.0.1",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-retry": "^4.0.1",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/core": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.750.0.tgz",
+ "integrity": "sha512-bZ5K7N5L4+Pa2epbVpUQqd1XLG2uU8BGs/Sd+2nbgTf+lNQJyIxAg/Qsrjz9MzmY8zzQIeRQEkNmR6yVAfCmmQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/core": "^3.1.4",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/signature-v4": "^5.0.1",
+ "@smithy/smithy-client": "^4.1.5",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "fast-xml-parser": "4.4.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/credential-provider-cognito-identity": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.750.0.tgz",
+ "integrity": "sha512-TwBzrxgIWcQk846XFn0A9DHBHbfg4sHR3M2GL4E7NcffEkh7r642ILiLa58VvQjO2nB1tcOOFtRqbZvVOKexUw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-cognito-identity": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.750.0.tgz",
+ "integrity": "sha512-In6bsG0p/P31HcH4DBRKBbcDS/3SHvEPjfXV8ODPWZO/l3/p7IRoYBdQ07C9R+VMZU2D0+/Sc/DWK/TUNDk1+Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.750.0.tgz",
+ "integrity": "sha512-wFB9qqfa20AB0dElsQz5ZlZT5o+a+XzpEpmg0erylmGYqEOvh8NQWfDUVpRmQuGq9VbvW/8cIbxPoNqEbPtuWQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/fetch-http-handler": "^5.0.1",
+ "@smithy/node-http-handler": "^4.0.2",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/smithy-client": "^4.1.5",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-stream": "^4.1.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.750.0.tgz",
+ "integrity": "sha512-2YIZmyEr5RUd3uxXpxOLD9G67Bibm4I/65M6vKFP17jVMUT+R1nL7mKqmhEVO2p+BoeV+bwMyJ/jpTYG368PCg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/credential-provider-env": "3.750.0",
+ "@aws-sdk/credential-provider-http": "3.750.0",
+ "@aws-sdk/credential-provider-process": "3.750.0",
+ "@aws-sdk/credential-provider-sso": "3.750.0",
+ "@aws-sdk/credential-provider-web-identity": "3.750.0",
+ "@aws-sdk/nested-clients": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/credential-provider-imds": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.750.0.tgz",
+ "integrity": "sha512-THWHHAceLwsOiowPEmKyhWVDlEUxH07GHSw5AQFDvNQtGKOQl0HSIFO1mKObT2Q2Vqzji9Bq8H58SO5BFtNPRw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "3.750.0",
+ "@aws-sdk/credential-provider-http": "3.750.0",
+ "@aws-sdk/credential-provider-ini": "3.750.0",
+ "@aws-sdk/credential-provider-process": "3.750.0",
+ "@aws-sdk/credential-provider-sso": "3.750.0",
+ "@aws-sdk/credential-provider-web-identity": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/credential-provider-imds": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.750.0.tgz",
+ "integrity": "sha512-Q78SCH1n0m7tpu36sJwfrUSxI8l611OyysjQeMiIOliVfZICEoHcLHLcLkiR+tnIpZ3rk7d2EQ6R1jwlXnalMQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.750.0.tgz",
+ "integrity": "sha512-FGYrDjXN/FOQVi/t8fHSv8zCk+NEvtFnuc4cZUj5OIbM4vrfFc5VaPyn41Uza3iv6Qq9rZg0QOwWnqK8lNrqUw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-sso": "3.750.0",
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/token-providers": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.750.0.tgz",
+ "integrity": "sha512-Nz8zs3YJ+GOTSrq+LyzbbC1Ffpt7pK38gcOyNZv76pP5MswKTUKNYBJehqwa+i7FcFQHsCk3TdhR8MT1ZR23uA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/nested-clients": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/credential-providers": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.750.0.tgz",
+ "integrity": "sha512-HpJyLHAjcn/IcvsL4WhEIgbzEWfTnn29u8QFNa5Ii9pVtxdeP/DkSthP3SNxLK2jVNcqWL9xago02SiasNOKfw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-cognito-identity": "3.750.0",
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/credential-provider-cognito-identity": "3.750.0",
+ "@aws-sdk/credential-provider-env": "3.750.0",
+ "@aws-sdk/credential-provider-http": "3.750.0",
+ "@aws-sdk/credential-provider-ini": "3.750.0",
+ "@aws-sdk/credential-provider-node": "3.750.0",
+ "@aws-sdk/credential-provider-process": "3.750.0",
+ "@aws-sdk/credential-provider-sso": "3.750.0",
+ "@aws-sdk/credential-provider-web-identity": "3.750.0",
+ "@aws-sdk/nested-clients": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/core": "^3.1.4",
+ "@smithy/credential-provider-imds": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/middleware-host-header": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.734.0.tgz",
+ "integrity": "sha512-LW7RRgSOHHBzWZnigNsDIzu3AiwtjeI2X66v+Wn1P1u+eXssy1+up4ZY/h+t2sU4LU36UvEf+jrZti9c6vRnFw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/middleware-logger": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.734.0.tgz",
+ "integrity": "sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/middleware-recursion-detection": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.734.0.tgz",
+ "integrity": "sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/middleware-user-agent": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.750.0.tgz",
+ "integrity": "sha512-YYcslDsP5+2NZoN3UwuhZGkhAHPSli7HlJHBafBrvjGV/I9f8FuOO1d1ebxGdEP4HyRXUGyh+7Ur4q+Psk0ryw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@aws-sdk/util-endpoints": "3.743.0",
+ "@smithy/core": "^3.1.4",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/region-config-resolver": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.734.0.tgz",
+ "integrity": "sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-config-provider": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/token-providers": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.750.0.tgz",
+ "integrity": "sha512-X/KzqZw41iWolwNdc8e3RMcNSMR364viHv78u6AefXOO5eRM40c4/LuST1jDzq35/LpnqRhL7/MuixOetw+sFw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/nested-clients": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/types": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz",
+ "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.743.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.743.0.tgz",
+ "integrity": "sha512-sN1l559zrixeh5x+pttrnd0A3+r34r0tmPkJ/eaaMaAzXqsmKU/xYre9K3FNnsSS1J1k4PEfk/nHDTVUgFYjnw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-endpoints": "^3.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/util-user-agent-browser": {
+ "version": "3.734.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.734.0.tgz",
+ "integrity": "sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/types": "^4.1.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "packages/bruno-electron/node_modules/@aws-sdk/util-user-agent-node": {
+ "version": "3.750.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.750.0.tgz",
+ "integrity": "sha512-84HJj9G9zbrHX2opLk9eHfDceB+UIHVrmflMzWHpsmo9fDuro/flIBqaVDlE021Osj6qIM0SJJcnL6s23j7JEw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-user-agent": "3.750.0",
+ "@aws-sdk/types": "3.734.0",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "aws-crt": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws-crt": {
+ "optional": true
+ }
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/abort-controller": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.1.tgz",
+ "integrity": "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/config-resolver": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.0.1.tgz",
+ "integrity": "sha512-Igfg8lKu3dRVkTSEm98QpZUvKEOa71jDX4vKRcvJVyRc3UgN3j7vFMf0s7xLQhYmKa8kyJGQgUJDOV5V3neVlQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-config-provider": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/core": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.1.5.tgz",
+ "integrity": "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/middleware-serde": "^4.0.2",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-body-length-browser": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-stream": "^4.1.2",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/credential-provider-imds": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.1.tgz",
+ "integrity": "sha512-l/qdInaDq1Zpznpmev/+52QomsJNZ3JkTl5yrTl02V6NBgJOQ4LY0SFw/8zsMwj3tLe8vqiIuwF6nxaEwgf6mg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/url-parser": "^4.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/fetch-http-handler": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.1.tgz",
+ "integrity": "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/querystring-builder": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-base64": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/hash-node": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.1.tgz",
+ "integrity": "sha512-TJ6oZS+3r2Xu4emVse1YPB3Dq3d8RkZDKcPr71Nj/lJsdAP1c7oFzYqEn1IBc915TsgLl2xIJNuxCz+gLbLE0w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-buffer-from": "^4.0.0",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/invalid-dependency": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.1.tgz",
+ "integrity": "sha512-gdudFPf4QRQ5pzj7HEnu6FhKRi61BfH/Gk5Yf6O0KiSbr1LlVhgjThcvjdu658VE6Nve8vaIWB8/fodmS1rBPQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/is-array-buffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz",
+ "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/middleware-content-length": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.1.tgz",
+ "integrity": "sha512-OGXo7w5EkB5pPiac7KNzVtfCW2vKBTZNuCctn++TTSOMpe6RZO/n6WEC1AxJINn3+vWLKW49uad3lo/u0WJ9oQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/middleware-endpoint": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.0.6.tgz",
+ "integrity": "sha512-ftpmkTHIFqgaFugcjzLZv3kzPEFsBFSnq1JsIkr2mwFzCraZVhQk2gqN51OOeRxqhbPTkRFj39Qd2V91E/mQxg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.1.5",
+ "@smithy/middleware-serde": "^4.0.2",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/url-parser": "^4.0.1",
+ "@smithy/util-middleware": "^4.0.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/middleware-retry": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.0.7.tgz",
+ "integrity": "sha512-58j9XbUPLkqAcV1kHzVX/kAR16GT+j7DUZJqwzsxh1jtz7G82caZiGyyFgUvogVfNTg3TeAOIJepGc8TXF4AVQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/service-error-classification": "^4.0.1",
+ "@smithy/smithy-client": "^4.1.6",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-retry": "^4.0.1",
+ "tslib": "^2.6.2",
+ "uuid": "^9.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/middleware-serde": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.2.tgz",
+ "integrity": "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/middleware-stack": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.1.tgz",
+ "integrity": "sha512-dHwDmrtR/ln8UTHpaIavRSzeIk5+YZTBtLnKwDW3G2t6nAupCiQUvNzNoHBpik63fwUaJPtlnMzXbQrNFWssIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/node-config-provider": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.0.1.tgz",
+ "integrity": "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/shared-ini-file-loader": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/node-http-handler": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.3.tgz",
+ "integrity": "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/abort-controller": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/querystring-builder": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/property-provider": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.1.tgz",
+ "integrity": "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/protocol-http": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.0.1.tgz",
+ "integrity": "sha512-TE4cpj49jJNB/oHyh/cRVEgNZaoPaxd4vteJNB0yGidOCVR0jCw/hjPVsT8Q8FRmj8Bd3bFZt8Dh7xGCT+xMBQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/querystring-builder": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.1.tgz",
+ "integrity": "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-uri-escape": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/querystring-parser": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.1.tgz",
+ "integrity": "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/service-error-classification": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.1.tgz",
+ "integrity": "sha512-3JNjBfOWpj/mYfjXJHB4Txc/7E4LVq32bwzE7m28GN79+M1f76XHflUaSUkhOriprPDzev9cX/M+dEB80DNDKA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/shared-ini-file-loader": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.1.tgz",
+ "integrity": "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/signature-v4": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.0.1.tgz",
+ "integrity": "sha512-nCe6fQ+ppm1bQuw5iKoeJ0MJfz2os7Ic3GBjOkLOPtavbD1ONoyE3ygjBfz2ythFWm4YnRm6OxW+8p/m9uCoIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^4.0.0",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-hex-encoding": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.1",
+ "@smithy/util-uri-escape": "^4.0.0",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/smithy-client": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.1.6.tgz",
+ "integrity": "sha512-UYDolNg6h2O0L+cJjtgSyKKvEKCOa/8FHYJnBobyeoeWDmNpXjwOAtw16ezyeu1ETuuLEOZbrynK0ZY1Lx9Jbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.1.5",
+ "@smithy/middleware-endpoint": "^4.0.6",
+ "@smithy/middleware-stack": "^4.0.1",
+ "@smithy/protocol-http": "^5.0.1",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-stream": "^4.1.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/types": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.1.0.tgz",
+ "integrity": "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/url-parser": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.1.tgz",
+ "integrity": "sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/querystring-parser": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-base64": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz",
+ "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.0.0",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-body-length-browser": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz",
+ "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-body-length-node": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz",
+ "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-buffer-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz",
+ "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-config-provider": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz",
+ "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-defaults-mode-browser": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.7.tgz",
+ "integrity": "sha512-CZgDDrYHLv0RUElOsmZtAnp1pIjwDVCSuZWOPhIOBvG36RDfX1Q9+6lS61xBf+qqvHoqRjHxgINeQz47cYFC2Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/smithy-client": "^4.1.6",
+ "@smithy/types": "^4.1.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-defaults-mode-node": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.7.tgz",
+ "integrity": "sha512-79fQW3hnfCdrfIi1soPbK3zmooRFnLpSx3Vxi6nUlqaaQeC5dm8plt4OTNDNqEEEDkvKghZSaoti684dQFVrGQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/config-resolver": "^4.0.1",
+ "@smithy/credential-provider-imds": "^4.0.1",
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/property-provider": "^4.0.1",
+ "@smithy/smithy-client": "^4.1.6",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-endpoints": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.1.tgz",
+ "integrity": "sha512-zVdUENQpdtn9jbpD9SCFK4+aSiavRb9BxEtw9ZGUR1TYo6bBHbIoi7VkrFQ0/RwZlzx0wRBaRmPclj8iAoJCLA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-hex-encoding": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz",
+ "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-middleware": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.1.tgz",
+ "integrity": "sha512-HiLAvlcqhbzhuiOa0Lyct5IIlyIz0PQO5dnMlmQ/ubYM46dPInB+3yQGkfxsk6Q24Y0n3/JmcA1v5iEhmOF5mA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-retry": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.1.tgz",
+ "integrity": "sha512-WmRHqNVwn3kI3rKk1LsKcVgPBG6iLTBGC1iYOV3GQegwJ3E8yjzHytPt26VNzOWr1qu0xE03nK0Ug8S7T7oufw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/service-error-classification": "^4.0.1",
+ "@smithy/types": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-stream": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.1.2.tgz",
+ "integrity": "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/fetch-http-handler": "^5.0.1",
+ "@smithy/node-http-handler": "^4.0.3",
+ "@smithy/types": "^4.1.0",
+ "@smithy/util-base64": "^4.0.0",
+ "@smithy/util-buffer-from": "^4.0.0",
+ "@smithy/util-hex-encoding": "^4.0.0",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-uri-escape": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz",
+ "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/@smithy/util-utf8": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz",
+ "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "packages/bruno-electron/node_modules/axios": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
+ "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"packages/bruno-electron/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -24330,10 +27445,11 @@
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"atob": "^2.1.2",
- "axios": "1.7.5",
+ "axios": "^1.8.3",
"btoa": "^1.2.1",
"chai": "^4.3.7",
"chai-string": "^1.5.0",
+ "cheerio": "^1.0.0",
"crypto-js": "^4.1.1",
"json-query": "^2.2.2",
"lodash": "^4.17.21",
@@ -24343,7 +27459,8 @@
"node-vault": "^0.10.2",
"path": "^0.12.7",
"quickjs-emscripten": "^0.29.2",
- "uuid": "^9.0.0"
+ "uuid": "^9.0.0",
+ "xml2js": "^0.6.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
@@ -24357,6 +27474,17 @@
"@usebruno/vm2": "^3.9.13"
}
},
+ "packages/bruno-js/node_modules/axios": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
+ "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"packages/bruno-js/node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
@@ -24434,13 +27562,13 @@
"version": "0.0.1",
"license": "MIT",
"dependencies": {
- "axios": "1.7.5",
+ "axios": "^1.8.3",
"body-parser": "1.20.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
- "express": "4.21.1",
+ "express": "^4.21.2",
"express-basic-auth": "^1.2.1",
- "fast-xml-parser": "^4.5.0",
+ "fast-xml-parser": "^5.0.8",
"http-proxy": "^1.18.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
@@ -24448,28 +27576,123 @@
"multer": "^1.4.5-lts.1"
}
},
+ "packages/bruno-tests/node_modules/axios": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
+ "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "packages/bruno-tests/node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "packages/bruno-tests/node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"packages/bruno-tests/node_modules/fast-xml-parser": {
- "version": "4.5.1",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz",
- "integrity": "sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w==",
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.0.9.tgz",
+ "integrity": "sha512-2mBwCiuW3ycKQQ6SOesSB8WeF+fIGb6I/GG5vU5/XEptwFFhp9PE8b9O7fbs2dpq9fXn4ULR3UsfydNUCntf5A==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
- },
- {
- "type": "paypal",
- "url": "https://paypal.me/naturalintelligence"
}
],
"license": "MIT",
"dependencies": {
- "strnum": "^1.0.5"
+ "strnum": "^2.0.5"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
+ "packages/bruno-tests/node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "packages/bruno-tests/node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "packages/bruno-tests/node_modules/strnum": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz",
+ "integrity": "sha512-YAT3K/sgpCUxhxNMrrdhtod3jckkpYwH6JAuwmUdXZsmzH1wUyzTMrrK2wYCEEqlKwrWDd35NeuUkbBy/1iK+Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"packages/bruno-toml": {
"name": "@usebruno/toml",
"version": "0.1.0",
diff --git a/package.json b/package.json
index 2c46fdd2c..ea8e56f36 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"pretty-quick": "^3.1.3",
"randomstring": "^1.2.2",
"rimraf": "^6.0.1",
- "ts-jest": "^29.0.5"
+ "ts-jest": "^29.2.6"
},
"scripts": {
"setup": "node ./scripts/setup.js",
@@ -36,6 +36,7 @@
"build:web": "npm run build --workspace=packages/bruno-app",
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
+ "dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json
index d4fb47bcf..2c50a3625 100644
--- a/packages/bruno-app/package.json
+++ b/packages/bruno-app/package.json
@@ -1,6 +1,6 @@
{
"name": "@usebruno/app",
- "version": "0.3.0",
+ "version": "1.39.0",
"private": true,
"scripts": {
"dev": "rsbuild dev",
@@ -20,11 +20,11 @@
"@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.7.0",
- "axios": "1.7.5",
"classnames": "^2.3.1",
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",
"cookie": "0.7.1",
+ "dompurify": "^3.2.4",
"escape-html": "^1.0.3",
"file": "^0.2.2",
"file-dialog": "^0.0.8",
@@ -34,19 +34,22 @@
"graphiql": "3.7.1",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
- "httpsnippet": "^3.0.6",
+ "httpsnippet": "^3.0.9",
"i18next": "24.1.2",
+ "iconv-lite": "^0.6.3",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",
"jshint": "^2.13.6",
"json5": "^2.2.3",
"jsonc-parser": "^3.2.1",
- "jsonpath-plus": "10.2.0",
+ "jsonpath-plus": "^10.3.0",
"know-your-http-well": "^0.5.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
+ "moment": "^2.30.1",
+ "moment-timezone": "^0.5.47",
"mousetrap": "^1.6.5",
"nanoid": "3.3.8",
"path": "^0.12.7",
@@ -69,6 +72,7 @@
"react-redux": "^7.2.9",
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",
+ "semver": "^7.7.1",
"strip-json-comments": "^5.0.1",
"styled-components": "^5.3.3",
"system": "^2.0.1",
diff --git a/packages/bruno-app/src/components/Accordion/index.js b/packages/bruno-app/src/components/Accordion/index.js
new file mode 100644
index 000000000..ed73921d7
--- /dev/null
+++ b/packages/bruno-app/src/components/Accordion/index.js
@@ -0,0 +1,62 @@
+import React, { createContext, useContext, useState } from 'react';
+import { IconChevronDown } from '@tabler/icons';
+import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrapper';
+
+const AccordionContext = createContext();
+
+const Accordion = ({ children, defaultIndex }) => {
+ const [openIndex, setOpenIndex] = useState(defaultIndex);
+
+ const toggleItem = (index) => {
+ setOpenIndex(openIndex === index ? null : index);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+const Item = ({ index, children, ...props }) => {
+ return (
+
+ {React.Children.map(children, (child) => React.cloneElement(child, { index }))}
+
+ );
+};
+
+export const Header = ({ index, children, ...props }) => {
+ const { openIndex, toggleItem } = useContext(AccordionContext);
+ const isOpen = openIndex === index;
+
+ return (
+ toggleItem(index)} {...props} className={isOpen ? 'open' : ''}>
+ {children}
+
+
+
+ );
+};
+
+const Content = ({ index, children, ...props }) => {
+ const { openIndex } = useContext(AccordionContext);
+ const isOpen = openIndex === index;
+
+ return (
+
+ {children}
+
+ );
+};
+
+Accordion.Item = Item;
+Accordion.Header = Header;
+Accordion.Content = Content;
+export default Accordion;
diff --git a/packages/bruno-app/src/components/Accordion/styledWrapper.js b/packages/bruno-app/src/components/Accordion/styledWrapper.js
new file mode 100644
index 000000000..351100204
--- /dev/null
+++ b/packages/bruno-app/src/components/Accordion/styledWrapper.js
@@ -0,0 +1,28 @@
+import styled from 'styled-components';
+
+const AccordionItem = styled.div`
+ border: 1px solid ${(props) => props.theme.input.border};
+ border-radius: 4px;
+ overflow: hidden;
+ margin-bottom: 1rem;
+`;
+
+const AccordionHeader = styled.button`
+ width: 100%;
+ display: flex;
+ padding: 0.75rem 1rem;
+ background: transparent;
+ cursor: pointer;
+ font-weight: 500;
+
+ &.open, &:hover {
+ background-color: ${(props) => props.theme.plainGrid.hoverBg};
+ }
+`;
+
+const AccordionContent = styled.div`
+ padding: ${(props) => (props.isOpen ? '1rem' : '0')};
+ max-height: ${(props) => (props.isOpen ? 'auto' : '0')};
+`;
+
+export { AccordionItem, AccordionHeader, AccordionContent };
diff --git a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js
index edcee4cd9..9573022a4 100644
--- a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js
+++ b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js
@@ -8,6 +8,8 @@ const StyledWrapper = styled.div`
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
line-break: anywhere;
flex: 1 1 0;
+ display: flex;
+ flex-direction: column-reverse;
}
/* Removes the glow outline around the folded json */
@@ -26,6 +28,10 @@ const StyledWrapper = styled.div`
.CodeMirror-dialog {
overflow: visible;
+ position: relative;
+ top: unset;
+ left: unset;
+
input {
background: transparent;
border: 1px solid #d3d6db;
diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js
index 398007a4a..304179189 100644
--- a/packages/bruno-app/src/components/CodeEditor/index.js
+++ b/packages/bruno-app/src/components/CodeEditor/index.js
@@ -31,6 +31,7 @@ if (!SERVER_RENDERED) {
'res.body',
'res.responseTime',
'res.getStatus()',
+ 'res.getStatusText()',
'res.getHeader(name)',
'res.getHeaders()',
'res.getBody()',
@@ -74,6 +75,9 @@ if (!SERVER_RENDERED) {
'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()',
'bru.getRequestVar(key)',
+ 'bru.runRequest(requestPathName)',
+ 'bru.getAssertionResults()',
+ 'bru.getTestResults()',
'bru.sleep(ms)',
'bru.getGlobalEnvVar(key)',
'bru.setGlobalEnvVar(key, value)',
@@ -171,11 +175,21 @@ export default class CodeEditor extends React.Component {
}
},
'Cmd-F': (cm) => {
+ if (this._isSearchOpen()) {
+ // replace the older search component with the new one
+ const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
+ search && search.remove();
+ }
cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
},
'Ctrl-F': (cm) => {
+ if (this._isSearchOpen()) {
+ // replace the older search component with the new one
+ const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
+ search && search.remove();
+ }
cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
@@ -194,8 +208,20 @@ export default class CodeEditor extends React.Component {
'Cmd-Y': 'foldAll',
'Ctrl-I': 'unfoldAll',
'Cmd-I': 'unfoldAll',
- 'Ctrl-/': 'toggleComment',
- 'Cmd-/': 'toggleComment'
+ 'Ctrl-/': () => {
+ if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
+ this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
+ } else {
+ this.editor.toggleComment();
+ }
+ },
+ 'Cmd-/': () => {
+ if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
+ this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
+ } else {
+ this.editor.toggleComment();
+ }
+ }
},
foldOptions: {
widget: (from, to) => {
@@ -350,6 +376,10 @@ export default class CodeEditor extends React.Component {
}
};
+ _isSearchOpen = () => {
+ return document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
+ };
+
/**
* Bind handler to search input to count number of search results
*/
diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/StyledWrapper.js
index e49220854..b7e4b56c7 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Auth/StyledWrapper.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Auth/StyledWrapper.js
@@ -1,5 +1,7 @@
import styled from 'styled-components';
-const Wrapper = styled.div``;
+const Wrapper = styled.div`
+ max-width: 800px;
+`;
export default Wrapper;
diff --git a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js
index ccfac9046..0bc91b5c3 100644
--- a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js
@@ -8,9 +8,7 @@ import { useState } from 'react';
import StyledWrapper from './StyledWrapper';
import { useRef } from 'react';
-import path from 'path';
-import slash from 'utils/common/slash';
-import { isWindowsOS } from 'utils/common/platform';
+import path from 'utils/common/path';
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
const certFilePathInputRef = useRef();
@@ -70,12 +68,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
const getFile = (e) => {
const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
if (filePath) {
- let relativePath;
- if (isWindowsOS()) {
- relativePath = slash(path.win32.relative(root, filePath));
- } else {
- relativePath = path.posix.relative(root, filePath);
- }
+ let relativePath = path.relative(root, filePath);
formik.setFieldValue(e.name, relativePath);
}
};
@@ -109,23 +102,23 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
{!clientCertConfig.length
? 'No client certificates added'
- : clientCertConfig.map((clientCert) => (
-
-
-
-
- {clientCert.domain}
-
-
-
- {clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
-
-
onRemove(clientCert)} className="remove-certificate ml-2">
-
-
+ : clientCertConfig.map((clientCert, index) => (
+
+
+
+
+ {clientCert.domain}
-
- ))}
+
+
+ {clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
+
+
onRemove(clientCert)} className="remove-certificate ml-2">
+
+
+
+
+ ))}
Add Client Certificate
@@ -198,9 +191,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
- {path.basename(slash(formik.values.certFilePath))}
+ {path.basename(formik.values.certFilePath)}
{
- {path.basename(slash(formik.values.keyFilePath))}
+ {path.basename(formik.values.keyFilePath)}
{
- {path.basename(slash(formik.values.pfxFilePath))}
+ {path.basename(formik.values.pfxFilePath)}
props.theme.colors.text.yellow};
}
`;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Docs/index.js b/packages/bruno-app/src/components/CollectionSettings/Docs/index.js
index 23dbe9e70..2d869de65 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Docs/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Docs/index.js
@@ -8,6 +8,7 @@ import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/acti
import Markdown from 'components/MarkDown';
import CodeEditor from 'components/CodeEditor';
import StyledWrapper from './StyledWrapper';
+import { IconEdit, IconX, IconFileText } from '@tabler/icons';
const Docs = ({ collection }) => {
const dispatch = useDispatch();
@@ -29,35 +30,95 @@ const Docs = ({ collection }) => {
);
};
- const onSave = () => dispatch(saveCollectionRoot(collection.uid));
+ const handleDiscardChanges = () => {
+ dispatch(
+ updateCollectionDocs({
+ collectionUid: collection.uid,
+ docs: docs
+ })
+ );
+ toggleViewMode();
+ }
+
+ const onSave = () => {
+ dispatch(saveCollectionRoot(collection.uid));
+ toggleViewMode();
+ }
return (
-
- {isEditing ? 'Preview' : 'Edit'}
-
-
- {isEditing ? (
-
-
-
- Save
-
+
+
+
+ Documentation
+
+ {isEditing ? (
+ <>
+
+
+
+
+ Save
+
+ >
+ ) : (
+
+
+
+ )}
+
+
+ {isEditing ? (
+
) : (
-
+
+
+ {
+ docs?.length > 0 ?
+
+ :
+
+ }
+
+
)}
);
};
export default Docs;
+
+
+const documentationPlaceholder = `
+Welcome to your collection documentation! This space is designed to help you document your API collection effectively.
+
+## Overview
+Use this section to provide a high-level overview of your collection. You can describe:
+- The purpose of these API endpoints
+- Key features and functionalities
+- Target audience or users
+
+## Best Practices
+- Keep documentation up to date
+- Include request/response examples
+- Document error scenarios
+- Add relevant links and references
+
+## Markdown Support
+This documentation supports Markdown formatting! You can use:
+- **Bold** and *italic* text
+- \`code blocks\` and syntax highlighting
+- Tables and lists
+- [Links](https://example.com)
+- And more!
+`;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Headers/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Headers/StyledWrapper.js
index 9f723cb81..c4d03c5ed 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Headers/StyledWrapper.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Headers/StyledWrapper.js
@@ -1,6 +1,8 @@
import styled from 'styled-components';
const Wrapper = styled.div`
+ max-width: 800px;
+
table {
width: 100%;
border-collapse: collapse;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Info/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Info/StyledWrapper.js
deleted file mode 100644
index 7fd98347c..000000000
--- a/packages/bruno-app/src/components/CollectionSettings/Info/StyledWrapper.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import styled from 'styled-components';
-
-const StyledWrapper = styled.div`
- table {
- td {
- &:first-child {
- width: 120px;
- }
- }
- }
-`;
-
-export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Info/index.js b/packages/bruno-app/src/components/CollectionSettings/Info/index.js
deleted file mode 100644
index 3b0a1297b..000000000
--- a/packages/bruno-app/src/components/CollectionSettings/Info/index.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-import StyledWrapper from './StyledWrapper';
-import { getTotalRequestCountInCollection } from 'utils/collections/';
-
-const Info = ({ collection }) => {
- const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
-
- return (
-
- General information about the collection.
-
-
-
- Name :
- {collection.name}
-
-
- Location :
- {collection.pathname}
-
-
- Ignored files :
- {collection.brunoConfig?.ignore?.map((x) => `'${x}'`).join(', ')}
-
-
- Environments :
- {collection.environments?.length || 0}
-
-
- Requests :
- {totalRequestsInCollection}
-
-
-
-
- );
-};
-
-export default Info;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js
new file mode 100644
index 000000000..751919cde
--- /dev/null
+++ b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js
@@ -0,0 +1,82 @@
+import React from "react";
+import { getTotalRequestCountInCollection } from 'utils/collections/';
+import { IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
+import { areItemsLoading, getItemsLoadStats } from "utils/collections/index";
+import { useState } from "react";
+import ShareCollection from "components/ShareCollection/index";
+
+const Info = ({ collection }) => {
+ const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
+
+ const isCollectionLoading = areItemsLoading(collection);
+ const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
+ const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
+
+ const handleToggleShowShareCollectionModal = (value) => (e) => {
+ toggleShowShareCollectionModal(value);
+ }
+
+ return (
+
+
+
+ {/* Location Row */}
+
+
+
+
+
+
Location
+
+ {collection.pathname}
+
+
+
+
+ {/* Environments Row */}
+
+
+
+
+
+
Environments
+
+ {collection.environments?.length || 0} environment{collection.environments?.length !== 1 ? 's' : ''} configured
+
+
+
+
+ {/* Requests Row */}
+
+
+
+
+
+
Requests
+
+ {
+ isCollectionLoading? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
+ }
+
+
+
+
+
+
+
+
+
+
Share
+
+ Share Collection
+
+
+
+ {showShareCollectionModal &&
}
+
+
+
+ );
+};
+
+export default Info;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/StyledWrapper.js
new file mode 100644
index 000000000..e9a9cd06f
--- /dev/null
+++ b/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/StyledWrapper.js
@@ -0,0 +1,25 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ &.card {
+ background-color: ${(props) => props.theme.requestTabPanel.card.bg};
+
+ .title {
+ border-top: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
+ border-left: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
+ border-right: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
+
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px;
+ }
+
+ .table {
+ thead {
+ background-color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.bg};
+ color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.color};
+ }
+ }
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js
new file mode 100644
index 000000000..4c7406580
--- /dev/null
+++ b/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js
@@ -0,0 +1,80 @@
+import React from 'react';
+import { flattenItems } from "utils/collections";
+import { IconAlertTriangle } from '@tabler/icons';
+import StyledWrapper from "./StyledWrapper";
+import { useDispatch, useSelector } from 'react-redux';
+import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index';
+import { getDefaultRequestPaneTab } from 'utils/collections/index';
+import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
+import { hideHomePage } from 'providers/ReduxStore/slices/app';
+
+const RequestsNotLoaded = ({ collection }) => {
+ const dispatch = useDispatch();
+ const tabs = useSelector((state) => state.tabs.tabs);
+ const flattenedItems = flattenItems(collection.items);
+ const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading);
+
+ if (!itemsFailedLoading?.length) {
+ return null;
+ }
+
+ const handleRequestClick = (item) => e => {
+ e.preventDefault();
+ if (isItemARequest(item)) {
+ dispatch(hideHomePage());
+ if (itemIsOpenedInTabs(item, tabs)) {
+ dispatch(
+ focusTab({
+ uid: item.uid
+ })
+ );
+ return;
+ }
+ dispatch(
+ addTab({
+ uid: item.uid,
+ collectionUid: collection.uid,
+ requestPaneTab: getDefaultRequestPaneTab(item)
+ })
+ );
+ return;
+ }
+ }
+
+ return (
+
+
+
+ Following requests were not loaded
+
+
+
+
+
+ Pathname
+
+
+ Size
+
+
+
+
+ {flattenedItems?.map((item, index) => (
+ item?.partial && !item?.loading ? (
+
+
+ {item?.pathname?.split(`${collection?.pathname}/`)?.[1]}
+
+
+ {item?.size?.toFixed?.(2)} MB
+
+
+ ) : null
+ ))}
+
+
+
+ );
+};
+
+export default RequestsNotLoaded;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Overview/StyledWrapper.js
new file mode 100644
index 000000000..4d77f2600
--- /dev/null
+++ b/packages/bruno-app/src/components/CollectionSettings/Overview/StyledWrapper.js
@@ -0,0 +1,25 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ .partial {
+ color: ${(props) => props.theme.colors.text.yellow};
+ opacity: 0.8;
+ }
+
+ .loading {
+ color: ${(props) => props.theme.colors.text.muted};
+ opacity: 0.8;
+ }
+
+ .completed {
+ color: ${(props) => props.theme.colors.text.green};
+ opacity: 0.8;
+ }
+
+ .failed {
+ color: ${(props) => props.theme.colors.text.danger};
+ opacity: 0.8;
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/index.js
new file mode 100644
index 000000000..87b461e9c
--- /dev/null
+++ b/packages/bruno-app/src/components/CollectionSettings/Overview/index.js
@@ -0,0 +1,27 @@
+import StyledWrapper from "./StyledWrapper";
+import Docs from "../Docs";
+import Info from "./Info";
+import { IconBox } from '@tabler/icons';
+import RequestsNotLoaded from "./RequestsNotLoaded";
+
+const Overview = ({ collection }) => {
+ return (
+
+
+
+
+
+ {collection?.name}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default Overview;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/CollectionSettings/Presets/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Presets/StyledWrapper.js
index 602851baa..db26e863b 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Presets/StyledWrapper.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Presets/StyledWrapper.js
@@ -1,6 +1,8 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
+ max-width: 800px;
+
.settings-label {
width: 110px;
}
diff --git a/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js
index 105a92642..bb48cbdc0 100644
--- a/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js
@@ -104,18 +104,15 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
Config
-
- global - use global proxy config
- enabled - use collection proxy config
- disable - disable proxy
+ global - use global proxy config
+ enabled - use collection proxy config
+ disable - disable proxy
- `}
- infotipId="request-var"
- />
+
diff --git a/packages/bruno-app/src/components/CollectionSettings/Script/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Script/StyledWrapper.js
index 66ba1ed3d..03aed74aa 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Script/StyledWrapper.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Script/StyledWrapper.js
@@ -1,6 +1,8 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
+ max-width: 800px;
+
div.CodeMirror {
height: inherit;
}
diff --git a/packages/bruno-app/src/components/CollectionSettings/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/StyledWrapper.js
index b88a31e0d..90ab7fee5 100644
--- a/packages/bruno-app/src/components/CollectionSettings/StyledWrapper.js
+++ b/packages/bruno-app/src/components/CollectionSettings/StyledWrapper.js
@@ -1,8 +1,6 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
- max-width: 800px;
-
div.tabs {
div.tab {
padding: 6px 0px;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Tests/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Tests/StyledWrapper.js
index ec278887d..b9014ebd5 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Tests/StyledWrapper.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Tests/StyledWrapper.js
@@ -1,5 +1,7 @@
import styled from 'styled-components';
-const StyledWrapper = styled.div``;
+const StyledWrapper = styled.div`
+ max-width: 800px;
+`;
export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js
index 44b01b464..26459a3c6 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js
@@ -1,6 +1,8 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
+ max-width: 800px;
+
div.title {
color: var(--color-tab-inactive);
}
diff --git a/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js
index ebc6a2fe7..0341c6ecd 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js
@@ -89,7 +89,7 @@ const VarsTable = ({ collection, vars, varType }) => {
Expr
-
+
)}
diff --git a/packages/bruno-app/src/components/CollectionSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/index.js
index b849d6b18..7d5d60574 100644
--- a/packages/bruno-app/src/components/CollectionSettings/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/index.js
@@ -12,12 +12,11 @@ import Headers from './Headers';
import Auth from './Auth';
import Script from './Script';
import Test from './Tests';
-import Docs from './Docs';
import Presets from './Presets';
-import Info from './Info';
import StyledWrapper from './StyledWrapper';
import Vars from './Vars/index';
import DotIcon from 'components/Icons/Dot';
+import Overview from './Overview/index';
const ContentIndicator = () => {
return (
@@ -97,6 +96,9 @@ const CollectionSettings = ({ collection }) => {
const getTabPanel = (tab) => {
switch (tab) {
+ case 'overview': {
+ return ;
+ }
case 'headers': {
return ;
}
@@ -128,12 +130,6 @@ const CollectionSettings = ({ collection }) => {
/>
);
}
- case 'docs': {
- return ;
- }
- case 'info': {
- return ;
- }
}
};
@@ -146,6 +142,9 @@ const CollectionSettings = ({ collection }) => {
return (
+
setTab('overview')}>
+ Overview
+
setTab('headers')}>
Headers
{activeHeadersCount > 0 && {activeHeadersCount} }
@@ -177,13 +176,6 @@ const CollectionSettings = ({ collection }) => {
Client Certificates
{clientCertConfig.length > 0 && }
-
setTab('docs')}>
- Docs
- {hasDocs && }
-
-
setTab('info')}>
- Info
-
diff --git a/packages/bruno-app/src/components/Cookies/ModifyCookieModal/StyledWrapper.js b/packages/bruno-app/src/components/Cookies/ModifyCookieModal/StyledWrapper.js
new file mode 100644
index 000000000..ec278887d
--- /dev/null
+++ b/packages/bruno-app/src/components/Cookies/ModifyCookieModal/StyledWrapper.js
@@ -0,0 +1,5 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div``;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/Cookies/ModifyCookieModal/index.js b/packages/bruno-app/src/components/Cookies/ModifyCookieModal/index.js
new file mode 100644
index 000000000..5f7f66867
--- /dev/null
+++ b/packages/bruno-app/src/components/Cookies/ModifyCookieModal/index.js
@@ -0,0 +1,371 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { useFormik } from 'formik';
+import * as Yup from 'yup';
+import Modal from 'components/Modal/index';
+import { modifyCookie, addCookie, getParsedCookie, createCookieString } from 'providers/ReduxStore/slices/app';
+import { useDispatch } from 'react-redux';
+import toast from 'react-hot-toast';
+import ToggleSwitch from 'components/ToggleSwitch/index';
+import { IconInfoCircle } from '@tabler/icons';
+import moment from 'moment';
+import 'moment-timezone';
+import { Tooltip } from 'react-tooltip';
+import { isEmpty } from 'lodash';
+
+const removeEmptyValues = (obj) => {
+ return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null && value !== undefined));
+};
+
+const ModifyCookieModal = ({ onClose, domain, cookie }) => {
+ const dispatch = useDispatch();
+ const [isRawMode, setIsRawMode] = useState(false);
+ const [cookieString, setCookieString] = useState('');
+ const initialParseRef = useRef(false);
+
+ const formik = useFormik({
+ enableReinitialize: true,
+ initialValues: {
+ ...(cookie ? cookie : {}),
+ key: cookie?.key || '',
+ value: cookie?.value || '',
+ path: cookie?.path || '/',
+ domain: cookie?.domain || domain || '',
+ expires: cookie?.expires ? moment(cookie.expires).format(moment.HTML5_FMT.DATETIME_LOCAL) : '',
+ secure: cookie?.secure || false,
+ httpOnly: cookie?.httpOnly || false
+ },
+ validationSchema: Yup.object({
+ key: Yup.string().required('Key is required'),
+ value: Yup.string().required('Value is required'),
+ domain: Yup.string().required('Domain is required'),
+ secure: Yup.boolean(),
+ httpOnly: Yup.boolean(),
+ expires: Yup.mixed()
+ .nullable()
+ .transform((value) => {
+ if (!value || value === '') return null;
+ return moment(value).isValid() ? moment(value).toDate() : null;
+ })
+ .test('future-date', 'Expiration date must be in the future', (value) => {
+ if (!value) return true;
+ return moment(value).isAfter(moment());
+ })
+ }),
+ onSubmit: (values) => {
+ const modValues = removeEmptyValues({
+ ...(cookie ? cookie : {}),
+ ...values,
+ expires: values.expires
+ ? moment(values.expires).isValid()
+ ? moment(values.expires).toDate()
+ : Infinity
+ : Infinity
+ });
+
+ handleCookieDispatch(cookie, domain, modValues, onClose);
+ }
+ });
+
+ const title = cookie ? 'Modify Cookie' : 'Add Cookie';
+
+ const handleCookieDispatch = (cookie, domain, modValues, onClose) => {
+ if (cookie) {
+ dispatch(modifyCookie(domain, cookie, modValues))
+ .then(() => {
+ toast.success('Cookie modified successfully');
+ onClose();
+ })
+ .catch((err) => {
+ toast.error('An error occurred while modifying cookie');
+ console.error(err);
+ });
+ } else {
+ dispatch(addCookie(domain, modValues))
+ .then(() => {
+ toast.success('Cookie added successfully');
+ onClose();
+ })
+ .catch((err) => {
+ toast.error('An error occurred while adding cookie');
+ console.error(err);
+ });
+ }
+ };
+
+ const onSubmit = async () => {
+ try {
+ if (isRawMode) {
+ const cookieObj = await dispatch(getParsedCookie(cookieString));
+
+ const modifiedCookie = removeEmptyValues({
+ ...formik.values,
+ ...cookieObj,
+ expires: cookieObj?.expires
+ ? moment(cookieObj.expires).isValid()
+ ? moment(cookieObj.expires).toDate()
+ : Infinity
+ : Infinity
+ });
+
+ if (!cookieObj) {
+ toast.error('Please enter a valid cookie string');
+ return;
+ }
+
+ const validationErrors = await formik.setValues(
+ (values) => ({
+ ...values,
+ ...modifiedCookie,
+ expires:
+ modifiedCookie?.expires && moment(modifiedCookie.expires).isValid()
+ ? moment(new Date(modifiedCookie.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)
+ : ''
+ }),
+ true
+ );
+
+ if (!isEmpty(validationErrors)) {
+ toast.error(Object.values(validationErrors).join("\n"));
+ return;
+ }
+
+ handleCookieDispatch(cookie, domain, modifiedCookie, onClose);
+ } else {
+ formik.handleSubmit();
+ }
+ } catch (error) {
+ const errMsg = error.message || 'An error occurred while parsing cookie string';
+ toast.error(errMsg);
+ }
+ };
+
+ useEffect(() => {
+ if (!isRawMode) return;
+ const loadCookieString = async () => {
+ if (cookie) {
+ const str = await dispatch(createCookieString(cookie));
+ setCookieString(str);
+ }
+ return '';
+ };
+
+ loadCookieString();
+ }, [cookie, isRawMode]);
+
+ // create the cookieString when raw mode is enabled
+ useEffect(() => {
+ if (isRawMode) {
+ const createCookieStr = async () => {
+ const str = await dispatch(createCookieString(formik.values));
+ setCookieString(str);
+ };
+
+ createCookieStr();
+ }
+ }, [isRawMode, formik.values]);
+
+ useEffect(() => {
+ // Reset the ref when raw mode changes
+ if (isRawMode) {
+ initialParseRef.current = false;
+ return;
+ }
+
+ const setParsedCookie = async () => {
+ if (!isRawMode && cookieString && !initialParseRef.current) {
+ initialParseRef.current = true;
+
+ try {
+ const cookieObj = await dispatch(getParsedCookie(cookieString));
+
+ if (!cookieObj) return;
+
+ formik.setValues(
+ (values) => ({
+ ...values,
+ ...removeEmptyValues(cookieObj),
+ expires:
+ cookieObj?.expires && moment(cookieObj.expires).isValid()
+ ? moment(new Date(cookieObj.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)
+ : ''
+ }),
+ true
+ );
+ } catch (error) {
+ const errMsg = error.message || 'An error occurred while parsing cookie string';
+ toast.error(errMsg);
+ }
+ }
+ };
+
+ setParsedCookie();
+ }, [isRawMode, cookieString, dispatch, formik]);
+
+ return (
+
+ {title}
+
+ {
+ setIsRawMode(e.target.checked);
+ }}
+ />
+ Edit Raw
+
+
+ }
+ >
+
+
+ );
+};
+
+export default ModifyCookieModal;
diff --git a/packages/bruno-app/src/components/Cookies/StyledWrapper.js b/packages/bruno-app/src/components/Cookies/StyledWrapper.js
index 102558382..5e3cdd125 100644
--- a/packages/bruno-app/src/components/Cookies/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Cookies/StyledWrapper.js
@@ -11,6 +11,65 @@ const Wrapper = styled.div`
user-select: none;
}
}
+
+ &.header {
+ input {
+ padding: 0.3rem 0.5rem;
+ }
+ }
+
+ .textbox {
+ line-height: 1.42857143;
+ border: 1px solid #ccc;
+ padding: 0.45rem;
+ box-shadow: none;
+ border-radius: 0px;
+ outline: none;
+ box-shadow: none;
+ transition: border-color ease-in-out 0.1s;
+ border-radius: 3px;
+ background-color: ${(props) => props.theme.modal.input.bg};
+ border: 1px solid ${(props) => props.theme.modal.input.border};
+
+ &:focus {
+ border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
+ outline: none !important;
+ }
+ }
+
+ .scroll-box {
+ max-height: 500px;
+ overflow-y: auto;
+
+ background:
+ /* Shadow Cover TOP */
+ linear-gradient(
+ ${(props) => props.theme.modal.body.bg} 20%,
+ rgba(255, 255, 255, 0)
+ ) center top,
+
+ /* Shadow Cover BOTTOM */
+ linear-gradient(
+ rgba(255, 255, 255, 0),
+ ${(props) => props.theme.modal.body.bg} 80%
+ ) center bottom,
+
+ /* Shadow TOP */
+ linear-gradient(
+ rgba(0, 0, 0, 0.1) 0%,
+ rgba(0, 0, 0, 0) 100%
+ ) center top,
+
+ /* Shadow BOTTOM */
+ linear-gradient(
+ rgba(0, 0, 0, 0) 0%,
+ rgba(0, 0, 0, 0.1) 100%
+ ) center bottom;
+
+ background-repeat: no-repeat;
+ background-size: 100% 30px, 100% 30px, 100% 10px, 100% 10px;
+ background-attachment: local, local, scroll, scroll;
+ }
`;
export default Wrapper;
diff --git a/packages/bruno-app/src/components/Cookies/index.js b/packages/bruno-app/src/components/Cookies/index.js
index f7420bed8..2afa3d37d 100644
--- a/packages/bruno-app/src/components/Cookies/index.js
+++ b/packages/bruno-app/src/components/Cookies/index.js
@@ -1,53 +1,331 @@
-import React from 'react';
+import React, { useState, useRef, useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Modal from 'components/Modal';
-import { IconTrash } from '@tabler/icons';
-import { deleteCookiesForDomain } from 'providers/ReduxStore/slices/app';
+import Accordion from 'components/Accordion/index';
+import { IconTrash, IconEdit, IconCirclePlus, IconCookieOff, IconAlertTriangle, IconSearch } from '@tabler/icons';
+import { deleteCookiesForDomain, deleteCookie } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast';
-
+import ModifyCookieModal from 'components/Cookies/ModifyCookieModal/index';
import StyledWrapper from './StyledWrapper';
+import moment from 'moment';
+import { Tooltip } from 'react-tooltip';
+
+const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
+
+
+
+
Hold on..
+
+
+ Are you sure you want to clear all cookies for the domain {domain}?
+
+
+
+
+
+ Close
+
+
+
+
+ Clear All
+
+
+
+
+);
+
+const DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (
+
+
+
+
Hold on..
+
+
+ Are you sure you want to delete the cookie {cookieName}?
+
+
+
+
+
+ Close
+
+
+
+
+ Delete
+
+
+
+
+);
const CollectionProperties = ({ onClose }) => {
const dispatch = useDispatch();
const cookies = useSelector((state) => state.app.cookies) || [];
+ const [isModifyCookieModalOpen, setIsModifyCookieModalOpen] = useState(false);
+ const [currentDomain, setCurrentDomain] = useState(null);
+ const [cookieToEdit, setCookieToEdit] = useState(null);
- const handleDeleteDomain = (domain) => {
- dispatch(deleteCookiesForDomain(domain))
- .then(() => {
- toast.success('Domain deleted successfully');
- })
- .catch((err) => console.log(err) && toast.error('Failed to delete domain'));
+ const [domainToClear, setDomainToClear] = useState(null);
+ const [cookieToDelete, setCookieToDelete] = useState(null);
+ const [searchText, setSearchText] = useState(null);
+
+ const handleAddCookie = (domain) => {
+ if(domain) setCurrentDomain(domain);
+ setIsModifyCookieModalOpen(true);
};
+ const handleEditCookie = (domain, cookie) => {
+ setCurrentDomain(domain);
+ setCookieToEdit(cookie);
+ setIsModifyCookieModalOpen(true);
+ };
+
+ const handleClearDomainCookies = (domain) => {
+ setDomainToClear(domain);
+ };
+
+ const clearDomainCookiesAction = () => {
+ dispatch(deleteCookiesForDomain(domainToClear))
+ .then(() => {
+ toast.success('Domain cookies cleared successfully');
+ })
+ .catch((err) => console.log(err) && toast.error('Failed to clear domain cookies'));
+ setDomainToClear(null);
+ };
+
+ const handleDeleteCookie = (domain, path, key) => {
+ setCookieToDelete({ key, domain, path });
+ };
+
+ const deleteCookieAction = () => {
+ if (cookieToDelete) {
+ const { domain, path, key } = cookieToDelete;
+ dispatch(deleteCookie(domain, path, key))
+ .then(() => {
+ toast.success('Cookie deleted successfully');
+ })
+ .catch((err) => console.log(err) && toast.error('Failed to delete cookie'));
+ }
+ setCookieToDelete(null);
+ };
+
+ const filteredCookies = useMemo(() => {
+ if (!searchText) return cookies;
+
+ return cookies.filter((cookie) =>
+ cookie.domain.toLowerCase().includes(searchText.toLowerCase())
+ );
+ }, [cookies, searchText]);
+
+ const shouldShowHeader = cookies && cookies.length > 0;
+
return (
-
-
-
-
-
- Domain
- Cookie
-
- Actions
-
-
-
-
- {cookies.map((cookie) => (
-
- {cookie.domain}
- {cookie.cookieString}
-
- handleDeleteDomain(cookie.domain)}>
-
-
-
-
- ))}
-
-
-
-
+ <>
+
+ Cookies
+ setSearchText(e.target.value)}
+ className="block textbox non-passphrase-input ml-auto font-normal"
+ />
+ {
+ e.stopPropagation();
+ handleAddCookie();
+ }}
+ >
+
+ Add Cookie
+
+
+ ) : null}
+ >
+
+ {!cookies || !cookies.length ? (
+ // No cookies found
+
+
+
No cookies found
+
Add cookies to get started
+
{
+ e.stopPropagation();
+ handleAddCookie();
+ }}
+ >
+
+ Add Cookie
+
+
+ ) : cookies.length && !filteredCookies.length ? (
+ // No search results
+
+
+
No search results
+
Try a different search term
+
+ ) : (
+ // Show cookies list
+
+
+ {filteredCookies.map((domainWithCookies, i) => (
+
+
+
+
{domainWithCookies.domain}
+
+ ({domainWithCookies.cookies.length}{' '}
+ {domainWithCookies.cookies.length === 1 ? 'cookie' : 'cookies'})
+
+
+ {
+ e.stopPropagation();
+ handleAddCookie(domainWithCookies.domain);
+ }}
+ >
+
+
+ {
+ e.stopPropagation();
+ handleClearDomainCookies(domainWithCookies.domain);
+ }}
+ className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600 mr-2"
+ >
+
+
+
+
+
+
+
+
+
+
+ Name
+ Value
+ Path
+ Expires
+ Secure
+ HTTP Only
+ Actions
+
+
+
+ {domainWithCookies.cookies.map((cookie) => (
+
+
+ {cookie.key}
+
+
+
+ {cookie.value}
+
+
+ {cookie.path || '/'}
+
+
+ {cookie.expires && moment(cookie.expires).isValid()
+ ? new Date(cookie.expires).toLocaleString()
+ : 'Session'}
+
+ {cookie.expires && moment(cookie.expires).isValid() && (
+
+ )}
+
+ {cookie.secure ? '✓' : ''}
+ {cookie.httpOnly ? '✓' : ''}
+
+
+ {
+ e.stopPropagation();
+ handleEditCookie(domainWithCookies.domain, cookie);
+ }}
+ className="text-gray-700 hover:text-gray-950
+ dark:text-white dark:hover:text-gray-300"
+ >
+
+
+ {
+ e.stopPropagation();
+ handleDeleteCookie(domainWithCookies.domain, cookie.path, cookie.key);
+ }}
+ className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600"
+ >
+
+
+
+
+
+ ))}
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ {isModifyCookieModalOpen && (
+
{
+ setCookieToEdit(null);
+ setCurrentDomain(null);
+ setIsModifyCookieModalOpen(false);
+ }}
+ domain={currentDomain}
+ cookie={cookieToEdit}
+ />
+ )}
+ {domainToClear ? (
+ setDomainToClear(null)}
+ domain={domainToClear}
+ onClear={clearDomainCookiesAction}
+ />
+ ) : null}
+ {cookieToDelete ? (
+ setCookieToDelete(null)}
+ cookieName={cookieToDelete.key}
+ onDelete={deleteCookieAction}
+ />
+ ) : null}
+ >
);
};
diff --git a/packages/bruno-app/src/components/Documentation/StyledWrapper.js b/packages/bruno-app/src/components/Documentation/StyledWrapper.js
index f159d94dc..af80d4c08 100644
--- a/packages/bruno-app/src/components/Documentation/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Documentation/StyledWrapper.js
@@ -3,7 +3,6 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
.editing-mode {
cursor: pointer;
- color: ${(props) => props.theme.colors.text.yellow};
}
`;
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
index c5130d038..4f3dcb5ba 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js
@@ -6,6 +6,7 @@ import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
+import { validateName, validateNameError } from 'utils/common/regex';
const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch();
@@ -23,7 +24,11 @@ const CreateEnvironment = ({ collection, onClose }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'Must be at least 1 character')
- .max(50, 'Must be 50 characters or less')
+ .max(255, 'Must be 255 characters or less')
+ .test('is-valid-filename', function(value) {
+ const isValid = validateName(value);
+ return isValid ? true : this.createError({ message: validateNameError(value) });
+ })
.required('Name is required')
.test('duplicate-name', 'Environment already exists', validateEnvironmentName)
}),
diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js
index 3ebcadca1..fee403d8a 100644
--- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js
+++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/RenameEnvironment/index.js
@@ -6,6 +6,7 @@ import { useFormik } from 'formik';
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
+import { validateName, validateNameError } from 'utils/common/regex';
const RenameEnvironment = ({ onClose, environment, collection }) => {
const dispatch = useDispatch();
@@ -18,7 +19,11 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be at least 1 character')
- .max(50, 'must be 50 characters or less')
+ .max(255, 'Must be 255 characters or less')
+ .test('is-valid-filename', function(value) {
+ const isValid = validateName(value);
+ return isValid ? true : this.createError({ message: validateNameError(value) });
+ })
.required('name is required')
}),
onSubmit: (values) => {
diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js
index 797771bbb..26969dde3 100644
--- a/packages/bruno-app/src/components/FilePickerEditor/index.js
+++ b/packages/bruno-app/src/components/FilePickerEditor/index.js
@@ -1,15 +1,13 @@
import React from 'react';
-import path from 'path';
+import path from 'utils/common/path';
import { useDispatch } from 'react-redux';
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
import { IconX } from '@tabler/icons';
import { isWindowsOS } from 'utils/common/platform';
-import slash from 'utils/common/slash';
-const FilePickerEditor = ({ value, onChange, collection }) => {
- value = value || [];
+const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => {
const dispatch = useDispatch();
- const filenames = value
+ const filenames = (isSingleFilePicker ? [value] : value || [])
.filter((v) => v != null && v != '')
.map((v) => {
const separator = isWindowsOS() ? '\\' : '/';
@@ -20,7 +18,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
const title = filenames.map((v) => `- ${v}`).join('\n');
const browse = () => {
- dispatch(browseFiles())
+ dispatch(browseFiles([], [!isSingleFilePicker ? "multiSelections": ""]))
.then((filePaths) => {
// If file is in the collection's directory, then we use relative path
// Otherwise, we use the absolute path
@@ -28,13 +26,13 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
const collectionDir = collection.pathname;
if (filePath.startsWith(collectionDir)) {
- return path.relative(slash(collectionDir), slash(filePath));
+ return path.relative(collectionDir, filePath);
}
return filePath;
});
- onChange(filePaths);
+ onChange(isSingleFilePicker ? filePaths[0] : filePaths);
})
.catch((error) => {
console.error(error);
@@ -42,14 +40,14 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
};
const clear = () => {
- onChange([]);
+ onChange(isSingleFilePicker ? '' : []);
};
const renderButtonText = (filenames) => {
if (filenames.length == 1) {
return filenames[0];
}
- return filenames.length + ' files selected';
+ return filenames.length + ' file(s) selected';
};
return filenames.length > 0 ? (
@@ -66,9 +64,9 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
) : (
- Select Files
+ {isSingleFilePicker ? 'Select File' : 'Select Files'}
);
};
-export default FilePickerEditor;
+export default FilePickerEditor;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js
index 17d79629e..b0815c018 100644
--- a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js
+++ b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js
@@ -88,7 +88,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
Expr
-
+
)}
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSelector/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSelector/index.js
index 5bf55809c..b0042bcbf 100644
--- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSelector/index.js
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSelector/index.js
@@ -81,7 +81,10 @@ const EnvironmentSelector = () => {
No Environment
-
+
{
+ handleSettingsIconClick();
+ dropdownTippyRef.current.hide();
+ }}>
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment/index.js
index 3bf8af65e..d9eb83191 100644
--- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment/index.js
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment/index.js
@@ -6,6 +6,7 @@ import { useDispatch, useSelector } from 'react-redux';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
+import { validateName, validateNameError } from 'utils/common/regex';
const CreateEnvironment = ({ onClose }) => {
const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);
@@ -25,7 +26,11 @@ const CreateEnvironment = ({ onClose }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'Must be at least 1 character')
- .max(50, 'Must be 50 characters or less')
+ .max(255, 'Must be 255 characters or less')
+ .test('is-valid-filename', function(value) {
+ const isValid = validateName(value);
+ return isValid ? true : this.createError({ message: validateNameError(value) });
+ })
.required('Name is required')
.test('duplicate-name', 'Global Environment already exists', validateEnvironmentName)
}),
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/RenameEnvironment/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/RenameEnvironment/index.js
index ff1809383..581abd27c 100644
--- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/RenameEnvironment/index.js
+++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/RenameEnvironment/index.js
@@ -3,10 +3,10 @@ import Portal from 'components/Portal/index';
import Modal from 'components/Modal/index';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
-import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import { renameGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
+import { validateName, validateNameError } from 'utils/common/regex';
const RenameEnvironment = ({ onClose, environment }) => {
const dispatch = useDispatch();
@@ -19,7 +19,11 @@ const RenameEnvironment = ({ onClose, environment }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be at least 1 character')
- .max(50, 'must be 50 characters or less')
+ .max(255, 'Must be 255 characters or less')
+ .test('is-valid-filename', function(value) {
+ const isValid = validateName(value);
+ return isValid ? true : this.createError({ message: validateNameError(value) });
+ })
.required('name is required')
}),
onSubmit: (values) => {
diff --git a/packages/bruno-app/src/components/Help/StyledWrapper.js b/packages/bruno-app/src/components/Help/StyledWrapper.js
new file mode 100644
index 000000000..f4a69fe40
--- /dev/null
+++ b/packages/bruno-app/src/components/Help/StyledWrapper.js
@@ -0,0 +1,11 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.div`
+ font-weight: 400;
+ font-size: 0.75rem;
+ background-color: ${props => props.theme.infoTip.bg};
+ border: 1px solid ${props => props.theme.infoTip.border};
+ box-shadow: ${props => props.theme.infoTip.boxShadow};
+`;
+
+export default Wrapper;
diff --git a/packages/bruno-app/src/components/Help/index.js b/packages/bruno-app/src/components/Help/index.js
new file mode 100644
index 000000000..6d3f40f87
--- /dev/null
+++ b/packages/bruno-app/src/components/Help/index.js
@@ -0,0 +1,40 @@
+/**
+ * The InfoTip components needs to be nuked
+ * This component will be the future replacement
+ * We should allow icon and placement props to be passed in
+ */
+
+import React, { useState } from 'react';
+import HelpIcon from 'components/Icons/Help';
+import StyledWrapper from './StyledWrapper';
+
+const Help = ({ children, width = 200 }) => {
+ const [showTooltip, setShowTooltip] = useState(false);
+
+ return (
+
+ setShowTooltip(true)}
+ onMouseLeave={() => setShowTooltip(false)}
+ >
+
+
+ {showTooltip && (
+
+ {children}
+
+ )}
+
+ );
+};
+
+export default Help;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Icons/Help/index.js b/packages/bruno-app/src/components/Icons/Help/index.js
new file mode 100644
index 000000000..95c8710af
--- /dev/null
+++ b/packages/bruno-app/src/components/Icons/Help/index.js
@@ -0,0 +1,20 @@
+import React from 'react';
+
+const HelpIcon = ({ size = 14 }) => {
+ return (
+
+
+
+
+ )
+}
+
+export default HelpIcon;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Icons/OpenAPILogo/index.js b/packages/bruno-app/src/components/Icons/OpenAPILogo/index.js
new file mode 100644
index 000000000..b472b3d8c
--- /dev/null
+++ b/packages/bruno-app/src/components/Icons/OpenAPILogo/index.js
@@ -0,0 +1,104 @@
+const OpenApiLogo = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default OpenApiLogo;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/InfoTip/index.js b/packages/bruno-app/src/components/InfoTip/index.js
index 97eb63d4d..2af6034ed 100644
--- a/packages/bruno-app/src/components/InfoTip/index.js
+++ b/packages/bruno-app/src/components/InfoTip/index.js
@@ -1,7 +1,7 @@
import React from 'react';
import { Tooltip as ReactInfoTip } from 'react-tooltip';
-const InfoTip = ({ text, infotipId }) => {
+const InfoTip = ({ html: _ignored, infotipId, ...props }) => {
return (
<>
{
-
+
>
);
};
diff --git a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js
index fa1269e14..a7a174a69 100644
--- a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js
+++ b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js
@@ -9,21 +9,20 @@ const StyledMarkdownBodyWrapper = styled.div`
box-sizing: border-box;
height: 100%;
margin: 0 auto;
- padding-top: 0.5rem;
font-size: 0.875rem;
h1 {
margin: 0.67em 0;
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: 0.3em;
- font-size: 1.3;
+ font-size: 1.3em;
border-bottom: 1px solid var(--color-border-muted);
}
h2 {
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: 0.3em;
- font-size: 1.2;
+ font-size: 1.2em;
border-bottom: 1px solid var(--color-border-muted);
}
@@ -80,12 +79,6 @@ const StyledMarkdownBodyWrapper = styled.div`
}
}
}
-
- @media (max-width: 767px) {
- .markdown-body {
- padding: 15px;
- }
- }
`;
export default StyledMarkdownBodyWrapper;
diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js
index 0b44b928b..99bf1f89d 100644
--- a/packages/bruno-app/src/components/Modal/index.js
+++ b/packages/bruno-app/src/components/Modal/index.js
@@ -16,7 +16,7 @@ const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
);
-const ModalContent = ({ children }) =>
{children}
;
+const ModalContent = ({ children }) =>
{children}
;
const ModalFooter = ({
confirmText,
diff --git a/packages/bruno-app/src/components/Notifications/index.js b/packages/bruno-app/src/components/Notifications/index.js
index ba257bf48..d11a6254f 100644
--- a/packages/bruno-app/src/components/Notifications/index.js
+++ b/packages/bruno-app/src/components/Notifications/index.js
@@ -3,6 +3,7 @@ import { useState } from 'react';
import StyledWrapper from './StyleWrapper';
import Modal from 'components/Modal/index';
import { useEffect } from 'react';
+import { useApp } from 'providers/App';
import {
fetchNotifications,
markAllNotificationsAsRead,
@@ -11,18 +12,18 @@ import {
import { useDispatch, useSelector } from 'react-redux';
import { humanizeDate, relativeDate } from 'utils/common';
import ToolHint from 'components/ToolHint';
-import { useTheme } from 'providers/Theme';
+import DOMPurify from 'dompurify';
const PAGE_SIZE = 5;
const Notifications = () => {
const dispatch = useDispatch();
+ const { version } = useApp();
const notifications = useSelector((state) => state.notifications.notifications);
const [showNotificationsModal, setShowNotificationsModal] = useState(false);
const [selectedNotification, setSelectedNotification] = useState(null);
const [pageNumber, setPageNumber] = useState(1);
- const { storedTheme } = useTheme();
const notificationsStartIndex = (pageNumber - 1) * PAGE_SIZE;
const notificationsEndIndex = pageNumber * PAGE_SIZE;
@@ -30,7 +31,9 @@ const Notifications = () => {
const unreadNotifications = notifications.filter((notification) => !notification.read);
useEffect(() => {
- dispatch(fetchNotifications());
+ dispatch(fetchNotifications({
+ currentVersion: version
+ }));
}, []);
useEffect(() => {
@@ -66,6 +69,13 @@ const Notifications = () => {
dispatch(markNotificationAsRead({ notificationId: notification?.id }));
};
+ const getSanitizedDescription = (description) => {
+ return DOMPurify.sanitize(encodeURIComponent(description), {
+ ALLOWED_TAGS: ['a', 'ul', 'img', 'li', 'div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
+ ALLOWED_ATTR: ['href', 'style', 'target', 'src', 'alt']
+ });
+ };
+
const modalCustomHeader = (
) : (
diff --git a/packages/bruno-app/src/components/PathDisplay/StyledWrapper.js b/packages/bruno-app/src/components/PathDisplay/StyledWrapper.js
new file mode 100644
index 000000000..bdaca8bbf
--- /dev/null
+++ b/packages/bruno-app/src/components/PathDisplay/StyledWrapper.js
@@ -0,0 +1,38 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ .path-display {
+ background: ${(props) => props.theme.requestTabPanel.url.bg};
+ border-radius: 4px;
+ padding: 8px 12px;
+ font-size: 0.8125rem;
+ border: 1px solid rgba(0, 0, 0, 0.08);
+
+ .icon-column {
+ padding-right: 8px;
+ align-items: flex-start;
+ padding-top: 2px;
+ }
+
+ .path-container {
+ flex-wrap: wrap;
+ }
+
+ .path-segment {
+ white-space: nowrap;
+ }
+
+
+ .filename, .file-extension {
+ color: ${(props) => props.theme.colors.text.yellow};
+ }
+
+ .separator {
+ color: ${(props) => props.theme.text};
+ opacity: 0.6;
+ margin: 0 1px;
+ }
+ }
+`;
+
+export default StyledWrapper;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/PathDisplay/index.js b/packages/bruno-app/src/components/PathDisplay/index.js
new file mode 100644
index 000000000..b7ffa087f
--- /dev/null
+++ b/packages/bruno-app/src/components/PathDisplay/index.js
@@ -0,0 +1,67 @@
+import React from 'react';
+import { IconEdit, IconFolder, IconFile } from '@tabler/icons';
+import path from 'utils/common/path';
+import StyledWrapper from './StyledWrapper';
+
+const PathDisplay = ({
+ collection,
+ item,
+ filename,
+ extension = '.bru',
+ showExtension = true,
+ toggleEditingFilename,
+ showDirectory = false
+}) => {
+ const relativePath = item?.pathname && path.relative(collection?.pathname, showDirectory ? path.dirname(item?.pathname) : item?.pathname);
+ const pathSegments = relativePath?.split(path.sep).filter(Boolean);
+
+ return (
+
+
+
+ Location
+ toggleEditingFilename(true)}
+ />
+
+
+
+
+ {showExtension ? : }
+
+
+
+ {collection?.name}
+
+
+ {pathSegments?.length > 0 && pathSegments?.map((segment, index) => (
+
+ /
+
+ {segment}
+
+
+ ))}
+
+ {collection && (
+
/
+ )}
+
+
+ {filename}
+ {showExtension && filename?.length ? (
+ {extension}
+ ) : null}
+
+
+
+
+
+
+ );
+};
+
+export default PathDisplay;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Preferences/General/index.js b/packages/bruno-app/src/components/Preferences/General/index.js
index 2867d9841..26054a881 100644
--- a/packages/bruno-app/src/components/Preferences/General/index.js
+++ b/packages/bruno-app/src/components/Preferences/General/index.js
@@ -6,8 +6,7 @@ import { savePreferences } from 'providers/ReduxStore/slices/app';
import StyledWrapper from './StyledWrapper';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
-import path from 'path';
-import slash from 'utils/common/slash';
+import path from 'utils/common/path';
import { IconTrash } from '@tabler/icons';
const General = ({ close }) => {
@@ -134,7 +133,7 @@ const General = ({ close }) => {
className={`flex items-center mt-2 pl-6 ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
>
- {path.basename(slash(formik.values.customCaCertificate.filePath))}
+ {path.basename(formik.values.customCaCertificate.filePath)}
props.theme.table.border};
+ }
+
+ thead {
+ color: ${(props) => props.theme.table.thead.color};
+ font-size: 0.8125rem;
+ user-select: none;
+ }
+ td {
+ padding: 6px 10px;
+
+ &:nth-child(1) {
+ width: 30%;
+ }
+
+ &:nth-child(2) {
+ width: 45%;
+ }
+
+ &:nth-child(3) {
+ width: 25%;
+ }
+
+ &:nth-child(4) {
+ width: 70px;
+ }
+ }
+ }
+
+ .btn-add-param {
+ font-size: 0.8125rem;
+ }
+
+ input[type='text'] {
+ width: 100%;
+ border: solid 1px transparent;
+ outline: none !important;
+ color: ${(props) => props.theme.table.input.color};
+ background: transparent;
+
+ &:focus {
+ outline: none !important;
+ border: solid 1px transparent;
+ }
+ }
+
+ input[type='radio'] {
+ cursor: pointer;
+ position: relative;
+ top: 1px;
+ }
+`;
+
+export default Wrapper;
diff --git a/packages/bruno-app/src/components/RequestPane/FileBody/index.js b/packages/bruno-app/src/components/RequestPane/FileBody/index.js
new file mode 100644
index 000000000..d97953aa5
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/FileBody/index.js
@@ -0,0 +1,164 @@
+import React, { useState, useEffect } from 'react';
+import { get, cloneDeep, isArray } from 'lodash';
+import { IconTrash } from '@tabler/icons';
+import { useDispatch } from 'react-redux';
+import { useTheme } from 'providers/Theme';
+import { addFile as _addFile, updateFile, deleteFile } from 'providers/ReduxStore/slices/collections/index';
+import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
+import StyledWrapper from './StyledWrapper';
+import FilePickerEditor from 'components/FilePickerEditor/index';
+import SingleLineEditor from 'components/SingleLineEditor/index';
+
+const FileBody = ({ item, collection }) => {
+ const dispatch = useDispatch();
+ const { storedTheme } = useTheme();
+ const params = item.draft ? get(item, 'draft.request.body.file') : get(item, 'request.body.file');
+
+ const [enabledFileUid, setEnableFileUid] = useState(params && params.length ? params[0].uid : '');
+
+ const addFile = () => {
+ dispatch(
+ _addFile({
+ itemUid: item.uid,
+ collectionUid: collection.uid,
+ })
+ );
+ };
+
+ const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
+ const handleRun = () => dispatch(sendRequest(item, collection.uid));
+
+ const handleParamChange = (e, _param, type) => {
+ const param = cloneDeep(_param);
+ switch (type) {
+ case 'filePath': {
+ param.filePath = e.target.filePath;
+ param.contentType = "";
+ break;
+ }
+ case 'contentType': {
+ param.contentType = e.target.contentType;
+ break;
+ }
+ case 'selected': {
+ param.selected = e.target.selected;
+ setEnableFileUid(param.uid)
+ break;
+ }
+ }
+ dispatch(
+ updateFile({
+ param: param,
+ itemUid: item.uid,
+ collectionUid: collection.uid
+ })
+ );
+ };
+
+ const handleRemoveParams = (param) => {
+ dispatch(
+ deleteFile({
+ paramUid: param.uid,
+ itemUid: item.uid,
+ collectionUid: collection.uid
+ })
+ );
+ };
+
+ return (
+
+
+
+
+
+ File
+
+
+ Content-Type
+
+
+ Selected
+
+
+
+
+
+ {params && params.length
+ ? params.map((param, index) => {
+ return (
+
+
+
+ handleParamChange(
+ {
+ target: {
+ filePath: path
+ }
+ },
+ param,
+ 'filePath'
+ )
+ }
+ collection={collection}
+ />
+
+
+
+ handleParamChange(
+ {
+ target: {
+ contentType: newValue
+ }
+ },
+ param,
+ 'contentType'
+ )
+ }
+ onRun={handleRun}
+ collection={collection}
+ />
+
+
+
+ handleParamChange(e, param, 'selected')}
+ />
+
+
+
+
+ handleRemoveParams(param)}>
+
+
+
+
+
+ );
+ })
+ : null}
+
+
+
+
+ + Add File
+
+
+
+ );
+};
+export default FileBody;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js
index 187a91a68..07dcf1419 100644
--- a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js
+++ b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js
@@ -154,7 +154,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
- {getTabPanel(focusedTab.requestPaneTab)}
+ {getTabPanel(focusedTab.requestPaneTab)}
);
};
diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js
index 91fea0134..eaac6f204 100644
--- a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js
+++ b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js
@@ -49,7 +49,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
-
+ <>
{
onRun={onRun}
onSave={onSave}
/>
-
+ >
);
};
diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
index 09a665e9f..7bbc903b2 100644
--- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
+++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js
@@ -15,6 +15,7 @@ import Tests from 'components/RequestPane/Tests';
import StyledWrapper from './StyledWrapper';
import { find, get } from 'lodash';
import Documentation from 'components/Documentation/index';
+import { useEffect } from 'react';
const ContentIndicator = () => {
return (
@@ -24,6 +25,14 @@ const ContentIndicator = () => {
);
};
+const ErrorIndicator = () => {
+ return (
+
+
+
+ );
+};
+
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
@@ -111,6 +120,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
requestVars.filter((request) => request.enabled).length +
responseVars.filter((response) => response.enabled).length;
+ useEffect(() => {
+ if (activeParamsLength === 0 && body.mode !== 'none') {
+ selectTab('body');
+ }
+ }, []);
+
return (
@@ -136,7 +151,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
selectTab('script')}>
Script
- {(script.req || script.res) && }
+ {(script.req || script.res) && (
+ item.preScriptResponseErrorMessage || item.postResponseScriptErrorMessage ?
+ :
+
+ )}
selectTab('assert')}>
Assert
diff --git a/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js b/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js
index 60bafc8fe..6571c14ae 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js
@@ -109,7 +109,7 @@ export default class QueryEditor extends React.Component {
this.props.onPrettifyQuery();
}
},
- /* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Pretiffy */
+ /* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Prettify */
'Shift-Ctrl-F': () => {
if (this.props.onPrettifyQuery) {
this.props.onPrettifyQuery();
diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js
index 777280eb0..3f7f7ef01 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js
@@ -176,8 +176,7 @@ const QueryParams = ({ item, collection }) => {
Path
-
Path variables are automatically added whenever the
:name
@@ -186,9 +185,7 @@ const QueryParams = ({ item, collection }) => {
https://example.com/v1/users/:id
- `}
- infotipId="path-param-InfoTip"
- />
+
diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js
index 29b66d58d..db73597df 100644
--- a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js
+++ b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js
@@ -128,6 +128,15 @@ const RequestBodyMode = ({ item, collection }) => {
SPARQL
Other
+ {
+ dropdownTippyRef.current.hide();
+ onModeChange('file');
+ }}
+ >
+ File / Binary
+
{
diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js
index ca60c8662..8f7fa8465 100644
--- a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js
+++ b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js
@@ -8,6 +8,7 @@ import { useTheme } from 'providers/Theme';
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
+import FileBody from '../FileBody/index';
const RequestBody = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -62,6 +63,10 @@ const RequestBody = ({ item, collection }) => {
);
}
+ if (bodyMode === 'file') {
+ return
+ }
+
if (bodyMode === 'formUrlEncoded') {
return ;
}
@@ -72,4 +77,4 @@ const RequestBody = ({ item, collection }) => {
return No Body ;
};
-export default RequestBody;
+export default RequestBody;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/RequestPane/Tests/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Tests/StyledWrapper.js
deleted file mode 100644
index 9f7583222..000000000
--- a/packages/bruno-app/src/components/RequestPane/Tests/StyledWrapper.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import styled from 'styled-components';
-
-const StyledWrapper = styled.div`
- div.CodeMirror {
- /* todo: find a better way */
- height: calc(100vh - 220px);
- }
-`;
-
-export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/RequestPane/Tests/index.js b/packages/bruno-app/src/components/RequestPane/Tests/index.js
index c781d34d5..d0d19c283 100644
--- a/packages/bruno-app/src/components/RequestPane/Tests/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Tests/index.js
@@ -5,7 +5,6 @@ import CodeEditor from 'components/CodeEditor';
import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
-import StyledWrapper from './StyledWrapper';
const Tests = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -28,19 +27,17 @@ const Tests = ({ item, collection }) => {
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
-
-
-
+
);
};
diff --git a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js
index c073135d3..cd3f83797 100644
--- a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js
@@ -98,7 +98,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
) : (
Expr
-
+
), accessor: 'value', width: '46%' },
{ name: '', accessor: '', width: '14%' }
diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/StyledWrapper.js
new file mode 100644
index 000000000..ff6c48575
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/StyledWrapper.js
@@ -0,0 +1,19 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ div.card {
+ background: ${(props) => props.theme.requestTabPanel.card.bg};
+ border: 1px solid ${(props) => props.theme.requestTabPanel.card.border};
+
+ div.hr {
+ border-bottom: 1px solid ${(props) => props.theme.requestTabPanel.card.hr};
+ height: 1px;
+ }
+
+ div.border-top {
+ border-top: 1px solid ${(props) => props.theme.requestTabPanel.card.border};
+ }
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/index.js b/packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/index.js
new file mode 100644
index 000000000..9d2ff1346
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/index.js
@@ -0,0 +1,47 @@
+import { IconLoader2, IconFile } from '@tabler/icons';
+import StyledWrapper from './StyledWrapper';
+
+const RequestIsLoading = ({ item }) => {
+ return
+
+
+
+
+
+ File Info
+
+
+
+
+
Name:
+
+ {item?.name}
+
+
+
+
+
Path:
+
+ {item?.pathname}
+
+
+
+
+
Size:
+
+ {item?.size?.toFixed?.(2)} MB
+
+
+
+
+
+
+ Loading...
+
+
+
+
+
+}
+
+export default RequestIsLoading;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/StyledWrapper.js
new file mode 100644
index 000000000..ff6c48575
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/StyledWrapper.js
@@ -0,0 +1,19 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ div.card {
+ background: ${(props) => props.theme.requestTabPanel.card.bg};
+ border: 1px solid ${(props) => props.theme.requestTabPanel.card.border};
+
+ div.hr {
+ border-bottom: 1px solid ${(props) => props.theme.requestTabPanel.card.hr};
+ height: 1px;
+ }
+
+ div.border-top {
+ border-top: 1px solid ${(props) => props.theme.requestTabPanel.card.border};
+ }
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js
new file mode 100644
index 000000000..7908dfc09
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js
@@ -0,0 +1,83 @@
+import { IconLoader2, IconFile, IconAlertTriangle } from '@tabler/icons';
+import { loadRequest, loadRequestViaWorker } from 'providers/ReduxStore/slices/collections/actions';
+import { useDispatch } from 'react-redux';
+import StyledWrapper from './StyledWrapper';
+
+const RequestNotLoaded = ({ collection, item }) => {
+ const dispatch = useDispatch();
+ const handleLoadRequestViaWorker = () => {
+ !item?.loading && dispatch(loadRequestViaWorker({ collectionUid: collection?.uid, pathname: item?.pathname }));
+ }
+
+ const handleLoadRequest = () => {
+ !item?.loading && dispatch(loadRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));
+ }
+
+ return
+
+
+
+
+
+ File Info
+
+
+
+
+
+
+
Path:
+
{item?.pathname}
+
+
+
+
Size:
+
{item?.size?.toFixed?.(2)} MB
+
+
+ {!item?.error && (
+
+
+
+ The request wasn't loaded due to its large size. Please try again with the following options:
+
+
+
+ Load in background
+
+
(Runs in background)
+
+
+
+ Force load
+
+
(May cause the app to freeze temporarily while it runs)
+
+
+ )}
+
+ {item?.loading && (
+ <>
+
+
+
+ Loading...
+
+ >
+ )}
+
+
+
+
+}
+
+export default RequestNotLoaded;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js
index 4bcfff1c3..d7690e08a 100644
--- a/packages/bruno-app/src/components/RequestTabPanel/index.js
+++ b/packages/bruno-app/src/components/RequestTabPanel/index.js
@@ -22,6 +22,9 @@ import SecuritySettings from 'components/SecuritySettings';
import FolderSettings from 'components/FolderSettings';
import { getGlobalEnvironmentVariables, getGlobalEnvironmentVariablesMasked } from 'utils/collections/index';
import { produce } from 'immer';
+import CollectionOverview from 'components/CollectionSettings/Overview';
+import RequestNotLoaded from './RequestNotLoaded';
+import RequestIsLoading from './RequestIsLoading';
const MIN_LEFT_PANE_WIDTH = 300;
const MIN_RIGHT_PANE_WIDTH = 350;
@@ -153,6 +156,11 @@ const RequestTabPanel = () => {
if (focusedTab.type === 'collection-settings') {
return ;
}
+
+ if (focusedTab.type === 'collection-overview') {
+ return ;
+ }
+
if (focusedTab.type === 'folder-settings') {
const folder = findItemInCollection(collection, focusedTab.folderUid);
return ;
@@ -167,6 +175,14 @@ const RequestTabPanel = () => {
return ;
}
+ if (item?.partial) {
+ return
+ }
+
+ if (item?.loading) {
+ return
+ }
+
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid)).catch((err) =>
toast.custom((t) => toast.dismiss(t.id)} />, {
diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js
index 8ca76b15e..447523fdb 100644
--- a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js
@@ -35,7 +35,7 @@ const CollectionToolBar = ({ collection }) => {
const viewCollectionSettings = () => {
dispatch(
addTab({
- uid: uuid(),
+ uid: collection.uid,
collectionUid: collection.uid,
type: 'collection-settings'
})
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js
index c5d09faa8..b895c10fe 100644
--- a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js
@@ -2,10 +2,18 @@ import React from 'react';
import CloseTabIcon from './CloseTabIcon';
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
-const SpecialTab = ({ handleCloseClick, type, tabName }) => {
+const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => {
const getTabInfo = (type, tabName) => {
switch (type) {
case 'collection-settings': {
+ return (
+
+
+ Collection
+
+ );
+ }
+ case 'collection-overview': {
return (
<>
@@ -23,7 +31,7 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => {
}
case 'folder-settings': {
return (
-
+
{tabName || 'Folder'}
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
index e73313c13..562fc319f 100644
--- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
+++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js
@@ -1,6 +1,6 @@
import React, { useState, useRef, Fragment } from 'react';
import get from 'lodash/get';
-import { closeTabs } from 'providers/ReduxStore/slices/tabs';
+import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
@@ -70,16 +70,16 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
};
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
- if (['collection-settings', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
+ if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
return (
{tab.type === 'folder-settings' ? (
-
+ dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} />
) : (
-
+ dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />
)}
);
@@ -144,8 +144,9 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
/>
)}
dispatch(makeTabPermanent({ uid: tab.uid }))}
onMouseUp={(e) => {
if (!item.draft) return handleMouseUp(e);
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
index b6b8d751d..78993a413 100644
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
@@ -3,48 +3,49 @@ import QueryResultFilter from './QueryResultFilter';
import { JSONPath } from 'jsonpath-plus';
import React from 'react';
import classnames from 'classnames';
+import iconv from 'iconv-lite';
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
import QueryResultPreview from './QueryResultPreview';
-
import StyledWrapper from './StyledWrapper';
-import { useState } from 'react';
-import { useMemo } from 'react';
-import { useEffect } from 'react';
+import { useState, useMemo, useEffect } from 'react';
import { useTheme } from 'providers/Theme/index';
-import { uuid } from 'utils/common/index';
+import { getEncoding, prettifyJson, uuid } from 'utils/common/index';
-const formatResponse = (data, mode, filter) => {
- if (data === undefined) {
+const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
+ if (data === undefined || !dataBuffer) {
return '';
}
- if (data === null) {
- return 'null';
- }
+ // TODO: We need a better way to get the raw response-data here instead
+ // of using this dataBuffer param.
+ // Also, we only need the raw response-data and content-type to show the preview.
+ const rawData = iconv.decode(
+ Buffer.from(dataBuffer, "base64"),
+ iconv.encodingExists(encoding) ? encoding : "utf-8"
+ );
if (mode.includes('json')) {
- let isValidJSON = false;
-
try {
- isValidJSON = typeof JSON.parse(JSON.stringify(data)) === 'object'
+ JSON.parse(rawData);
} catch (error) {
- console.log('Error parsing JSON: ', error.message);
- }
-
- if (!isValidJSON && typeof data === 'string') {
- return data;
+ // If the response content-type is JSON and it fails parsing, its an invalid JSON.
+ // In that case, just show the response as it is in the preview.
+ return rawData;
}
if (filter) {
try {
data = JSONPath({ path: filter, json: data });
+ return prettifyJson(JSON.stringify(data));
} catch (e) {
console.warn('Could not apply JSONPath filter:', e.message);
}
}
- return safeStringifyJSON(data, true);
+ // Prettify the JSON string directly instead of parse->stringify to avoid
+ // issues like rounding numbers bigger than Number.MAX_SAFE_INTEGER etc.
+ return prettifyJson(rawData);
}
if (mode.includes('xml')) {
@@ -59,14 +60,27 @@ const formatResponse = (data, mode, filter) => {
return data;
}
- return safeStringifyJSON(data, true);
+ return prettifyJson(rawData);
+};
+
+const formatErrorMessage = (error) => {
+ if (!error) return 'Something went wrong';
+
+ const remoteMethodError = "Error invoking remote method 'send-http-request':";
+
+ if (error.includes(remoteMethodError)) {
+ const parts = error.split(remoteMethodError);
+ return parts[1]?.trim() || error;
+ }
+
+ return error;
};
const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEventListener, headers, error }) => {
const contentType = getContentType(headers);
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
const [filter, setFilter] = useState(null);
- const formattedData = formatResponse(data, mode, filter);
+ const formattedData = formatResponse(data, dataBuffer, getEncoding(headers), mode, filter);
const { displayedTheme } = useTheme();
const debouncedResultFilterOnChange = debounce((e) => {
@@ -121,6 +135,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
}, [allowedPreviewModes, previewTab]);
const queryFilterEnabled = useMemo(() => mode.includes('json'), [mode]);
+ const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;
return (
{error ? (
-
{error}
+ {hasScriptError ? null :
{formatErrorMessage(error)}
}
{error && typeof error === 'string' && error.toLowerCase().includes('self signed certificate') ? (
@@ -143,24 +158,26 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
) : null}
) : (
- <>
-
- {queryFilterEnabled && (
-
- )}
- >
+
+
+
+ {queryFilterEnabled && (
+
+ )}
+
+
)}
);
diff --git a/packages/bruno-app/src/components/ResponsePane/ScriptError/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/ScriptError/StyledWrapper.js
new file mode 100644
index 000000000..c4a38e80f
--- /dev/null
+++ b/packages/bruno-app/src/components/ResponsePane/ScriptError/StyledWrapper.js
@@ -0,0 +1,55 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ border-left: 4px solid ${(props) => props.theme.colors.text.danger};
+ border-top: 1px solid transparent;
+ border-right: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ border-radius: 0.375rem;
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+ max-height: 200px;
+ min-height: 70px;
+ overflow-y: auto;
+ background-color: ${(props) => props.theme.bg === '#1e1e1e' ? 'rgba(40, 40, 40, 0.5)' : 'rgba(250, 250, 250, 0.9)'};
+
+ .error-icon-container {
+ margin-top: 0.125rem;
+ padding: 0.375rem;
+ border-radius: 9999px;
+ background-color: ${(props) => props.theme.bg === '#1e1e1e' ? 'rgba(40, 40, 40, 0.8)' : 'rgba(240, 240, 240, 0.8)'};
+
+ svg {
+ color: ${(props) => props.theme.colors.text.danger};
+ }
+ }
+
+ .close-button {
+ opacity: 0.7;
+ transition: opacity 0.2s;
+
+ &:hover {
+ opacity: 1;
+ }
+
+ svg {
+ color: ${(props) => props.theme.text};
+ }
+ }
+
+ .error-title {
+ font-weight: 600;
+ margin-bottom: 0.375rem;
+ color: ${(props) => props.theme.colors.text.danger};
+ }
+
+ .error-message {
+ font-family: monospace;
+ font-size: 0.6875rem;
+ line-height: 1.25rem;
+ white-space: pre-wrap;
+ word-break: break-all;
+ color: ${(props) => props.theme.text};
+ }
+`;
+
+export default StyledWrapper;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js b/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js
new file mode 100644
index 000000000..4af07c587
--- /dev/null
+++ b/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import { IconX } from '@tabler/icons';
+import StyledWrapper from './StyledWrapper';
+
+
+const ScriptError = ({ item, onClose }) => {
+ const preRequestError = item?.preRequestScriptErrorMessage;
+ const postResponseError = item?.postResponseScriptErrorMessage;
+
+ if (!preRequestError && !postResponseError) return null;
+
+ const errorMessage = preRequestError || postResponseError;
+ const errorTitle = preRequestError ? 'Pre-Request Script Error' : 'Post-Response Script Error';
+
+ return (
+
+
+
+
+ {errorTitle}
+
+
+ {errorMessage}
+
+
+
+
+
+
+
+ );
+};
+
+export default ScriptError;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/ResponsePane/ScriptErrorIcon/index.js b/packages/bruno-app/src/components/ResponsePane/ScriptErrorIcon/index.js
new file mode 100644
index 000000000..208a49ffd
--- /dev/null
+++ b/packages/bruno-app/src/components/ResponsePane/ScriptErrorIcon/index.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import { IconAlertCircle } from '@tabler/icons';
+import ToolHint from 'components/ToolHint';
+
+const ScriptErrorIcon = ({ itemUid, onClick }) => {
+ const toolhintId = `script-error-icon-${itemUid}`;
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default ScriptErrorIcon;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js
index 91d40ed7d..606c2043d 100644
--- a/packages/bruno-app/src/components/ResponsePane/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/index.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState, useEffect } from 'react';
import find from 'lodash/find';
import classnames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
@@ -13,6 +13,8 @@ import ResponseSize from './ResponseSize';
import Timeline from './Timeline';
import TestResults from './TestResults';
import TestResultsLabel from './TestResultsLabel';
+import ScriptError from './ScriptError';
+import ScriptErrorIcon from './ScriptErrorIcon';
import StyledWrapper from './StyledWrapper';
import ResponseSave from 'src/components/ResponsePane/ResponseSave';
import ResponseClear from 'src/components/ResponsePane/ResponseClear';
@@ -22,6 +24,13 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isLoading = ['queued', 'sending'].includes(item.requestState);
+ const [showScriptErrorCard, setShowScriptErrorCard] = useState(false);
+
+ useEffect(() => {
+ if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage) {
+ setShowScriptErrorCard(true);
+ }
+ }, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage]);
const selectTab = (tab) => {
dispatch(
@@ -98,6 +107,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
};
const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0;
+
+ const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage;
return (
@@ -117,6 +128,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
{!isLoading ? (
+ {hasScriptError && !showScriptErrorCard && (
+ setShowScriptErrorCard(true)}
+ />
+ )}
@@ -126,9 +143,15 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
) : null}
{isLoading ? : null}
+ {hasScriptError && showScriptErrorCard && (
+ setShowScriptErrorCard(false)}
+ />
+ )}
{getTabPanel(focusedTab.responsePaneTab)}
diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx
index f7c1e4d9c..9e23780e9 100644
--- a/packages/bruno-app/src/components/RunnerResults/index.jsx
+++ b/packages/bruno-app/src/components/RunnerResults/index.jsx
@@ -1,22 +1,18 @@
import React, { useState, useRef, useEffect } from 'react';
-import path from 'path';
+import path from 'utils/common/path';
import { useDispatch } from 'react-redux';
import { get, cloneDeep } from 'lodash';
import { runCollectionFolder, cancelRunnerExecution } from 'providers/ReduxStore/slices/collections/actions';
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
-import slash from 'utils/common/slash';
import ResponsePane from './ResponsePane';
import StyledWrapper from './StyledWrapper';
+import { areItemsLoading } from 'utils/collections';
-const getRelativePath = (fullPath, pathname) => {
- // convert to unix style path
- fullPath = slash(fullPath);
- pathname = slash(pathname);
-
+const getDisplayName = (fullPath, pathname, name = '') => {
let relativePath = path.relative(fullPath, pathname);
- const { dir, name } = path.parse(relativePath);
+ const { dir = '' } = path.parse(relativePath);
return path.join(dir, name);
};
@@ -57,7 +53,7 @@ export default function RunnerResults({ collection }) {
type: info.type,
filename: info.filename,
pathname: info.pathname,
- relativePath: getRelativePath(collection.pathname, info.pathname)
+ displayName: getDisplayName(collection.pathname, info.pathname, info.name)
};
if (newItem.status !== 'error' && newItem.status !== 'skipped') {
if (newItem.testResults) {
@@ -106,6 +102,8 @@ export default function RunnerResults({ collection }) {
return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail';
});
+ let isCollectionLoading = areItemsLoading(collection);
+
if (!items || !items.length) {
return (
@@ -116,7 +114,7 @@ export default function RunnerResults({ collection }) {
You have {totalRequestsInCollection} requests in this collection.
-
+ {isCollectionLoading ? Requests in this collection are still loading.
: null}
Delay (in ms)
- {item.relativePath}
+ {item.displayName}
{item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? (
@@ -263,7 +261,7 @@ export default function RunnerResults({ collection }) {
- {selectedItem.relativePath}
+ {selectedItem.displayName}
{selectedItem.testStatus === 'pass' ? (
@@ -272,7 +270,6 @@ export default function RunnerResults({ collection }) {
)}
- {/*
{selectedItem.relativePath}
*/}
diff --git a/packages/bruno-app/src/components/ShareCollection/StyledWrapper.js b/packages/bruno-app/src/components/ShareCollection/StyledWrapper.js
new file mode 100644
index 000000000..5e1e3be3d
--- /dev/null
+++ b/packages/bruno-app/src/components/ShareCollection/StyledWrapper.js
@@ -0,0 +1,30 @@
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ .tabs {
+ .tab {
+ padding: 6px 0px;
+ border: none;
+ border-bottom: solid 2px transparent;
+ margin-right: 1.25rem;
+ color: var(--color-tab-inactive);
+ cursor: pointer;
+
+ &:focus,
+ &:active,
+ &:focus-within,
+ &:focus-visible,
+ &:target {
+ outline: none !important;
+ box-shadow: none !important;
+ }
+
+ &.active {
+ color: ${(props) => props.theme.tabs.active.color} !important;
+ border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
+ }
+ }
+ }
+`;
+
+export default StyledWrapper;
diff --git a/packages/bruno-app/src/components/ShareCollection/index.js b/packages/bruno-app/src/components/ShareCollection/index.js
new file mode 100644
index 000000000..19f5f00be
--- /dev/null
+++ b/packages/bruno-app/src/components/ShareCollection/index.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import Modal from 'components/Modal';
+import { IconDownload } from '@tabler/icons';
+import StyledWrapper from './StyledWrapper';
+import Bruno from 'components/Bruno';
+import exportBrunoCollection from 'utils/collections/export';
+import exportPostmanCollection from 'utils/exporters/postman-collection';
+import { cloneDeep } from 'lodash';
+import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
+
+const ShareCollection = ({ onClose, collection }) => {
+ const handleExportBrunoCollection = () => {
+ const collectionCopy = cloneDeep(collection);
+ exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
+ onClose();
+ };
+
+ const handleExportPostmanCollection = () => {
+ const collectionCopy = cloneDeep(collection);
+ exportPostmanCollection(collectionCopy);
+ onClose();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
Bruno Collection
+
Export in Bruno format
+
+
+
+
+
+
+
+
+
Postman Collection
+
Export in Postman format
+
+
+
+
+
+ );
+};
+
+export default ShareCollection;
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js
index 41d3e5ff2..30337e3cb 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js
@@ -5,12 +5,16 @@ import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import { cloneCollection } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
-import InfoTip from 'components/InfoTip';
import Modal from 'components/Modal';
+import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
+import PathDisplay from 'components/PathDisplay/index';
+import { useState } from 'react';
+import { IconArrowBackUp } from "@tabler/icons";
const CloneCollection = ({ onClose, collection }) => {
const inputRef = useRef();
const dispatch = useDispatch();
+ const [isEditingFilename, toggleEditingFilename] = useState(false);
const formik = useFormik({
enableReinitialize: true,
@@ -22,12 +26,15 @@ const CloneCollection = ({ onClose, collection }) => {
validationSchema: Yup.object({
collectionName: Yup.string()
.min(1, 'must be at least 1 character')
- .max(50, 'must be 50 characters or less')
+ .max(255, 'must be 255 characters or less')
.required('collection name is required'),
collectionFolderName: Yup.string()
.min(1, 'must be at least 1 character')
- .max(50, 'must be 50 characters or less')
- .matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
+ .max(255, 'must be 255 characters or less')
+ .test('is-valid-dir-name', function(value) {
+ const isValid = validateName(value);
+ return isValid ? true : this.createError({ message: validateNameError(value) });
+ })
.required('folder name is required'),
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
}),
@@ -51,7 +58,7 @@ const CloneCollection = ({ onClose, collection }) => {
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
- // When the user closes the diolog without selecting anything dirPath will be false
+ // When the user closes the dialog without selecting anything dirPath will be false
if (typeof dirPath === 'string') {
formik.setFieldValue('collectionLocation', dirPath);
}
@@ -85,9 +92,7 @@ const CloneCollection = ({ onClose, collection }) => {
className="block textbox mt-2 w-full"
onChange={(e) => {
formik.handleChange(e);
- if (formik.values.collectionName === formik.values.collectionFolderName) {
- formik.setFieldValue('collectionFolderName', e.target.value);
- }
+ !isEditingFilename && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
}}
autoComplete="off"
autoCorrect="off"
@@ -123,26 +128,42 @@ const CloneCollection = ({ onClose, collection }) => {
Browse
-
-
- Folder Name
-
+
+
+
+ Directory Name
+
+ toggleEditingFilename(false)}
+ />
+
+
+
+ >
+ :
+
-
-
+ }
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
{formik.errors.collectionFolderName}
) : null}
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js
index 0bf17603d..428ef7dfc 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js
@@ -1,4 +1,4 @@
-import React, { useRef, useEffect } from 'react';
+import React, { useState, useRef, useEffect } from 'react';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import * as Yup from 'yup';
@@ -6,24 +6,42 @@ import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { cloneItem } from 'providers/ReduxStore/slices/collections/actions';
+import { IconArrowBackUp } from '@tabler/icons';
+import path from "utils/common/path";
+import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
+import PathDisplay from 'components/PathDisplay/index';
const CloneCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
const isFolder = isItemAFolder(item);
const inputRef = useRef();
+ const [isEditingFilename, toggleEditingFilename] = useState(false);
+ const itemName = item?.name;
+ const itemType = item?.type;
+ const itemFilename = item?.filename ? path.parse(item?.filename).name : '';
const formik = useFormik({
enableReinitialize: true,
initialValues: {
- name: item.name
+ name: itemName,
+ filename: sanitizeName(itemFilename)
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be at least 1 character')
- .max(50, 'must be 50 characters or less')
+ .max(255, 'must be 255 characters or less')
+ .required('name is required'),
+ filename: Yup.string()
+ .min(1, 'must be at least 1 character')
+ .max(255, 'must be 255 characters or less')
.required('name is required')
+ .test('is-valid-filename', function(value) {
+ const isValid = validateName(value);
+ return isValid ? true : this.createError({ message: validateNameError(value) });
+ })
+ .test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value))
}),
onSubmit: (values) => {
- dispatch(cloneItem(values.name, item.uid, collection.uid))
+ dispatch(cloneItem(values.name, values.filename, item.uid, collection.uid))
.then(() => {
toast.success('Request cloned!');
onClose();
@@ -44,7 +62,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
return (
{
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
- onChange={formik.handleChange}
+ onChange={e => {
+ formik.setFieldValue('name', e.target.value);
+ !isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value));
+ }}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? {formik.errors.name}
: null}
+ {isEditingFilename ? (
+
+
+
+ {isFolder ? 'Directory' : 'File'} Name
+
+ toggleEditingFilename(false)}
+ />
+
+
+
+ {itemType !== 'folder' && .bru }
+
+
+ ) : (
+
+ )}
+ {formik.touched.filename && formik.errors.filename ? (
+
{formik.errors.filename}
+ ) : null}
);
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/StyledWrapper.js
new file mode 100644
index 000000000..66bfe719b
--- /dev/null
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/StyledWrapper.js
@@ -0,0 +1,12 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.div`
+ .partial {
+ color: ${(props) => props.theme.colors.text.yellow};
+ }
+ .error {
+ color: ${(props) => props.theme.colors.text.danger};
+ }
+`;
+
+export default Wrapper;
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/index.js
new file mode 100644
index 000000000..82d87aa7d
--- /dev/null
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/index.js
@@ -0,0 +1,21 @@
+import RequestMethod from "../RequestMethod";
+import { IconLoader2, IconAlertTriangle, IconAlertCircle } from '@tabler/icons';
+import StyledWrapper from "./StyledWrapper";
+
+const CollectionItemIcon = ({ item }) => {
+ if (item?.error) {
+ return
;
+ }
+
+ if (item?.loading) {
+ return
;
+ }
+
+ if (item?.partial) {
+ return
;
+ }
+
+ return
;
+};
+
+export default CollectionItemIcon;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemInfo/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemInfo/index.js
new file mode 100644
index 000000000..6df7f0634
--- /dev/null
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemInfo/index.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import Modal from 'components/Modal';
+import path from "utils/common/path";
+
+const CollectionItemInfo = ({ collection, item, onClose }) => {
+ const { pathname: collectionPathname } = collection;
+ const { name, filename, pathname, type } = item;
+ const relativePathname = path.relative(collectionPathname, pathname);
+ return (
+
+
+
+
+
+ Name :
+ {name}
+
+
+ {type=='folder' ? 'Directory Name' : 'File Name'} :
+ {filename}
+
+
+ Pathname :
+ {relativePathname}
+
+
+
+
+
+ );
+};
+
+export default CollectionItemInfo;
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js
index 04744b6d8..9d174560d 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js
@@ -1,46 +1,73 @@
-import React, { useRef, useEffect } from 'react';
+import React, { useRef, useEffect, useState } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
+import path from 'utils/common/path';
+import { IconArrowBackUp } from '@tabler/icons';
+import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import toast from 'react-hot-toast';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
+import PathDisplay from 'components/PathDisplay';
const RenameCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
const isFolder = isItemAFolder(item);
const inputRef = useRef();
+ const [isEditingFilename, toggleEditingFilename] = useState(false);
+ const itemName = item?.name;
+ const itemType = item?.type;
+ const itemFilename = item?.filename ? path.parse(item?.filename).name : '';
const formik = useFormik({
enableReinitialize: true,
initialValues: {
- name: item.name
+ name: itemName,
+ filename: sanitizeName(itemFilename)
},
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be at least 1 character')
- .max(50, 'must be 50 characters or less')
+ .max(255, 'must be 255 characters or less')
+ .required('name is required'),
+ filename: Yup.string()
+ .min(1, 'must be at least 1 character')
+ .max(255, 'must be 255 characters or less')
.required('name is required')
+ .test('is-valid-filename', function(value) {
+ const isValid = validateName(value);
+ return isValid ? true : this.createError({ message: validateNameError(value) });
+ })
+ .test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value))
}),
onSubmit: async (values) => {
// if there is unsaved changes in the request,
// save them before renaming the request
+ if ((item.name === values.name) && (itemFilename === values.filename)) {
+ return;
+ }
if (!isFolder && item.draft) {
await dispatch(saveRequest(item.uid, collection.uid, true));
}
- if (item.name === values.name) {
- return;
+ const { name: newName, filename: newFilename } = values;
+ try {
+ let renameConfig = {
+ itemUid: item.uid,
+ collectionUid: collection.uid,
+ };
+ renameConfig['newName'] = newName;
+ if (itemFilename !== newFilename) {
+ renameConfig['newFilename'] = newFilename;
+ }
+ await dispatch(renameItem(renameConfig));
+ if (isFolder) {
+ dispatch(closeTabs({ tabUids: [item.uid] }));
+ }
+ onClose();
+ } catch (error) {
+ toast.error(error.message || 'An error occurred while renaming');
}
- dispatch(renameItem(values.name, item.uid, collection.uid))
- .then(() => {
- isFolder && dispatch(closeTabs({ tabUids: [item.uid] }));
- toast.success(isFolder ? 'Folder renamed' : 'Request renamed');
- onClose();
- })
- .catch((err) => {
- toast.error(err ? err.message : 'An error occurred while renaming the request');
- });
}
});
@@ -54,14 +81,14 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
return (
- e.preventDefault()}>
-
+
{e.preventDefault()}}>
+
{isFolder ? 'Folder' : 'Request'} Name
@@ -75,11 +102,59 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
- onChange={formik.handleChange}
+ onChange={e => {
+ formik.setFieldValue('name', e.target.value);
+ !isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value));
+ }}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ?
{formik.errors.name}
: null}
+
+ {isEditingFilename ? (
+
+
+
+ {isFolder ? 'Directory' : 'File'} Name
+
+ toggleEditingFilename(false)}
+ />
+
+
+
+ {itemType !== 'folder' && .bru }
+
+
+ ) : (
+
+ )}
+ {formik.touched.filename && formik.errors.filename ? (
+ {formik.errors.filename}
+ ) : null}
);
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js
index 3b6e08f42..e7dd94d2f 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js
@@ -4,6 +4,9 @@ const Wrapper = styled.div`
.bruno-modal-content {
padding-bottom: 1rem;
}
+ .warning {
+ color: ${(props) => props.theme.colors.text.danger};
+ }
`;
export default Wrapper;
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js
index 4a81f59af..cfd236f8c 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js
@@ -7,6 +7,7 @@ import { addTab } from 'providers/ReduxStore/slices/tabs';
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
import { flattenItems } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
+import { areItemsLoading } from 'utils/collections';
const RunCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
@@ -32,6 +33,10 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
const flattenedItems = flattenItems(item ? item.items : collection.items);
const recursiveRunLength = getRequestsCount(flattenedItems);
+ const isFolderLoading = areItemsLoading(item);
+ console.log(item);
+ console.log(isFolderLoading);
+
return (
@@ -44,13 +49,12 @@ const RunCollectionItem = ({ collection, item, onClose }) => {
({runLength} requests)
This will only run the requests in this folder.
-
Recursive Run
({recursiveRunLength} requests)
- This will run all the requests in this folder and all its subfolders.
-
+ This will run all the requests in this folder and all its subfolders.
+ {isFolderLoading ? Requests in this folder are still loading.
: null}
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
index ed402825d..41a8a9d32 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js
@@ -5,13 +5,12 @@ import classnames from 'classnames';
import { useDrag, useDrop } from 'react-dnd';
import { IconChevronRight, IconDots } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
-import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
-import { moveItem, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
+import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
+import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
import Dropdown from 'components/Dropdown';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
-import RequestMethod from './RequestMethod';
import RenameCollectionItem from './RenameCollectionItem';
import CloneCollectionItem from './CloneCollectionItem';
import DeleteCollectionItem from './DeleteCollectionItem';
@@ -24,13 +23,17 @@ import { hideHomePage } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import NetworkError from 'components/ResponsePane/NetworkError/index';
-import { uuid } from 'utils/common';
+import CollectionItemInfo from './CollectionItemInfo/index';
+import { findItemInCollection } from 'utils/collections';
+import CollectionItemIcon from './CollectionItemIcon';
+import { scrollToTheActiveTab } from 'utils/tabs';
const CollectionItem = ({ item, collection, searchText }) => {
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const isSidebarDragging = useSelector((state) => state.app.isDragging);
const dispatch = useDispatch();
+ const collectionItemRef = useRef(null);
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
@@ -39,38 +42,35 @@ const CollectionItem = ({ item, collection, searchText }) => {
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
- const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
+ const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false);
+ const hasSearchText = searchText && searchText?.trim()?.length;
+ const itemIsCollapsed = hasSearchText ? false : item.collapsed;
const [{ isDragging }, drag] = useDrag({
- type: `COLLECTION_ITEM_${collection.uid}`,
+ type: `collection-item-${collection.uid}`,
item: item,
collect: (monitor) => ({
isDragging: monitor.isDragging()
- })
+ }),
+ options: {
+ dropEffect: "move"
+ }
});
const [{ isOver }, drop] = useDrop({
- accept: `COLLECTION_ITEM_${collection.uid}`,
+ accept: `collection-item-${collection.uid}`,
drop: (draggedItem) => {
- if (draggedItem.uid !== item.uid) {
- dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
- }
+ dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
},
canDrop: (draggedItem) => {
return draggedItem.uid !== item.uid;
},
collect: (monitor) => ({
- isOver: monitor.isOver()
- })
+ isOver: monitor.isOver(),
+ }),
});
- useEffect(() => {
- if (searchText && searchText.length) {
- setItemisCollapsed(false);
- } else {
- setItemisCollapsed(item.collapsed);
- }
- }, [searchText, item]);
+ drag(drop(collectionItemRef));
const dropdownTippyRef = useRef();
const MenuIcon = forwardRef((props, ref) => {
@@ -90,13 +90,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
'item-hovered': isOver
});
- const scrollToTheActiveTab = () => {
- const activeTab = document.querySelector('.request-tab.active');
- if (activeTab) {
- activeTab.scrollIntoView({ behavior: 'smooth', block: 'start' });
- }
- };
-
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid)).catch((err) =>
toast.custom((t) => toast.dismiss(t.id)} />, {
@@ -106,10 +99,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
};
const handleClick = (event) => {
+ if (event.detail != 1) return;
//scroll to the active tab
setTimeout(scrollToTheActiveTab, 50);
-
- if (isItemARequest(item)) {
+
+ const isRequest = isItemARequest(item);
+
+ if (isRequest) {
dispatch(hideHomePage());
if (itemIsOpenedInTabs(item, tabs)) {
dispatch(
@@ -119,20 +115,21 @@ const CollectionItem = ({ item, collection, searchText }) => {
);
return;
}
+
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
- requestPaneTab: getDefaultRequestPaneTab(item)
+ requestPaneTab: getDefaultRequestPaneTab(item),
+ type: 'request',
})
);
- return;
- }
+ } else {
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
- type: 'folder-settings'
+ type: 'folder-settings',
})
);
dispatch(
@@ -141,9 +138,12 @@ const CollectionItem = ({ item, collection, searchText }) => {
collectionUid: collection.uid
})
);
+ }
};
- const handleFolderCollapse = () => {
+ const handleFolderCollapse = (e) => {
+ e.stopPropagation();
+ e.preventDefault();
dispatch(
collectionFolderClicked({
itemUid: item.uid,
@@ -163,10 +163,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
}
};
- const handleDoubleClick = (event) => {
- setRenameItemModalOpen(true);
- };
-
let indents = range(item.depth);
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const isFolder = isItemAFolder(item);
@@ -187,6 +183,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
}
}
+ const handleDoubleClick = (event) => {
+ dispatch(makeTabPermanent({ uid: item.uid }))
+ };
+
// we need to sort request items by seq property
const sortRequestItems = (items = []) => {
return items.sort((a, b) => a.seq - b.seq);
@@ -227,6 +227,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
}
};
+ const handleShowInFolder = () => {
+ dispatch(showInFolder(item.pathname)).catch((error) => {
+ console.error('Error opening the folder', error);
+ toast.error('Error opening the folder');
+ });
+ };
+
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
@@ -253,7 +260,10 @@ const CollectionItem = ({ item, collection, searchText }) => {
{generateCodeItemModalOpen && (
setGenerateCodeItemModalOpen(false)} />
)}
- drag(drop(node))}>
+ {itemInfoModalOpen && (
+
setItemInfoModalOpen(false)} />
+ )}
+
{indents && indents.length
? indents.map((i) => {
@@ -280,6 +290,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
style={{
paddingLeft: 8
}}
+ onClick={handleClick}
+ onContextMenu={handleRightClick}
+ onDoubleClick={handleDoubleClick}
>
{isFolder ? (
@@ -294,12 +307,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
-
+
{item.name}
@@ -378,6 +388,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
Generate Code
)}
+
{
+ dropdownTippyRef.current.hide();
+ handleShowInFolder();
+ }}
+ >
+ Show in Folder
+
{
@@ -398,6 +417,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
Settings
)}
+
{
+ dropdownTippyRef.current.hide();
+ setItemInfoModalOpen(true);
+ }}
+ >
+ Info
+
@@ -421,4 +449,4 @@ const CollectionItem = ({ item, collection, searchText }) => {
);
};
-export default CollectionItem;
+export default CollectionItem;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js
index b8e0d21fd..5c06cc42a 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js
@@ -12,6 +12,17 @@ const Wrapper = styled.div`
transform: rotateZ(90deg);
}
+ &.item-hovered {
+ background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
+ .collection-actions {
+ .dropdown {
+ div[aria-expanded='false'] {
+ visibility: visible;
+ }
+ }
+ }
+ }
+
.collection-actions {
.dropdown {
div[aria-expanded='true'] {
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
index 3b814a7e5..e81b43a1f 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js
@@ -2,35 +2,37 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react';
import classnames from 'classnames';
import { uuid } from 'utils/common';
import filter from 'lodash/filter';
-import { useDrop } from 'react-dnd';
-import { IconChevronRight, IconDots } from '@tabler/icons';
+import { useDrop, useDrag } from 'react-dnd';
+import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
-import { collectionClicked } from 'providers/ReduxStore/slices/collections';
-import { moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
-import { useDispatch } from 'react-redux';
-import { addTab } from 'providers/ReduxStore/slices/tabs';
+import { collapseCollection } from 'providers/ReduxStore/slices/collections';
+import { mountCollection, moveItemToRootOfCollection, moveCollectionAndPersist } from 'providers/ReduxStore/slices/collections/actions';
+import { useDispatch, useSelector } from 'react-redux';
+import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
import CollectionItem from './CollectionItem';
import RemoveCollection from './RemoveCollection';
-import ExportCollection from './ExportCollection';
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
-import { isItemAFolder, isItemARequest, transformCollectionToSaveToExportAsFile } from 'utils/collections';
-import exportCollection from 'utils/collections/export';
+import { isItemAFolder, isItemARequest } from 'utils/collections';
import RenameCollection from './RenameCollection';
import StyledWrapper from './StyledWrapper';
-import CloneCollection from './CloneCollection/index';
+import CloneCollection from './CloneCollection';
+import { areItemsLoading, findItemInCollection } from 'utils/collections';
+import { scrollToTheActiveTab } from 'utils/tabs';
+import ShareCollection from 'components/ShareCollection/index';
const Collection = ({ collection, searchText }) => {
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false);
- const [showExportCollectionModal, setShowExportCollectionModal] = useState(false);
+ const [showShareCollectionModal, setShowShareCollectionModal] = useState(false);
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
- const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
const dispatch = useDispatch();
+ const isLoading = areItemsLoading(collection);
+ const collectionRef = useRef(null);
const menuDropdownTippyRef = useRef();
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
@@ -52,31 +54,53 @@ const Collection = ({ collection, searchText }) => {
);
};
- useEffect(() => {
- if (searchText && searchText.length) {
- setCollectionIsCollapsed(false);
- } else {
- setCollectionIsCollapsed(collection.collapsed);
+ const ensureCollectionIsMounted = () => {
+ if (collection.mountStatus === 'unmounted') {
+ dispatch(mountCollection({
+ collectionUid: collection.uid,
+ collectionPathname: collection.pathname,
+ brunoConfig: collection.brunoConfig
+ }));
}
- }, [searchText, collection]);
+ }
+
+ const hasSearchText = searchText && searchText?.trim()?.length;
+ const collectionIsCollapsed = hasSearchText ? false : collection.collapsed;
const iconClassName = classnames({
'rotate-90': !collectionIsCollapsed
});
const handleClick = (event) => {
- dispatch(collectionClicked(collection.uid));
+ if (event.detail != 1) return;
+ // Check if the click came from the chevron icon
+ const isChevronClick = event.target.closest('svg')?.classList.contains('chevron-icon');
+ setTimeout(scrollToTheActiveTab, 50);
+
+ ensureCollectionIsMounted();
+
+ dispatch(collapseCollection(collection.uid));
+
+ if(!isChevronClick) {
+ dispatch(
+ addTab({
+ uid: collection.uid,
+ collectionUid: collection.uid,
+ type: 'collection-settings',
+ })
+ );
+ }
};
- const handleCollapseCollection = () => {
- dispatch(collectionClicked(collection.uid));
- dispatch(
- addTab({
- uid: uuid(),
- collectionUid: collection.uid,
- type: 'collection-settings'
- })
- );
+ const handleDoubleClick = (event) => {
+ dispatch(makeTabPermanent({ uid: collection.uid }))
+ };
+
+ const handleCollectionCollapse = (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ ensureCollectionIsMounted();
+ dispatch(collapseCollection(collection.uid));
}
const handleRightClick = (event) => {
@@ -93,33 +117,58 @@ const Collection = ({ collection, searchText }) => {
const viewCollectionSettings = () => {
dispatch(
addTab({
- uid: uuid(),
+ uid: collection.uid,
collectionUid: collection.uid,
type: 'collection-settings'
})
);
};
+ const isCollectionItem = (itemType) => {
+ return itemType.startsWith('collection-item');
+ };
+
+ const [{ isDragging }, drag] = useDrag({
+ type: "collection",
+ item: collection,
+ collect: (monitor) => ({
+ isDragging: monitor.isDragging(),
+ }),
+ options: {
+ dropEffect: "move"
+ }
+ });
+
const [{ isOver }, drop] = useDrop({
- accept: `COLLECTION_ITEM_${collection.uid}`,
- drop: (draggedItem) => {
- dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid));
+ accept: ["collection", `collection-item-${collection.uid}`],
+ drop: (draggedItem, monitor) => {
+ const itemType = monitor.getItemType();
+ if (isCollectionItem(itemType)) {
+ dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid))
+ } else {
+ dispatch(moveCollectionAndPersist({draggedItem, targetItem: collection}));
+ }
},
canDrop: (draggedItem) => {
- // todo need to make sure that draggedItem belongs to the collection
- return true;
+ return draggedItem.uid !== collection.uid;
},
collect: (monitor) => ({
- isOver: monitor.isOver()
- })
+ isOver: monitor.isOver(),
+ }),
});
+ drag(drop(collectionRef));
+
if (searchText && searchText.length) {
if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) {
return null;
}
}
+ const collectionRowClassName = classnames('flex py-1 collection-name items-center', {
+ 'item-hovered': isOver
+ });
+
// we need to sort request items by seq property
const sortRequestItems = (items = []) => {
return items.sort((a, b) => a.seq - b.seq);
@@ -143,28 +192,32 @@ const Collection = ({ collection, searchText }) => {
{showRemoveCollectionModal && (
setShowRemoveCollectionModal(false)} />
)}
- {showExportCollectionModal && (
- setShowExportCollectionModal(false)} />
+ {showShareCollectionModal && (
+ setShowShareCollectionModal(false)} />
)}
{showCloneCollectionModalOpen && (
setShowCloneCollectionModalOpen(false)} />
)}
-
+
-
} placement="bottom-start">
@@ -217,10 +270,10 @@ const Collection = ({ collection, searchText }) => {
className="dropdown-item"
onClick={(e) => {
menuDropdownTippyRef.current.hide();
- setShowExportCollectionModal(true);
+ setShowShareCollectionModal(true);
}}
>
- Export
+ Share
{
{
{collections && collections.length
? collections.map((c) => {
return (
-
-
-
+
);
})
: null}
diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
index 6f05207d2..a1b48843f 100644
--- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
+++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js
@@ -5,12 +5,17 @@ import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
-import InfoTip from 'components/InfoTip';
import Modal from 'components/Modal';
+import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
+import PathDisplay from 'components/PathDisplay/index';
+import { useState } from 'react';
+import { IconArrowBackUp } from '@tabler/icons';
+import Help from 'components/Help';
const CreateCollection = ({ onClose }) => {
const inputRef = useRef();
const dispatch = useDispatch();
+ const [isEditingFilename, toggleEditingFilename] = useState(false);
const formik = useFormik({
enableReinitialize: true,
@@ -22,12 +27,15 @@ const CreateCollection = ({ onClose }) => {
validationSchema: Yup.object({
collectionName: Yup.string()
.min(1, 'must be at least 1 character')
- .max(50, 'must be 50 characters or less')
+ .max(255, 'must be 255 characters or less')
.required('collection name is required'),
collectionFolderName: Yup.string()
.min(1, 'must be at least 1 character')
- .max(50, 'must be 50 characters or less')
- .matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
+ .max(255, 'must be 255 characters or less')
+ .test('is-valid-dir-name', function(value) {
+ const isValid = validateName(value);
+ return isValid ? true : this.createError({ message: validateNameError(value) });
+ })
.required('folder name is required'),
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
}),
@@ -44,7 +52,7 @@ const CreateCollection = ({ onClose }) => {
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
- // When the user closes the diolog without selecting anything dirPath will be false
+ // When the user closes the dialog without selecting anything dirPath will be false
if (typeof dirPath === 'string') {
formik.setFieldValue('collectionLocation', dirPath);
}
@@ -78,9 +86,7 @@ const CreateCollection = ({ onClose }) => {
className="block textbox mt-2 w-full"
onChange={(e) => {
formik.handleChange(e);
- if (formik.values.collectionName === formik.values.collectionFolderName) {
- formik.setFieldValue('collectionFolderName', e.target.value);
- }
+ !isEditingFilename && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));
}}
autoComplete="off"
autoCorrect="off"
@@ -92,8 +98,16 @@ const CreateCollection = ({ onClose }) => {
{formik.errors.collectionName}
) : null}
-
+
Location
+
+
+ Bruno stores your collections on your computer's filesystem.
+
+
+ Choose where you want to store this collection.
+
+
{
Browse
-
-
- Folder Name
-
+
+
+
+ Directory Name
+
+ toggleEditingFilename(false)}
+ />
+
+
+
+ >
+ :
+
-
-
+ }
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
{formik.errors.collectionFolderName}
) : null}
diff --git a/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js b/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js
index ac6acee68..d238fd206 100644
--- a/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js
+++ b/packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js
@@ -85,7 +85,7 @@ const GoldenEdition = ({ onClose }) => {
});
};
- const goldenEditonIndividuals = [
+ const goldenEditionIndividuals = [
'Inbuilt Bru File Explorer',
'Visual Git (Like Gitlens for Vscode)',
'GRPC, Websocket, SocketIO, MQTT',
@@ -97,7 +97,7 @@ const GoldenEdition = ({ onClose }) => {
'Custom Themes'
];
- const goldenEditonOrganizations = [
+ const goldenEditionOrganizations = [
'Centralized License Management',
'Integration with Secret Managers',
'Private Collection Registry',
@@ -179,7 +179,7 @@ const GoldenEdition = ({ onClose }) => {
{pricingOption === 'individuals' ? (
<>
- {goldenEditonIndividuals.map((item, index) => (
+ {goldenEditionIndividuals.map((item, index) => (
{item}
@@ -192,7 +192,7 @@ const GoldenEdition = ({ onClose }) => {
Everything in the Individual Plan
- {goldenEditonOrganizations.map((item, index) => (
+ {goldenEditionOrganizations.map((item, index) => (
{item}
diff --git a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
index ada38a1cb..3f6c4ccf4 100644
--- a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
+++ b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js
@@ -1,40 +1,52 @@
-import React, { useRef, useEffect } from 'react';
+import React, { useRef, useEffect, useState } from 'react';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
import * as Yup from 'yup';
import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { newFolder } from 'providers/ReduxStore/slices/collections/actions';
+import { IconArrowBackUp } from '@tabler/icons';
+import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
+import PathDisplay from 'components/PathDisplay';
const NewFolder = ({ collection, item, onClose }) => {
const dispatch = useDispatch();
const inputRef = useRef();
+ const [isEditingFilename, toggleEditingFilename] = useState(false);
+
const formik = useFormik({
enableReinitialize: true,
initialValues: {
- folderName: ''
+ folderName: '',
+ directoryName: ''
},
validationSchema: Yup.object({
folderName: Yup.string()
.trim()
.min(1, 'must be at least 1 character')
- .required('name is required')
+ .required('name is required'),
+ directoryName: Yup.string()
+ .trim()
+ .min(1, 'must be at least 1 character')
+ .required('foldername is required')
+ .test('is-valid-folder-name', function(value) {
+ const isValid = validateName(value);
+ return isValid ? true : this.createError({ message: validateNameError(value) });
+ })
.test({
name: 'folderName',
message: 'The folder name "environments" at the root of the collection is reserved in bruno',
test: (value) => {
- if (item && item.uid) {
- return true;
- }
+ if (item?.uid) return true;
return value && !value.trim().toLowerCase().includes('environments');
}
})
}),
onSubmit: (values) => {
- dispatch(newFolder(values.folderName, collection.uid, item ? item.uid : null))
+ dispatch(newFolder(values.folderName, values.directoryName, collection.uid, item ? item.uid : null))
.then(() => {
toast.success('New folder created!');
- onClose()
+ onClose();
})
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the folder'));
}
@@ -49,8 +61,8 @@ const NewFolder = ({ collection, item, onClose }) => {
const onSubmit = () => formik.handleSubmit();
return (
-
- e.preventDefault()}>
+
+
Folder Name
@@ -65,13 +77,59 @@ const NewFolder = ({ collection, item, onClose }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
- onChange={formik.handleChange}
+ onChange={e => {
+ formik.setFieldValue('folderName', e.target.value);
+ !isEditingFilename && formik.setFieldValue('directoryName', sanitizeName(e.target.value));
+ }}
value={formik.values.folderName || ''}
/>
{formik.touched.folderName && formik.errors.folderName ? (
{formik.errors.folderName}
) : null}
+
+ {isEditingFilename ? (
+
+
+
+ Directory Name
+
+ toggleEditingFilename(false)}
+ />
+
+
+
+
+
+ ) : (
+
+ )}
+ {formik.touched.directoryName && formik.errors.directoryName ? (
+ {formik.errors.directoryName}
+ ) : null}
);
diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js
index f7d7e914d..872ba2877 100644
--- a/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js
+++ b/packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js
@@ -1,52 +1,45 @@
import styled from 'styled-components';
-const StyledWrapper = styled.div`
- div.method-selector-container {
- border: solid 1px ${(props) => props.theme.modal.input.border};
- border-right: none;
- background-color: ${(props) => props.theme.modal.input.bg};
- border-top-left-radius: 3px;
- border-bottom-left-radius: 3px;
+ const StyledWrapper = styled.div`
+ div.method-selector-container {
+ border: solid 1px ${(props) => props.theme.modal.input.border};
+ border-right: none;
+ background-color: ${(props) => props.theme.modal.input.bg};
+ border-top-left-radius: 3px;
+ border-bottom-left-radius: 3px;
+ .method-selector {
+ min-width: 80px;
+ }
+ }
+ div.method-selector-container,
+ div.input-container {
+ background-color: ${(props) => props.theme.modal.input.bg};
+ height: 2.3rem;
+ }
+ div.input-container {
+ border: solid 1px ${(props) => props.theme.modal.input.border};
+ border-top-right-radius: 3px;
+ border-bottom-right-radius: 3px;
+ input {
+ background-color: ${(props) => props.theme.modal.input.bg};
+ outline: none;
+ box-shadow: none;
+ &:focus {
+ outline: none !important;
+ box-shadow: none !important;
+ }
+ }
+ }
+ textarea.curl-command {
+ min-height: 150px;
+ }
+ .dropdown {
+ width: fit-content;
+
+ .dropdown-item {
+ padding: 0.2rem 0.6rem !important;
+ }
+ }
+ `;
- .method-selector {
- min-width: 80px;
- }
- }
-
- div.method-selector-container,
- div.input-container {
- background-color: ${(props) => props.theme.modal.input.bg};
- height: 2.3rem;
- }
-
- div.input-container {
- border: solid 1px ${(props) => props.theme.modal.input.border};
- border-top-right-radius: 3px;
- border-bottom-right-radius: 3px;
-
- input {
- background-color: ${(props) => props.theme.modal.input.bg};
- outline: none;
- box-shadow: none;
-
- &:focus {
- outline: none !important;
- box-shadow: none !important;
- }
- }
- }
-
- textarea.curl-command {
- min-height: 150px;
- }
-
- .dropdown {
- width: fit-content;
-
- .dropdown-item {
- padding: 0.2rem 0.6rem !important;
- }
- }
-`;
-
-export default StyledWrapper;
+ export default StyledWrapper;
\ No newline at end of file
diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
index f95b3efcc..2f201f469 100644
--- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
+++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js
@@ -10,10 +10,12 @@ import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions'
import { addTab } from 'providers/ReduxStore/slices/tabs';
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
import { getDefaultRequestPaneTab } from 'utils/collections';
-import StyledWrapper from './StyledWrapper';
import { getRequestFromCurlCommand } from 'utils/curl';
+import { IconArrowBackUp, IconCaretDown } from '@tabler/icons';
+import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import Dropdown from 'components/Dropdown';
-import { IconCaretDown } from '@tabler/icons';
+import PathDisplay from 'components/PathDisplay';
+import StyledWrapper from './StyledWrapper';
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const dispatch = useDispatch();
@@ -55,6 +57,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
setCurlRequestTypeDetected(type);
};
+ const [isEditingFilename, toggleEditingFilename] = useState(false);
+
const getRequestType = (collectionPresets) => {
if (!collectionPresets || !collectionPresets.requestType) {
return 'http-request';
@@ -79,6 +83,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
enableReinitialize: true,
initialValues: {
requestName: '',
+ filename: '',
requestType: getRequestType(collectionPresets),
requestUrl: collectionPresets.requestUrl || '',
requestMethod: 'GET',
@@ -88,15 +93,18 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
requestName: Yup.string()
.trim()
.min(1, 'must be at least 1 character')
- .required('name is required')
- .test({
- name: 'requestName',
- message: `The request names - collection and folder is reserved in bruno`,
- test: (value) => {
- const trimmedValue = value ? value.trim().toLowerCase() : '';
- return !['collection', 'folder'].includes(trimmedValue);
- }
- }),
+ .max(255, 'must be 255 characters or less')
+ .required('name is required'),
+ filename: Yup.string()
+ .trim()
+ .min(1, 'must be at least 1 character')
+ .max(255, 'must be 255 characters or less')
+ .required('filename is required')
+ .test('is-valid-filename', function(value) {
+ const isValid = validateName(value);
+ return isValid ? true : this.createError({ message: validateNameError(value) });
+ })
+ .test('not-reserved', `The file names "collection" and "folder" are reserved in bruno`, value => !['collection', 'folder'].includes(value)),
curlCommand: Yup.string().when('requestType', {
is: (requestType) => requestType === 'from-curl',
then: Yup.string()
@@ -116,6 +124,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
newEphemeralHttpRequest({
uid: uid,
requestName: values.requestName,
+ filename: values.filename,
requestType: values.requestType,
requestUrl: values.requestUrl,
requestMethod: values.requestMethod,
@@ -138,6 +147,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
dispatch(
newHttpRequest({
requestName: values.requestName,
+ filename: values.filename,
requestType: curlRequestTypeDetected,
requestUrl: request.url,
requestMethod: request.method,
@@ -157,6 +167,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
dispatch(
newHttpRequest({
requestName: values.requestName,
+ filename: values.filename,
requestType: values.requestType,
requestUrl: values.requestUrl,
requestMethod: values.requestMethod,
@@ -221,7 +232,16 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
return (
- e.preventDefault()}>
+ {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ formik.handleSubmit();
+ }
+ }}
+ >
Type
@@ -287,20 +307,64 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
- onChange={formik.handleChange}
+ onChange={e => {
+ formik.setFieldValue('requestName', e.target.value);
+ !isEditingFilename && formik.setFieldValue('filename', sanitizeName(e.target.value));
+ }}
value={formik.values.requestName || ''}
/>
{formik.touched.requestName && formik.errors.requestName ? (
{formik.errors.requestName}
) : null}
+ {isEditingFilename ? (
+
+
+
+ File Name
+
+ toggleEditingFilename(false)}
+ />
+
+
+
+ .bru
+
+
+ ) : (
+
+ )}
+ {formik.touched.filename && formik.errors.filename ? (
+ {formik.errors.filename}
+ ) : null}
{formik.values.requestType !== 'from-curl' ? (
<>
URL
-
{
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
const preferencesOpen = useSelector((state) => state.app.showPreferences);
const [goldenEditionOpen, setGoldenEditionOpen] = useState(false);
-
+ const { version } = useApp();
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
const [cookiesOpen, setCookiesOpen] = useState(false);
@@ -184,7 +185,7 @@ const Sidebar = () => {
Star
*/}
-
v1.36.0
+
v{version}
diff --git a/packages/bruno-app/src/components/ToggleSwitch/StyledWrapper.js b/packages/bruno-app/src/components/ToggleSwitch/StyledWrapper.js
new file mode 100644
index 000000000..d4216860a
--- /dev/null
+++ b/packages/bruno-app/src/components/ToggleSwitch/StyledWrapper.js
@@ -0,0 +1,91 @@
+import styled from 'styled-components';
+
+const switchSizes = {
+ '2xs': { width: 32, height: 16, buttonSize: 14 },
+ xs: { width: 40, height: 20, buttonSize: 18 },
+ s: { width: 44, height: 22, buttonSize: 20 },
+ m: { width: 50, height: 24, buttonSize: 22 }, // default size
+ l: { width: 56, height: 28, buttonSize: 26 },
+ xl: { width: 64, height: 32, buttonSize: 30 },
+ '2xl': { width: 72, height: 36, buttonSize: 34 }
+};
+
+const getSizeValues = (size = 'm') => switchSizes[size] || switchSizes.m;
+
+export const Switch = styled.div`
+ position: relative;
+ display: inline-block;
+ width: ${(props) => getSizeValues(props.size).width}px;
+ height: ${(props) => getSizeValues(props.size).height}px;
+ border-radius: ${(props) => getSizeValues(props.size).height}px;
+`;
+
+export const Checkbox = styled.input`
+ opacity: 0;
+ width: 0;
+ height: 0;
+
+ &:checked + label div {
+ background-color: ${(props) => props.theme.textLink};
+ }
+
+ &:checked + label div:before {
+ transform: translateX(${(props) => getSizeValues(props.size).width - getSizeValues(props.size).buttonSize - 2}px);
+ }
+`;
+
+export const Label = styled.label`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
+ background-color: ${(props) => props.theme.input.bg};
+ border-radius: 24px;
+
+ div {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: ${(props) => props.theme.colors.text.muted};
+ border-radius: 24px;
+ transition: transform 0.2s;
+ }
+`;
+
+export const Inner = styled.div`
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ right: 2px;
+ bottom: 2px;
+ background-color: #fafafa;
+ transition: 0.4s;
+ border-radius: ${(props) => getSizeValues(props.size).height - 2}px;
+`;
+
+export const SwitchButton = styled.div`
+ position: absolute;
+ height: ${(props) => getSizeValues(props.size).buttonSize}px;
+ width: ${(props) => getSizeValues(props.size).buttonSize}px;
+ left: 2px;
+ bottom: 2px;
+ background-color: white;
+ transition: 0.4s;
+ border-radius: 50%;
+
+ &:before {
+ content: '';
+ position: absolute;
+ height: ${(props) => getSizeValues(props.size).buttonSize - 2}px;
+ width: ${(props) => getSizeValues(props.size).buttonSize - 2}px;
+ background-color: white;
+ top: 2px;
+ left: 2px;
+ transition: 0.4s;
+ border-radius: 50%;
+ }
+`;
diff --git a/packages/bruno-app/src/components/ToggleSwitch/index.js b/packages/bruno-app/src/components/ToggleSwitch/index.js
new file mode 100644
index 000000000..cf386a347
--- /dev/null
+++ b/packages/bruno-app/src/components/ToggleSwitch/index.js
@@ -0,0 +1,15 @@
+import { Checkbox, Inner, Label, Switch, SwitchButton } from './StyledWrapper';
+
+const ToggleSwitch = ({ isOn, handleToggle, size = 'm', ...props }) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default ToggleSwitch;
diff --git a/packages/bruno-app/src/components/ToolHint/index.js b/packages/bruno-app/src/components/ToolHint/index.js
index b8799dd69..3d559625e 100644
--- a/packages/bruno-app/src/components/ToolHint/index.js
+++ b/packages/bruno-app/src/components/ToolHint/index.js
@@ -34,7 +34,7 @@ const ToolHint = ({
{
useEffect(
() => {
const listener = (event) => {
- // Do nothing if clicking ref's element or descendent elements
+ // Do nothing if clicking ref's element or descendant elements
if (!ref.current || ref.current.contains(event.target)) {
return;
}
diff --git a/packages/bruno-app/src/index.js b/packages/bruno-app/src/index.js
index 0e5187ebe..36b1d0bc6 100644
--- a/packages/bruno-app/src/index.js
+++ b/packages/bruno-app/src/index.js
@@ -1,6 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './pages/index';
+import { DndProvider } from 'react-dnd';
+import { HTML5Backend } from 'react-dnd-html5-backend';
const rootElement = document.getElementById('root');
@@ -8,7 +10,9 @@ if (rootElement) {
const root = ReactDOM.createRoot(rootElement);
root.render(
-
+
+
+
);
}
diff --git a/packages/bruno-app/src/providers/App/index.js b/packages/bruno-app/src/providers/App/index.js
index 7664ae03e..b06d1d3a8 100644
--- a/packages/bruno-app/src/providers/App/index.js
+++ b/packages/bruno-app/src/providers/App/index.js
@@ -6,11 +6,12 @@ import ConfirmAppClose from './ConfirmAppClose';
import useIpcEvents from './useIpcEvents';
import useTelemetry from './useTelemetry';
import StyledWrapper from './StyledWrapper';
+import { version } from '../../../package.json';
export const AppContext = React.createContext();
export const AppProvider = (props) => {
- useTelemetry();
+ useTelemetry({ version });
useIpcEvents();
const dispatch = useDispatch();
@@ -37,7 +38,7 @@ export const AppProvider = (props) => {
}, []);
return (
-
+
{props.children}
@@ -46,4 +47,12 @@ export const AppProvider = (props) => {
);
};
+export const useApp = () => {
+ const context = React.useContext(AppContext);
+ if (!context) {
+ throw new Error('useApp must be used within an AppProvider');
+ }
+ return context;
+};
+
export default AppProvider;
diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js
index 6b64e1279..712a6efb7 100644
--- a/packages/bruno-app/src/providers/App/useTelemetry.js
+++ b/packages/bruno-app/src/providers/App/useTelemetry.js
@@ -42,7 +42,7 @@ const getAnonymousTrackingId = () => {
return id;
};
-const trackStart = () => {
+const trackStart = (version) => {
if (isPlaywrightTestRunning()) {
return;
}
@@ -58,16 +58,18 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
- version: '1.38.1'
+ version: version
}
});
};
-const useTelemetry = () => {
+const useTelemetry = ({ version }) => {
useEffect(() => {
- trackStart();
- setInterval(trackStart, 24 * 60 * 60 * 1000);
- }, []);
+ if (posthogApiKey && posthogApiKey.length) {
+ trackStart(version);
+ setInterval(trackStart, 24 * 60 * 60 * 1000);
+ }
+ }, [posthogApiKey]);
};
export default useTelemetry;
diff --git a/packages/bruno-app/src/providers/ReduxStore/index.js b/packages/bruno-app/src/providers/ReduxStore/index.js
index fbfe473e8..e02886582 100644
--- a/packages/bruno-app/src/providers/ReduxStore/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/index.js
@@ -6,12 +6,13 @@ import collectionsReducer from './slices/collections';
import tabsReducer from './slices/tabs';
import notificationsReducer from './slices/notifications';
import globalEnvironmentsReducer from './slices/global-environments';
+import { draftDetectMiddleware } from './middlewares/draft/middleware';
const isDevEnv = () => {
return import.meta.env.MODE === 'development';
};
-let middleware = [tasksMiddleware.middleware];
+let middleware = [tasksMiddleware.middleware, draftDetectMiddleware];
if (isDevEnv()) {
middleware = [...middleware, debugMiddleware.middleware];
}
diff --git a/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/middleware.js b/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/middleware.js
new file mode 100644
index 000000000..4b8f39443
--- /dev/null
+++ b/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/middleware.js
@@ -0,0 +1,56 @@
+import { handleMakeTabParmanent } from "./utils";
+
+const actionsToIntercept = [
+ 'collections/requestUrlChanged',
+ 'collections/updateAuth',
+ 'collections/addQueryParam',
+ 'collections/moveQueryParam',
+ 'collections/updateQueryParam',
+ 'collections/deleteQueryParam',
+ 'collections/updatePathParam',
+ 'collections/addRequestHeader',
+ 'collections/updateRequestHeader',
+ 'collections/deleteRequestHeader',
+ 'collections/moveRequestHeader',
+ 'collections/addFormUrlEncodedParam',
+ 'collections/updateFormUrlEncodedParam',
+ 'collections/deleteFormUrlEncodedParam',
+ 'collections/moveFormUrlEncodedParam',
+ 'collections/addMultipartFormParam',
+ 'collections/updateMultipartFormParam',
+ 'collections/deleteMultipartFormParam',
+ 'collections/moveMultipartFormParam',
+ 'collections/updateRequestAuthMode',
+ 'collections/updateRequestBodyMode',
+ 'collections/updateRequestBody',
+ 'collections/updateRequestGraphqlQuery',
+ 'collections/updateRequestGraphqlVariables',
+ 'collections/updateRequestScript',
+ 'collections/updateResponseScript',
+ 'collections/updateRequestTests',
+ 'collections/updateRequestMethod',
+ 'collections/addAssertion',
+ 'collections/updateAssertion',
+ 'collections/deleteAssertion',
+ 'collections/moveAssertion',
+ 'collections/addVar',
+ 'collections/updateVar',
+ 'collections/deleteVar',
+ 'collections/moveVar',
+ 'collections/addFolderHeader',
+ 'collections/updateFolderHeader',
+ 'collections/deleteFolderHeader',
+ 'collections/addFolderVar',
+ 'collections/updateFolderVar',
+ 'collections/deleteFolderVar',
+ 'collections/updateRequestDocs',
+ 'collections/runRequestEvent', // TODO: This doesn't necessarily related to a draft state, need to rethink.
+];
+
+export const draftDetectMiddleware = ({ dispatch, getState }) => (next) => (action) => {
+ if (actionsToIntercept.includes(action.type)) {
+ const state = getState();
+ handleMakeTabParmanent(state, action, dispatch);
+ }
+ return next(action);
+};
diff --git a/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/utils.js b/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/utils.js
new file mode 100644
index 000000000..ab84ccaf4
--- /dev/null
+++ b/packages/bruno-app/src/providers/ReduxStore/middlewares/draft/utils.js
@@ -0,0 +1,21 @@
+import { makeTabPermanent } from "providers/ReduxStore/slices/tabs";
+import { findCollectionByUid, findItemInCollection } from "utils/collections/index";
+import find from 'lodash/find';
+
+function handleMakeTabParmanent(state, action, dispatch) {
+ const tabs = state.tabs.tabs;
+ const activeTabUid = state.tabs.activeTabUid;
+ const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
+ const itemUid = action.payload.itemUid || action.payload.folderUid
+ const collection = findCollectionByUid(state.collections.collections, action.payload.collectionUid);
+ if (collection) {
+ const item = findItemInCollection(collection, itemUid);
+ if (item && focusedTab.preview == true) {
+ dispatch(makeTabPermanent({ uid: itemUid }));
+ }
+ }
+}
+
+export {
+ handleMakeTabParmanent
+}
\ No newline at end of file
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js
index f5034b5d5..f19c51101 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js
@@ -122,6 +122,44 @@ export const deleteCookiesForDomain = (domain) => (dispatch, getState) => {
});
};
+export const deleteCookie = (domain, path, cookieKey) => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ const { ipcRenderer } = window;
+
+ ipcRenderer.invoke('renderer:delete-cookie', domain, path, cookieKey).then(resolve).catch(reject);
+ });
+};
+
+export const addCookie = (domain, cookie) => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ const { ipcRenderer } = window;
+
+ ipcRenderer.invoke('renderer:add-cookie', domain, cookie).then(resolve).catch(reject);
+ });
+};
+
+export const modifyCookie = (domain, oldCookie, cookie) => (dispatch, getState) => {
+ return new Promise((resolve, reject) => {
+ const { ipcRenderer } = window;
+
+ ipcRenderer.invoke('renderer:modify-cookie', domain, oldCookie, cookie).then(resolve).catch(reject);
+ });
+};
+
+export const getParsedCookie = (cookieStr) => () => {
+ return new Promise((resolve, reject) => {
+ const { ipcRenderer } = window;
+ ipcRenderer.invoke('renderer:get-parsed-cookie', cookieStr).then(resolve).catch(reject);
+ });
+};
+
+export const createCookieString = (cookieObj) => () => {
+ return new Promise((resolve, reject) => {
+ const { ipcRenderer } = window;
+ ipcRenderer.invoke('renderer:create-cookie-string', cookieObj).then(resolve).catch(reject);
+ });
+};
+
export const completeQuitFlow = () => (dispatch, getState) => {
const { ipcRenderer } = window;
return ipcRenderer.invoke('main:complete-quit-flow');
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index d11da5fc8..43c80c49e 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -3,8 +3,9 @@ import cloneDeep from 'lodash/cloneDeep';
import filter from 'lodash/filter';
import find from 'lodash/find';
import get from 'lodash/get';
+import set from 'lodash/set';
import trim from 'lodash/trim';
-import path from 'path';
+import path from 'utils/common/path';
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast';
import {
@@ -21,8 +22,8 @@ import {
transformRequestToSaveToFilesystem
} from 'utils/collections';
import { uuid, waitForNextTick } from 'utils/common';
-import { PATH_SEPARATOR, getDirectoryName } from 'utils/common/platform';
import { cancelNetworkRequest, sendNetworkRequest } from 'utils/network';
+import { callIpc } from 'utils/common/ipc';
import {
collectionAddEnvFileEvent as _collectionAddEnvFileEvent,
@@ -30,6 +31,8 @@ import {
removeCollection as _removeCollection,
selectEnvironment as _selectEnvironment,
sortCollections as _sortCollections,
+ updateCollectionMountStatus,
+ moveCollection,
requestCancelled,
resetRunResults,
responseReceived,
@@ -44,9 +47,9 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
-import slash from 'utils/common/slash';
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index';
+import { sanitizeName } from 'utils/common/regex';
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
const state = getState();
@@ -162,7 +165,6 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState)
if (!folder) {
return reject(new Error('Folder not found'));
}
- console.log(collection);
const { ipcRenderer } = window;
@@ -171,7 +173,6 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState)
pathname: folder.pathname,
root: folder.root
};
- console.log(folderData);
ipcRenderer
.invoke('renderer:save-folder-root', folderData)
@@ -353,7 +354,7 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay)
});
};
-export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getState) => {
+export const newFolder = (folderName, directoryName, collectionUid, itemUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -365,14 +366,14 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
if (!itemUid) {
const folderWithSameNameExists = find(
collection.items,
- (i) => i.type === 'folder' && trim(i.name) === trim(folderName)
+ (i) => i.type === 'folder' && trim(i.filename) === trim(directoryName)
);
if (!folderWithSameNameExists) {
- const fullName = `${collection.pathname}${PATH_SEPARATOR}${folderName}`;
+ const fullName = path.join(collection.pathname, directoryName);
const { ipcRenderer } = window;
ipcRenderer
- .invoke('renderer:new-folder', fullName)
+ .invoke('renderer:new-folder', fullName, folderName)
.then(() => resolve())
.catch((error) => reject(error));
} else {
@@ -383,14 +384,14 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
if (currentItem) {
const folderWithSameNameExists = find(
currentItem.items,
- (i) => i.type === 'folder' && trim(i.name) === trim(folderName)
+ (i) => i.type === 'folder' && trim(i.filename) === trim(directoryName)
);
if (!folderWithSameNameExists) {
- const fullName = `${currentItem.pathname}${PATH_SEPARATOR}${folderName}`;
+ const fullName = path.join(currentItem.pathname, directoryName);
const { ipcRenderer } = window;
ipcRenderer
- .invoke('renderer:new-folder', fullName)
+ .invoke('renderer:new-folder', fullName, folderName)
.then(() => resolve())
.catch((error) => reject(error));
} else {
@@ -403,8 +404,7 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
});
};
-// rename item
-export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getState) => {
+export const renameItem = ({ newName, newFilename, itemUid, collectionUid }) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -419,22 +419,53 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
return reject(new Error('Unable to locate item'));
}
- const dirname = getDirectoryName(item.pathname);
-
- let newPathname = '';
- if (item.type === 'folder') {
- newPathname = path.join(dirname, trim(newName));
- } else {
- const filename = resolveRequestFilename(newName);
- newPathname = path.join(dirname, filename);
- }
const { ipcRenderer } = window;
- ipcRenderer.invoke('renderer:rename-item', slash(item.pathname), newPathname, newName).then(resolve).catch(reject);
+ const renameName = async () => {
+ return ipcRenderer.invoke('renderer:rename-item-name', { itemPath: item.pathname, newName })
+ .catch((err) => {
+ toast.error('Failed to rename the item name');
+ console.error(err);
+ throw new Error('Failed to rename the item name');
+ });
+ };
+
+ const renameFile = async () => {
+ const dirname = path.dirname(item.pathname);
+ let newPath = '';
+ if (item.type === 'folder') {
+ newPath = path.join(dirname, trim(newFilename));
+ } else {
+ const filename = resolveRequestFilename(newFilename);
+ newPath = path.join(dirname, filename);
+ }
+
+ return ipcRenderer.invoke('renderer:rename-item-filename', { oldPath: item.pathname, newPath, newName, newFilename })
+ .catch((err) => {
+ toast.error('Failed to rename the file');
+ console.error(err);
+ throw new Error('Failed to rename the file');
+ });
+ };
+
+ let renameOperation = null;
+ if (newName) renameOperation = renameName;
+ if (newFilename) renameOperation = renameFile;
+
+ if (!renameOperation) {
+ resolve();
+ }
+
+ renameOperation()
+ .then(() => {
+ toast.success('Item renamed successfully');
+ resolve();
+ })
+ .catch((err) => reject(err));
});
};
-export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getState) => {
+export const cloneItem = (newName, newFilename, itemUid, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -453,36 +484,41 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
const folderWithSameNameExists = find(
parentFolder.items,
- (i) => i.type === 'folder' && trim(i.name) === trim(newName)
+ (i) => i.type === 'folder' && trim(i?.filename) === trim(newFilename)
);
if (folderWithSameNameExists) {
return reject(new Error('Duplicate folder names under same parent folder are not allowed'));
}
- const collectionPath = `${parentFolder.pathname}${PATH_SEPARATOR}${newName}`;
+ set(item, 'name', newName);
+ set(item, 'filename', newFilename);
+ set(item, 'root.meta.name', newName);
+
+ const collectionPath = path.join(parentFolder.pathname, newFilename);
ipcRenderer.invoke('renderer:clone-folder', item, collectionPath).then(resolve).catch(reject);
return;
}
const parentItem = findParentItemInCollection(collectionCopy, itemUid);
- const filename = resolveRequestFilename(newName);
+ const filename = resolveRequestFilename(newFilename);
const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(item));
- itemToSave.name = trim(newName);
+ set(itemToSave, 'name', trim(newName));
+ set(itemToSave, 'filename', trim(filename));
if (!parentItem) {
const reqWithSameNameExists = find(
collection.items,
(i) => i.type !== 'folder' && trim(i.filename) === trim(filename)
);
if (!reqWithSameNameExists) {
- const fullName = `${collection.pathname}${PATH_SEPARATOR}${filename}`;
+ const fullPathname = path.join(collection.pathname, filename);
const { ipcRenderer } = window;
const requestItems = filter(collection.items, (i) => i.type !== 'folder');
itemToSave.seq = requestItems ? requestItems.length + 1 : 1;
itemSchema
.validate(itemToSave)
- .then(() => ipcRenderer.invoke('renderer:new-request', fullName, itemToSave))
+ .then(() => ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave))
.then(resolve)
.catch(reject);
@@ -491,7 +527,7 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
uid: uuid(),
type: 'OPEN_REQUEST',
collectionUid,
- itemPathname: fullName
+ itemPathname: fullPathname
})
);
} else {
@@ -503,7 +539,7 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
(i) => i.type !== 'folder' && trim(i.filename) === trim(filename)
);
if (!reqWithSameNameExists) {
- const dirname = getDirectoryName(item.pathname);
+ const dirname = path.dirname(item.pathname);
const fullName = path.join(dirname, filename);
const { ipcRenderer } = window;
const requestItems = filter(parentItem.items, (i) => i.type !== 'folder');
@@ -729,7 +765,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
};
export const newHttpRequest = (params) => (dispatch, getState) => {
- const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params;
+ const { requestName, filename, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params;
return new Promise((resolve, reject) => {
const state = getState();
@@ -757,6 +793,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
uid: uuid(),
type: requestType,
name: requestName,
+ filename,
request: {
method: requestMethod,
url: requestUrl,
@@ -769,7 +806,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
xml: null,
sparql: null,
multipartForm: null,
- formUrlEncoded: null
+ formUrlEncoded: null,
+ file: null
},
auth: auth ?? {
mode: 'none'
@@ -778,46 +816,20 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
};
// itemUid is null when we are creating a new request at the root level
- const filename = resolveRequestFilename(requestName);
+ const resolvedFilename = resolveRequestFilename(filename);
if (!itemUid) {
const reqWithSameNameExists = find(
collection.items,
- (i) => i.type !== 'folder' && trim(i.filename) === trim(filename)
+ (i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename)
);
const requestItems = filter(collection.items, (i) => i.type !== 'folder');
item.seq = requestItems.length + 1;
if (!reqWithSameNameExists) {
- const fullName = `${collection.pathname}${PATH_SEPARATOR}${filename}`;
+ const fullName = path.join(collection.pathname, resolvedFilename);
const { ipcRenderer } = window;
- ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
- // task middleware will track this and open the new request in a new tab once request is created
- dispatch(
- insertTaskIntoQueue({
- uid: uuid(),
- type: 'OPEN_REQUEST',
- collectionUid,
- itemPathname: fullName
- })
- );
- } else {
- return reject(new Error('Duplicate request names are not allowed under the same folder'));
- }
- } else {
- const currentItem = findItemInCollection(collection, itemUid);
- if (currentItem) {
- const reqWithSameNameExists = find(
- currentItem.items,
- (i) => i.type !== 'folder' && trim(i.filename) === trim(filename)
- );
- const requestItems = filter(currentItem.items, (i) => i.type !== 'folder');
- item.seq = requestItems.length + 1;
- if (!reqWithSameNameExists) {
- const fullName = `${currentItem.pathname}${PATH_SEPARATOR}${filename}`;
- const { ipcRenderer } = window;
-
- ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
+ ipcRenderer.invoke('renderer:new-request', fullName, item).then(() => {
// task middleware will track this and open the new request in a new tab once request is created
dispatch(
insertTaskIntoQueue({
@@ -827,6 +839,35 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
itemPathname: fullName
})
);
+ resolve();
+ }).catch(reject);
+ } else {
+ return reject(new Error('Duplicate request names are not allowed under the same folder'));
+ }
+ } else {
+ const currentItem = findItemInCollection(collection, itemUid);
+ if (currentItem) {
+ const reqWithSameNameExists = find(
+ currentItem.items,
+ (i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename)
+ );
+ const requestItems = filter(currentItem.items, (i) => i.type !== 'folder');
+ item.seq = requestItems.length + 1;
+ if (!reqWithSameNameExists) {
+ const fullName = path.join(currentItem.pathname, resolvedFilename);
+ const { ipcRenderer } = window;
+ ipcRenderer.invoke('renderer:new-request', fullName, item).then(() => {
+ // task middleware will track this and open the new request in a new tab once request is created
+ dispatch(
+ insertTaskIntoQueue({
+ uid: uuid(),
+ type: 'OPEN_REQUEST',
+ collectionUid,
+ itemPathname: fullName
+ })
+ );
+ resolve();
+ }).catch(reject);
} else {
return reject(new Error('Duplicate request names are not allowed under the same folder'));
}
@@ -868,16 +909,18 @@ export const importEnvironment = (name, variables, collectionUid) => (dispatch,
if (!collection) {
return reject(new Error('Collection not found'));
}
+
+ const sanitizedName = sanitizeName(name);
ipcRenderer
- .invoke('renderer:create-environment', collection.pathname, name, variables)
+ .invoke('renderer:create-environment', collection.pathname, sanitizedName, variables)
.then(
dispatch(
updateLastAction({
collectionUid,
lastAction: {
type: 'ADD_ENVIRONMENT',
- payload: name
+ payload: sanitizedName
}
})
)
@@ -897,18 +940,20 @@ export const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, g
const baseEnv = findEnvironmentInCollection(collection, baseEnvUid);
if (!collection) {
- return reject(new Error('Environmnent not found'));
+ return reject(new Error('Environment not found'));
}
+ const sanitizedName = sanitizeName(name);
+
ipcRenderer
- .invoke('renderer:create-environment', collection.pathname, name, baseEnv.variables)
+ .invoke('renderer:create-environment', collection.pathname, sanitizedName, baseEnv.variables)
.then(
dispatch(
updateLastAction({
collectionUid,
lastAction: {
type: 'ADD_ENVIRONMENT',
- payload: name
+ payload: sanitizedName
}
})
)
@@ -932,12 +977,13 @@ export const renameEnvironment = (newName, environmentUid, collectionUid) => (di
return reject(new Error('Environment not found'));
}
+ const sanitizedName = sanitizeName(newName);
const oldName = environment.name;
- environment.name = newName;
+ environment.name = sanitizedName;
environmentSchema
.validate(environment)
- .then(() => ipcRenderer.invoke('renderer:rename-environment', collection.pathname, oldName, newName))
+ .then(() => ipcRenderer.invoke('renderer:rename-environment', collection.pathname, oldName, sanitizedName))
.then(resolve)
.catch(reject);
});
@@ -1049,14 +1095,17 @@ export const browseDirectory = () => (dispatch, getState) => {
};
export const browseFiles =
- (filters = []) =>
- (dispatch, getState) => {
+ (filters, properties) =>
+ (_dispatch, _getState) => {
const { ipcRenderer } = window;
return new Promise((resolve, reject) => {
- ipcRenderer.invoke('renderer:browse-files', filters).then(resolve).catch(reject);
+ ipcRenderer
+ .invoke('renderer:browse-files', filters, properties)
+ .then(resolve)
+ .catch(reject);
});
- };
+};
export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {
const state = getState();
@@ -1106,7 +1155,7 @@ export const createCollection = (collectionName, collectionFolderName, collectio
.catch(reject);
});
};
-export const cloneCollection = (collectionName, collectionFolderName, collectionLocation, perviousPath) => () => {
+export const cloneCollection = (collectionName, collectionFolderName, collectionLocation, previousPath) => () => {
const { ipcRenderer } = window;
return ipcRenderer.invoke(
@@ -1114,7 +1163,7 @@ export const cloneCollection = (collectionName, collectionFolderName, collection
collectionName,
collectionFolderName,
collectionLocation,
- perviousPath
+ previousPath
);
};
export const openCollection = () => () => {
@@ -1158,6 +1207,22 @@ export const importCollection = (collection, collectionLocation) => (dispatch, g
});
};
+export const moveCollectionAndPersist = ({ draggedItem, targetItem }) => (dispatch, getState) => {
+ dispatch(moveCollection({ draggedItem, targetItem }));
+
+ return new Promise((resolve, reject) => {
+ const { ipcRenderer } = window;
+ const state = getState();
+
+ const collectionPaths = state.collections.collections.map((collection) => collection.pathname);
+
+ ipcRenderer
+ .invoke('renderer:update-collection-paths', collectionPaths)
+ .then(resolve)
+ .catch(reject);
+ });
+};
+
export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
@@ -1176,89 +1241,123 @@ export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (
export const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getState) => {
- const collectionSnapshotData = payload;
- return new Promise((resolve, reject) => {
- const state = getState();
- try {
- if(!collectionSnapshotData) resolve();
- const { pathname, selectedEnvironment } = collectionSnapshotData;
- const collection = findCollectionByPathname(state.collections.collections, pathname);
- const collectionCopy = cloneDeep(collection);
- const collectionUid = collectionCopy?.uid;
+ const collectionSnapshotData = payload;
+ return new Promise((resolve, reject) => {
+ const state = getState();
+ try {
+ if(!collectionSnapshotData) resolve();
+ const { pathname, selectedEnvironment } = collectionSnapshotData;
+ const collection = findCollectionByPathname(state.collections.collections, pathname);
+ const collectionCopy = cloneDeep(collection);
+ const collectionUid = collectionCopy?.uid;
- // update selected environment
- if (selectedEnvironment) {
- const environment = findEnvironmentInCollectionByName(collectionCopy, selectedEnvironment);
- if (environment) {
- dispatch(_selectEnvironment({ environmentUid: environment?.uid, collectionUid }));
- }
+ // update selected environment
+ if (selectedEnvironment) {
+ const environment = findEnvironmentInCollectionByName(collectionCopy, selectedEnvironment);
+ if (environment) {
+ dispatch(_selectEnvironment({ environmentUid: environment?.uid, collectionUid }));
}
+ }
- // todo: add any other redux state that you want to save
-
+ // todo: add any other redux state that you want to save
+
+ resolve();
+ }
+ catch(error) {
+ reject(error);
+ }
+ });
+};
+
+export const fetchOauth2Credentials = (payload) => async (dispatch, getState) => {
+ const { request, collection, itemUid, folderUid } = payload;
+ return new Promise((resolve, reject) => {
+ window.ipcRenderer
+ .invoke('renderer:fetch-oauth2-credentials', { itemUid, request, collection })
+ .then(({ credentials, url, collectionUid, credentialsId, debugInfo }) => {
+ dispatch(
+ collectionAddOauth2CredentialsByUrl({
+ credentials,
+ url,
+ collectionUid,
+ credentialsId,
+ debugInfo,
+ folderUid: folderUid || null,
+ itemUid: !folderUid ? itemUid : null
+ })
+ );
+ resolve(credentials);
+ })
+ .catch(reject);
+ });
+};
+
+export const refreshOauth2Credentials = (payload) => async (dispatch, getState) => {
+ const { request, collection, folderUid, itemId } = payload;
+ return new Promise((resolve, reject) => {
+ window.ipcRenderer
+ .invoke('renderer:refresh-oauth2-credentials', { request, collection })
+ .then(({ credentials, url, collectionUid, debugInfo }) => {
+ dispatch(
+ collectionAddOauth2CredentialsByUrl({
+ credentials,
+ url,
+ collectionUid,
+ debugInfo,
+ folderUid: folderUid || null,
+ itemId: !folderUid ? itemId : null
+ })
+ );
+ resolve(credentials);
+ })
+ .catch(reject);
+ });
+};
+
+export const clearOauth2Cache = (payload) => async (dispatch, getState) => {
+ const { collectionUid, url, credentialsId } = payload;
+ return new Promise((resolve, reject) => {
+ window.ipcRenderer
+ .invoke('clear-oauth2-cache', collectionUid, url, credentialsId)
+ .then(() => {
+ // We do not dispatch any action to modify the Redux store,
+ // since we are only clearing the session on the main process side.
resolve();
- }
- catch(error) {
- reject(error);
- }
- });
- };
+ })
+ .catch(reject);
+ });
+};
- export const fetchOauth2Credentials = (payload) => async (dispatch, getState) => {
- const { request, collection, itemUid, folderUid } = payload;
+export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch, getState) => {
+ return new Promise(async (resolve, reject) => {
+ const { ipcRenderer } = window;
+ ipcRenderer.invoke('renderer:load-request-via-worker', { collectionUid, pathname }).then(resolve).catch(reject);
+ });
+};
+
+export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState) => {
+ return new Promise(async (resolve, reject) => {
+ const { ipcRenderer } = window;
+ ipcRenderer.invoke('renderer:load-request', { collectionUid, pathname }).then(resolve).catch(reject);
+ });
+};
+
+export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig }) => (dispatch, getState) => {
+ dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounting' }));
+ return new Promise(async (resolve, reject) => {
+ callIpc('renderer:mount-collection', { collectionUid, collectionPathname, brunoConfig })
+ .then(() => dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounted' })))
+ .then(resolve)
+ .catch(() => {
+ dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'unmounted' }));
+ reject();
+ });
+ });
+};
+
+ export const showInFolder = (collectionPath) => () => {
return new Promise((resolve, reject) => {
- window.ipcRenderer
- .invoke('renderer:fetch-oauth2-credentials', { itemUid, request, collection })
- .then(({ credentials, url, collectionUid, credentialsId, debugInfo }) => {
- dispatch(
- collectionAddOauth2CredentialsByUrl({
- credentials,
- url,
- collectionUid,
- credentialsId,
- debugInfo,
- folderUid: folderUid || null,
- itemUid: !folderUid ? itemUid : null
- })
- );
- resolve(credentials);
- })
- .catch(reject);
+ const { ipcRenderer } = window;
+ ipcRenderer.invoke('renderer:show-in-folder', collectionPath).then(resolve).catch(reject);
});
};
-
- export const refreshOauth2Credentials = (payload) => async (dispatch, getState) => {
- const { request, collection, folderUid, itemId } = payload;
- return new Promise((resolve, reject) => {
- window.ipcRenderer
- .invoke('renderer:refresh-oauth2-credentials', { request, collection })
- .then(({ credentials, url, collectionUid, debugInfo }) => {
- dispatch(
- collectionAddOauth2CredentialsByUrl({
- credentials,
- url,
- collectionUid,
- debugInfo,
- folderUid: folderUid || null,
- itemId: !folderUid ? itemId : null
- })
- );
- resolve(credentials);
- })
- .catch(reject);
- });
- };
-
- export const clearOauth2Cache = (payload) => async (dispatch, getState) => {
- const { collectionUid, url, credentialsId } = payload;
- return new Promise((resolve, reject) => {
- window.ipcRenderer
- .invoke('clear-oauth2-cache', collectionUid, url, credentialsId)
- .then(() => {
- // We do not dispatch any action to modify the Redux store,
- // since we are only clearing the session on the main process side.
- resolve();
- })
- .catch(reject);
- });
- };
\ No newline at end of file
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index afe76e776..82691fd3e 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -1,10 +1,10 @@
import { uuid } from 'utils/common';
-import { find, map, forOwn, concat, filter, each, cloneDeep, get, set } from 'lodash';
+import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, findIndex } from 'lodash';
import { createSlice } from '@reduxjs/toolkit';
import {
addDepth,
areItemsTheSameExceptSeqUpdate,
- collapseCollection,
+ collapseAllItemsInCollection,
deleteItemInCollection,
deleteItemInCollectionByPathname,
findCollectionByPathname,
@@ -16,8 +16,10 @@ import {
isItemARequest
} from 'utils/collections';
import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
-import { getDirectoryName, getSubdirectoriesFromRoot, PATH_SEPARATOR } from 'utils/common/platform';
+import { getSubdirectoriesFromRoot } from 'utils/common/platform';
import toast from 'react-hot-toast';
+import mime from 'mime-types';
+import path from 'utils/common/path';
const initialState = {
collections: [],
@@ -32,9 +34,13 @@ export const collectionsSlice = createSlice({
const collectionUids = map(state.collections, (c) => c.uid);
const collection = action.payload;
- collection.settingsSelectedTab = 'headers';
+ collection.settingsSelectedTab = 'overview';
collection.folderLevelSettingsSelectedTab = {};
+ // Collection mount status is used to track the mount status of the collection
+ // values can be 'unmounted', 'mounting', 'mounted'
+ collection.mountStatus = 'unmounted';
+
// TODO: move this to use the nextAction approach
// last action is used to track the last action performed on the collection
// this is optional
@@ -44,12 +50,20 @@ export const collectionsSlice = createSlice({
collection.importedAt = new Date().getTime();
collection.lastAction = null;
- collapseCollection(collection);
+ collapseAllItemsInCollection(collection);
addDepth(collection.items);
if (!collectionUids.includes(collection.uid)) {
state.collections.push(collection);
}
},
+ updateCollectionMountStatus: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+ if (collection) {
+ if (action.payload.mountStatus) {
+ collection.mountStatus = action.payload.mountStatus;
+ }
+ }
+ },
setCollectionSecurityConfig: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
@@ -76,18 +90,25 @@ export const collectionsSlice = createSlice({
},
sortCollections: (state, action) => {
state.collectionSortOrder = action.payload.order;
+ const collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
switch (action.payload.order) {
case 'default':
state.collections = state.collections.sort((a, b) => a.importedAt - b.importedAt);
break;
case 'alphabetical':
- state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name));
+ state.collections = state.collections.sort((a, b) => collator.compare(a.name, b.name));
break;
case 'reverseAlphabetical':
- state.collections = state.collections.sort((a, b) => b.name.localeCompare(a.name));
+ state.collections = state.collections.sort((a, b) => -collator.compare(a.name, b.name));
break;
}
},
+ moveCollection: (state, action) => {
+ const { draggedItem, targetItem } = action.payload;
+ state.collections = state.collections.filter((i) => i.uid !== draggedItem.uid); // Remove dragged item
+ const targetItemIndex = state.collections.findIndex((i) => i.uid === targetItem.uid); // Find target item
+ state.collections.splice(targetItemIndex, 0, draggedItem); // Insert dragged-item above target-item
+ },
updateLastAction: (state, action) => {
const { collectionUid, lastAction } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -381,7 +402,7 @@ export const collectionsSlice = createSlice({
collection.items.push(item);
}
},
- collectionClicked: (state, action) => {
+ collapseCollection: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload);
if (collection) {
@@ -908,6 +929,76 @@ export const collectionsSlice = createSlice({
}
}
},
+ addFile: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ const item = findItemInCollection(collection, action.payload.itemUid);
+
+ if (item && isItemARequest(item)) {
+ if (!item.draft) {
+ item.draft = cloneDeep(item);
+ }
+ item.draft.request.body.file = item.draft.request.body.file || [];
+
+ item.draft.request.body.file.push({
+ uid: uuid(),
+ filePath: '',
+ contentType: '',
+ selected: false
+ });
+ }
+ }
+ },
+ updateFile: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ const item = findItemInCollection(collection, action.payload.itemUid);
+
+ if (item && isItemARequest(item)) {
+ if (!item.draft) {
+ item.draft = cloneDeep(item);
+ }
+
+ const param = find(item.draft.request.body.file, (p) => p.uid === action.payload.param.uid);
+
+ if (param) {
+ const contentType = mime.contentType(path.extname(action.payload.param.filePath));
+ param.filePath = action.payload.param.filePath;
+ param.contentType = action.payload.param.contentType || contentType || '';
+ param.selected = action.payload.param.selected;
+
+ item.draft.request.body.file = item.draft.request.body.file.map((p) => {
+ p.selected = p.uid === param.uid;
+ return p;
+ });
+ }
+ }
+ }
+ },
+ deleteFile: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ const item = findItemInCollection(collection, action.payload.itemUid);
+
+ if (item && isItemARequest(item)) {
+ if (!item.draft) {
+ item.draft = cloneDeep(item);
+ }
+
+ item.draft.request.body.file = filter(
+ item.draft.request.body.file,
+ (p) => p.uid !== action.payload.paramUid
+ );
+
+ if (item.draft.request.body.file.length > 0) {
+ item.draft.request.body.file[0].selected = true;
+ }
+ }
+ }
+ },
updateRequestAuthMode: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -964,6 +1055,10 @@ export const collectionsSlice = createSlice({
item.draft.request.body.sparql = action.payload.content;
break;
}
+ case 'file': {
+ item.draft.request.body.file = action.payload.content;
+ break;
+ }
case 'formUrlEncoded': {
item.draft.request.body.formUrlEncoded = action.payload.content;
break;
@@ -1603,34 +1698,36 @@ export const collectionsSlice = createSlice({
}
if (isFolderRoot) {
- const folderPath = getDirectoryName(file.meta.pathname);
+ const folderPath = path.dirname(file.meta.pathname);
const folderItem = findItemInCollectionByPathname(collection, folderPath);
if (folderItem) {
+ if (file?.data?.meta?.name) {
+ folderItem.name = file?.data?.meta?.name;
+ }
folderItem.root = file.data;
}
return;
}
if (collection) {
- const dirname = getDirectoryName(file.meta.pathname);
+ const dirname = path.dirname(file.meta.pathname);
const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname);
let currentPath = collection.pathname;
let currentSubItems = collection.items;
for (const directoryName of subDirectories) {
- let childItem = currentSubItems.find((f) => f.type === 'folder' && f.name === directoryName);
+ let childItem = currentSubItems.find((f) => f.type === 'folder' && f.filename === directoryName);
+ currentPath = path.join(currentPath, directoryName);
if (!childItem) {
childItem = {
uid: uuid(),
- pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
+ pathname: currentPath,
name: directoryName,
collapsed: true,
type: 'folder',
- items: []
+ items: [],
};
currentSubItems.push(childItem);
}
-
- currentPath = `${currentPath}${PATH_SEPARATOR}${directoryName}`;
currentSubItems = childItem.items;
}
@@ -1647,6 +1744,10 @@ export const collectionsSlice = createSlice({
currentItem.filename = file.meta.name;
currentItem.pathname = file.meta.pathname;
currentItem.draft = null;
+ currentItem.partial = file.partial;
+ currentItem.loading = file.loading;
+ currentItem.size = file.size;
+ currentItem.error = file.error;
} else {
currentSubItems.push({
uid: file.data.uid,
@@ -1656,7 +1757,11 @@ export const collectionsSlice = createSlice({
request: file.data.request,
filename: file.meta.name,
pathname: file.meta.pathname,
- draft: null
+ draft: null,
+ partial: file.partial,
+ loading: file.loading,
+ size: file.size,
+ error: file.error
});
}
}
@@ -1672,20 +1777,20 @@ export const collectionsSlice = createSlice({
let currentPath = collection.pathname;
let currentSubItems = collection.items;
for (const directoryName of subDirectories) {
- let childItem = currentSubItems.find((f) => f.type === 'folder' && f.name === directoryName);
+ let childItem = currentSubItems.find((f) => f.type === 'folder' && f.filename === directoryName);
+ currentPath = path.join(currentPath, directoryName);
if (!childItem) {
childItem = {
uid: uuid(),
- pathname: `${currentPath}${PATH_SEPARATOR}${directoryName}`,
- name: directoryName,
+ pathname: currentPath,
+ name: dir?.meta?.name || directoryName,
+ filename: directoryName,
collapsed: true,
type: 'folder',
items: []
};
currentSubItems.push(childItem);
}
-
- currentPath = `${currentPath}${PATH_SEPARATOR}${directoryName}`;
currentSubItems = childItem.items;
}
addDepth(collection.items);
@@ -1693,11 +1798,25 @@ export const collectionsSlice = createSlice({
},
collectionChangeFileEvent: (state, action) => {
const { file } = action.payload;
+ const isCollectionRoot = file.meta.collectionRoot ? true : false;
+ const isFolderRoot = file.meta.folderRoot ? true : false;
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
+ if (isCollectionRoot) {
+ if (collection) {
+ collection.root = file.data;
+ }
+ return;
+ }
- // check and update collection root
- if (collection && file.meta.collectionRoot) {
- collection.root = file.data;
+ if (isFolderRoot) {
+ const folderPath = path.dirname(file.meta.pathname);
+ const folderItem = findItemInCollectionByPathname(collection, folderPath);
+ if (folderItem) {
+ if (file?.data?.meta?.name) {
+ folderItem.name = file?.data?.meta?.name;
+ }
+ folderItem.root = file.data;
+ }
return;
}
@@ -1710,6 +1829,12 @@ export const collectionsSlice = createSlice({
// we don't want to lose the draft in this case
if (areItemsTheSameExceptSeqUpdate(item, file.data)) {
item.seq = file.data.seq;
+ if (item?.draft) {
+ item.draft.seq = file.data.seq;
+ }
+ if (item?.draft && areItemsTheSameExceptSeqUpdate(item?.draft, file.data)) {
+ item.draft = null;
+ }
} else {
item.name = file.data.name;
item.type = file.data.type;
@@ -1788,12 +1913,22 @@ export const collectionsSlice = createSlice({
}
},
runRequestEvent: (state, action) => {
- const { itemUid, collectionUid, type, requestUid } = action.payload;
+ const { itemUid, collectionUid, type, requestUid, hasError } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
const item = findItemInCollection(collection, itemUid);
if (item) {
+ if (type === 'pre-request-script-execution') {
+ item.requestUid = requestUid;
+ item.preRequestScriptErrorMessage = action.payload.errorMessage;
+ }
+
+ if(type === 'post-response-script-execution') {
+ item.requestUid = requestUid;
+ item.postResponseScriptErrorMessage = action.payload.errorMessage;
+ }
+
if (type === 'request-queued') {
const { cancelTokenUid } = action.payload;
item.requestUid = requestUid;
@@ -2011,6 +2146,7 @@ export const collectionsSlice = createSlice({
export const {
createCollection,
+ updateCollectionMountStatus,
setCollectionSecurityConfig,
brunoConfigUpdateEvent,
renameCollection,
@@ -2034,7 +2170,7 @@ export const {
saveRequest,
deleteRequestDraft,
newEphemeralHttpRequest,
- collectionClicked,
+ collapseCollection,
collectionFolderClicked,
requestUrlChanged,
updateAuth,
@@ -2054,6 +2190,9 @@ export const {
addMultipartFormParam,
updateMultipartFormParam,
deleteMultipartFormParam,
+ addFile,
+ updateFile,
+ deleteFile,
moveMultipartFormParam,
updateRequestAuthMode,
updateRequestBodyMode,
@@ -2110,7 +2249,8 @@ export const {
collectionClearOauth2CredentialsByUrl,
collectionGetOauth2CredentialsByUrl,
updateFolderAuth,
- updateFolderAuthMode
+ updateFolderAuthMode,
+ moveCollection
} = collectionsSlice.actions;
export default collectionsSlice.reducer;
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js
index ca6c232d8..062f367ca 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js
@@ -1,7 +1,7 @@
import toast from 'react-hot-toast';
import { createSlice } from '@reduxjs/toolkit';
import { getAppInstallDate } from 'utils/common/platform';
-
+import semver from 'semver';
const getReadNotificationIds = () => {
try {
let readNotificationIdsString = window.localStorage.getItem('bruno.notifications.read');
@@ -27,6 +27,26 @@ const initialState = {
readNotificationIds: getReadNotificationIds() || []
};
+export const filterNotificationsByVersion = (notifications, currentVersion) => {
+ try {
+ if (!notifications) return [];
+
+ if (!currentVersion) return notifications;
+
+ return notifications.filter(notification => {
+ const { minVersion, maxVersion } = notification;
+ if (!minVersion && !maxVersion) return true;
+ if (!minVersion) return semver.lte(currentVersion, maxVersion);
+ if (!maxVersion) return semver.gte(currentVersion, minVersion);
+
+ return semver.gte(currentVersion, minVersion) && semver.lte(currentVersion, maxVersion);
+ });
+ } catch (error) {
+ console.error(error);
+ return [];
+ }
+};
+
export const notificationSlice = createSlice({
name: 'notifications',
initialState,
@@ -86,13 +106,14 @@ export const notificationSlice = createSlice({
export const { setNotifications, setFetchingStatus, markNotificationAsRead, markAllNotificationsAsRead } =
notificationSlice.actions;
-export const fetchNotifications = () => (dispatch, getState) => {
+export const fetchNotifications = ({currentVersion}) => (dispatch, getState) => {
return new Promise((resolve) => {
const { ipcRenderer } = window;
dispatch(setFetchingStatus(true));
ipcRenderer
.invoke('renderer:fetch-notifications')
.then((notifications) => {
+ notifications = filterNotificationsByVersion(notifications, currentVersion);
dispatch(setNotifications({ notifications }));
dispatch(setFetchingStatus(false));
resolve(notifications);
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/notifications.spec.js b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.spec.js
new file mode 100644
index 000000000..80e84d9bd
--- /dev/null
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.spec.js
@@ -0,0 +1,133 @@
+const { filterNotificationsByVersion } = require('./notifications');
+
+describe('filterNotificationsByVersion - basic', () => {
+ it('should filter notifications by version', () => {
+ const notifications = [{ minVersion: '1.0.0', maxVersion: '1.1.0' }];
+ const currentVersion = '1.0.5';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual([{ minVersion: '1.0.0', maxVersion: '1.1.0' }]);
+ });
+
+ it('should gracefully handle no notifications', () => {
+ const notifications = [];
+ const currentVersion = '1.0.5';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual([]);
+ });
+
+ it('should gracefully handle notifications are undefined', () => {
+ const notifications = undefined;
+ const currentVersion = '1.0.5';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual([]);
+ });
+
+ it('should gracefully handle scenario when no current version is provided', () => {
+ const notifications = [{ minVersion: '1.0.0', maxVersion: '1.1.0' }];
+ const filteredNotifications = filterNotificationsByVersion(notifications);
+ expect(filteredNotifications).toEqual(notifications);
+ });
+
+ it('should gracefully handle scenario minVersion is undefined', () => {
+ const notifications = [{ minVersion: undefined, maxVersion: '1.1.0' }];
+ const currentVersion = '1.0.5';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual(notifications);
+ });
+
+ it('should gracefully handle scenario maxVersion is undefined', () => {
+ const notifications = [{ minVersion: '1.0.0', maxVersion: undefined }];
+ const currentVersion = '1.0.5';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual(notifications);
+ });
+
+ it('should gracefully handle scenario minVersion and maxVersion are undefined', () => {
+ const notifications = [{ minVersion: undefined, maxVersion: undefined }];
+ const currentVersion = '1.0.5';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual(notifications);
+ });
+});
+
+describe('filterNotificationsByVersion - semver', () => {
+ it('should filter out notifications outside version range', () => {
+ const notifications = [
+ { minVersion: '1.0.0', maxVersion: '1.1.0' }, // should be included
+ { minVersion: '2.0.0', maxVersion: '2.1.0' }, // should be filtered out
+ { minVersion: '0.5.0', maxVersion: '0.9.0' } // should be filtered out
+ ];
+ const currentVersion = '1.0.5';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual([
+ { minVersion: '1.0.0', maxVersion: '1.1.0' }
+ ]);
+ });
+
+ it('should handle mixed valid and invalid version ranges', () => {
+ const notifications = [
+ { minVersion: '1.0.0', maxVersion: '2.0.0' }, // should be included
+ { minVersion: '3.0.0', maxVersion: '4.0.0' }, // should be filtered out
+ { minVersion: '1.5.0', maxVersion: '1.8.0' }, // should be included
+ { minVersion: '0.1.0', maxVersion: '0.5.0' } // should be filtered out
+ ];
+ const currentVersion = '1.6.0';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual([
+ { minVersion: '1.0.0', maxVersion: '2.0.0' },
+ { minVersion: '1.5.0', maxVersion: '1.8.0' }
+ ]);
+ });
+
+ it('should handle edge cases of version ranges', () => {
+ const notifications = [
+ { minVersion: '1.0.0', maxVersion: '1.0.0' }, // should be included
+ { minVersion: '1.0.1', maxVersion: '2.0.0' }, // should be filtered out
+ { minVersion: '0.9.9', maxVersion: '1.0.0' } // should be included
+ ];
+ const currentVersion = '1.0.0';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual([
+ { minVersion: '1.0.0', maxVersion: '1.0.0' },
+ { minVersion: '0.9.9', maxVersion: '1.0.0' }
+ ]);
+ });
+});
+
+describe('filterNotificationsByVersion - undefined version bounds', () => {
+ it('should include notifications when minVersion is undefined and current version is below maxVersion', () => {
+ const notifications = [
+ { minVersion: undefined, maxVersion: '2.0.0' }
+ ];
+ const currentVersion = '1.5.0';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual(notifications);
+ });
+
+ it('should exclude notifications when minVersion is undefined and current version is above maxVersion', () => {
+ const notifications = [
+ { minVersion: undefined, maxVersion: '2.0.0' }
+ ];
+ const currentVersion = '2.1.0';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual([]);
+ });
+
+ it('should include notifications when maxVersion is undefined and current version is above minVersion', () => {
+ const notifications = [
+ { minVersion: '1.0.0', maxVersion: undefined }
+ ];
+ const currentVersion = '2.0.0';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual(notifications);
+ });
+
+ it('should exclude notifications when maxVersion is undefined and current version is below minVersion', () => {
+ const notifications = [
+ { minVersion: '1.0.0', maxVersion: undefined }
+ ];
+ const currentVersion = '0.9.0';
+ const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);
+ expect(filteredNotifications).toEqual([]);
+ });
+});
\ No newline at end of file
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js
index 935be6075..219655d70 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js
@@ -1,4 +1,5 @@
import { createSlice } from '@reduxjs/toolkit';
+import { findIndex } from 'lodash';
import filter from 'lodash/filter';
import find from 'lodash/find';
import last from 'lodash/last';
@@ -19,31 +20,59 @@ export const tabsSlice = createSlice({
initialState,
reducers: {
addTab: (state, action) => {
- const alreadyExists = find(state.tabs, (tab) => tab.uid === action.payload.uid);
- if (alreadyExists) {
+ const { uid, collectionUid, type, requestPaneTab, preview } = action.payload;
+ const nonReplaceableTabTypes = [
+ "variables",
+ "collection-runner",
+ "security-settings",
+ ];
+
+ const existingTab = find(state.tabs, (tab) => tab.uid === uid);
+ if (existingTab) {
+ state.activeTabUid = existingTab.uid;
return;
}
- if (
- ['variables', 'collection-settings', 'collection-runner', 'security-settings'].includes(action.payload.type)
- ) {
- const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
- if (tab) {
- state.activeTabUid = tab.uid;
+ if (nonReplaceableTabTypes.includes(type)) {
+ const existingTab = tabTypeAlreadyExists(state.tabs, collectionUid, type);
+ if (existingTab) {
+ state.activeTabUid = existingTab.uid;
return;
}
}
+ const lastTab = state.tabs[state.tabs.length - 1];
+ if (state.tabs.length > 0 && lastTab.preview) {
+ state.tabs[state.tabs.length - 1] = {
+ uid,
+ collectionUid,
+ requestPaneWidth: null,
+ requestPaneTab: requestPaneTab || 'params',
+ responsePaneTab: 'response',
+ type: type || 'request',
+ preview: preview !== undefined
+ ? preview
+ : !nonReplaceableTabTypes.includes(type),
+ ...(uid ? { folderUid: uid } : {})
+ }
+
+ state.activeTabUid = uid;
+ return
+ }
+
state.tabs.push({
- uid: action.payload.uid,
- collectionUid: action.payload.collectionUid,
+ uid,
+ collectionUid,
requestPaneWidth: null,
- requestPaneTab: action.payload.requestPaneTab || 'params',
+ requestPaneTab: requestPaneTab || 'params',
responsePaneTab: 'response',
- type: action.payload.type || 'request',
- ...(action.payload.uid ? { folderUid: action.payload.uid } : {})
+ type: type || 'request',
+ ...(uid ? { folderUid: uid } : {}),
+ preview: preview !== undefined
+ ? preview
+ : !nonReplaceableTabTypes.includes(type)
});
- state.activeTabUid = action.payload.uid;
+ state.activeTabUid = uid;
},
focusTab: (state, action) => {
state.activeTabUid = action.payload.uid;
@@ -124,6 +153,15 @@ export const tabsSlice = createSlice({
const collectionUid = action.payload.collectionUid;
state.tabs = filter(state.tabs, (t) => t.collectionUid !== collectionUid);
state.activeTabUid = null;
+ },
+ makeTabPermanent: (state, action) => {
+ const { uid } = action.payload;
+ const tab = find(state.tabs, (t) => t.uid === uid);
+ if (tab) {
+ tab.preview = false;
+ } else{
+ console.error("Tab not found!")
+ }
}
}
});
@@ -136,7 +174,8 @@ export const {
updateRequestPaneTab,
updateResponsePaneTab,
closeTabs,
- closeAllCollectionTabs
+ closeAllCollectionTabs,
+ makeTabPermanent
} = tabsSlice.actions;
-export default tabsSlice.reducer;
+export default tabsSlice.reducer;
\ No newline at end of file
diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js
index 9e8e923aa..861290981 100644
--- a/packages/bruno-app/src/themes/dark.js
+++ b/packages/bruno-app/src/themes/dark.js
@@ -114,7 +114,25 @@ const darkTheme = {
responseStatus: '#ccc',
responseOk: '#8cd656',
responseError: '#f06f57',
- responseOverlayBg: 'rgba(30, 30, 30, 0.6)'
+ responseOverlayBg: 'rgba(30, 30, 30, 0.6)',
+
+ card: {
+ bg: '#252526',
+ border: 'transparent',
+ borderDark: '#8cd656',
+ hr: '#424242'
+ },
+
+ cardTable: {
+ border: '#333',
+ bg: '#252526',
+ table: {
+ thead: {
+ bg: '#3D3D3D',
+ color: '#ccc'
+ }
+ }
+ }
},
collection: {
@@ -261,6 +279,12 @@ const darkTheme = {
scrollbar: {
color: 'rgb(52 51 49)'
+ },
+
+ infoTip: {
+ bg: '#1f1f1f',
+ border: '#333333',
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)'
}
};
diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js
index a25583136..6ce9fa583 100644
--- a/packages/bruno-app/src/themes/light.js
+++ b/packages/bruno-app/src/themes/light.js
@@ -114,7 +114,22 @@ const lightTheme = {
responseStatus: 'rgb(117 117 117)',
responseOk: '#047857',
responseError: 'rgb(185, 28, 28)',
- responseOverlayBg: 'rgba(255, 255, 255, 0.6)'
+ responseOverlayBg: 'rgba(255, 255, 255, 0.6)',
+ card: {
+ bg: '#fff',
+ border: '#f4f4f4',
+ hr: '#f4f4f4'
+ },
+ cardTable: {
+ border: '#efefef',
+ bg: '#fff',
+ table: {
+ thead: {
+ bg: 'rgb(249, 250, 251)',
+ color: 'rgb(75 85 99)'
+ }
+ }
+ }
},
collection: {
@@ -265,6 +280,12 @@ const lightTheme = {
scrollbar: {
color: 'rgb(152 151 149)'
+ },
+
+ infoTip: {
+ bg: 'white',
+ border: '#e0e0e0',
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
}
};
diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js
index 479fcd67a..110f82db5 100644
--- a/packages/bruno-app/src/utils/codegenerator/har.js
+++ b/packages/bruno-app/src/utils/codegenerator/har.js
@@ -14,6 +14,8 @@ const createContentType = (mode) => {
return 'application/json';
case 'multipartForm':
return 'multipart/form-data';
+ case 'file':
+ return 'application/octet-stream';
default:
return '';
}
@@ -60,22 +62,51 @@ const createPostData = (body, type) => {
}
const contentType = createContentType(body.mode);
- if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
- return {
- mimeType: contentType,
- params: body[body.mode]
- .filter((param) => param.enabled)
- .map((param) => ({
- name: param.name,
- value: param.value,
- ...(param.type === 'file' && { fileName: param.value })
- }))
- };
- } else {
- return {
- mimeType: contentType,
- text: body[body.mode]
- };
+
+ switch (body.mode) {
+ case 'formUrlEncoded':
+ return {
+ mimeType: contentType,
+ text: new URLSearchParams(
+ body[body.mode]
+ .filter((param) => param.enabled)
+ .reduce((acc, param) => {
+ acc[param.name] = param.value;
+ return acc;
+ }, {})
+ ).toString(),
+ params: body[body.mode]
+ .filter((param) => param.enabled)
+ .map((param) => ({
+ name: param.name,
+ value: param.value
+ }))
+ };
+ case 'multipartForm':
+ return {
+ mimeType: contentType,
+ params: body[body.mode]
+ .filter((param) => param.enabled)
+ .map((param) => ({
+ name: param.name,
+ value: param.value,
+ ...(param.type === 'file' && { fileName: param.value })
+ }))
+ };
+ case 'file':
+ return {
+ mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType,
+ params: body[body.mode]
+ .filter((param) => param.selected)
+ .map((param) => ({
+ value: param.filePath,
+ }))
+ };
+ default:
+ return {
+ mimeType: contentType,
+ text: body[body.mode]
+ };
}
};
@@ -89,6 +120,7 @@ export const buildHarRequest = ({ request, headers, type }) => {
queryString: createQuery(request.params),
postData: createPostData(request.body, type),
headersSize: 0,
- bodySize: 0
+ bodySize: 0,
+ binary: true
};
};
diff --git a/packages/bruno-app/src/utils/codemirror/javascript-lint.js b/packages/bruno-app/src/utils/codemirror/javascript-lint.js
index 475686f5d..ec612e5f9 100644
--- a/packages/bruno-app/src/utils/codemirror/javascript-lint.js
+++ b/packages/bruno-app/src/utils/codemirror/javascript-lint.js
@@ -19,6 +19,27 @@ if (!SERVER_RENDERED) {
}
return [];
}
+
+ // Set default options for Bruno
+ const defaultOptions = {
+ esversion: 11,
+ expr: true,
+ asi: true,
+ undef: true,
+ browser: true,
+ devel: true,
+ predef: {
+ 'bru': false,
+ 'req': false,
+ 'res': false,
+ 'test': false,
+ 'expect': false
+ }
+ };
+
+ // Merge provided options with defaults
+ options = Object.assign({}, defaultOptions, options);
+
if (!options.indent)
// JSHint error.character actually is a column index, this fixes underlining on lines using tabs for indentation
options.indent = 1; // JSHint default value is 4
diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js
index 5ef7b1b49..3d15fdd07 100644
--- a/packages/bruno-app/src/utils/collections/export.js
+++ b/packages/bruno-app/src/utils/collections/export.js
@@ -14,6 +14,7 @@ export const deleteUidsInItems = (items) => {
each(get(item, 'request.vars.assertions'), (a) => delete a.uid);
each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);
each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);
+ each(get(item, 'request.body.file'), (param) => delete param.uid);
}
if (item.items && item.items.length) {
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 423afc82c..fb37cc455 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -1,16 +1,6 @@
-import get from 'lodash/get';
-import each from 'lodash/each';
-import find from 'lodash/find';
-import findIndex from 'lodash/findIndex';
-import isString from 'lodash/isString';
-import map from 'lodash/map';
-import filter from 'lodash/filter';
-import sortBy from 'lodash/sortBy';
-import isEqual from 'lodash/isEqual';
-import cloneDeep from 'lodash/cloneDeep';
+import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';
import { uuid } from 'utils/common';
-import path from 'path';
-import slash from 'utils/common/slash';
+import path from 'utils/common/path';
import brunoCommon from '@usebruno/common';
const { interpolate } = brunoCommon;
@@ -36,7 +26,7 @@ export const addDepth = (items = []) => {
depth(items, 1);
};
-export const collapseCollection = (collection) => {
+export const collapseAllItemsInCollection = (collection) => {
collection.collapsed = true;
const collapseItem = (items) => {
@@ -49,7 +39,7 @@ export const collapseCollection = (collection) => {
});
};
- collapseItem(collection.items, 1);
+ collapseItem(collection.items);
};
export const sortItems = (collection) => {
@@ -101,7 +91,7 @@ export const findCollectionByItemUid = (collections, itemUid) => {
};
export const findItemByPathname = (items = [], pathname) => {
- return find(items, (i) => slash(i.pathname) === slash(pathname));
+ return find(items, (i) => i.pathname === pathname);
};
export const findItemInCollectionByPathname = (collection, pathname) => {
@@ -138,6 +128,30 @@ export const findEnvironmentInCollectionByName = (collection, name) => {
return find(collection.environments, (e) => e.name === name);
};
+export const areItemsLoading = (folder) => {
+ let flattenedItems = flattenItems(folder.items);
+ return flattenedItems?.reduce((isLoading, i) => {
+ if (i?.loading) {
+ isLoading = true;
+ }
+ return isLoading;
+ }, false);
+}
+
+export const getItemsLoadStats = (folder) => {
+ let loadingCount = 0;
+ let flattenedItems = flattenItems(folder.items);
+ flattenedItems?.forEach(i => {
+ if(i?.loading) {
+ loadingCount += 1;
+ }
+ });
+ return {
+ loading: loadingCount,
+ total: flattenedItems?.length
+ };
+}
+
export const moveCollectionItem = (collection, draggedItem, targetItem) => {
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
@@ -273,6 +287,17 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
});
};
+ const copyFileParams = (params = []) => {
+ return map(params, (param) => {
+ return {
+ uid: param.uid,
+ filePath: param.filePath,
+ contentType: param.contentType,
+ selected: param.selected
+ }
+ });
+ }
+
const copyItems = (sourceItems, destItems) => {
each(sourceItems, (si) => {
if (!isItemAFolder(si) && !isItemARequest(si) && si.type !== 'js') {
@@ -283,6 +308,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
uid: si.uid,
type: si.type,
name: si.name,
+ filename: si.filename,
seq: si.seq
};
@@ -300,7 +326,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
graphql: si.request.body.graphql,
sparql: si.request.body.sparql,
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
- multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
+ multipartForm: copyMultipartFormParams(si.request.body.multipartForm),
+ file: copyFileParams(si.request.body.file)
},
script: si.request.script,
vars: si.request.vars,
@@ -677,6 +704,10 @@ export const humanizeRequestBodyMode = (mode) => {
label = 'SPARQL';
break;
}
+ case 'file': {
+ label = 'File / Binary';
+ break;
+ }
case 'formUrlEncoded': {
label = 'Form URL Encoded';
break;
@@ -777,6 +808,7 @@ export const refreshUidsInItem = (item) => {
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
+ each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));
return item;
};
@@ -787,11 +819,13 @@ export const deleteUidsInItem = (item) => {
const headers = get(item, 'request.headers', []);
const bodyFormUrlEncoded = get(item, 'request.body.formUrlEncoded', []);
const bodyMultipartForm = get(item, 'request.body.multipartForm', []);
+ const file = get(item, 'request.body.file', []);
params.forEach((param) => delete param.uid);
headers.forEach((header) => delete header.uid);
bodyFormUrlEncoded.forEach((param) => delete param.uid);
bodyMultipartForm.forEach((param) => delete param.uid);
+ file.forEach((param) => delete param.uid);
return item;
};
diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js
index 82b98a325..661b84433 100644
--- a/packages/bruno-app/src/utils/common/codemirror.js
+++ b/packages/bruno-app/src/utils/common/codemirror.js
@@ -159,6 +159,8 @@ export const getCodeMirrorModeBasedOnContentType = (contentType, body) => {
if (contentType.includes('json')) {
return 'application/ld+json';
+ } else if (contentType.includes('image')) {
+ return 'application/image';
} else if (contentType.includes('xml')) {
return 'application/xml';
} else if (contentType.includes('html')) {
@@ -169,8 +171,6 @@ export const getCodeMirrorModeBasedOnContentType = (contentType, body) => {
return 'application/xml';
} else if (contentType.includes('yaml')) {
return 'application/yaml';
- } else if (contentType.includes('image')) {
- return 'application/image';
} else {
return 'application/text';
}
diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js
index 1244966b7..518b1908b 100644
--- a/packages/bruno-app/src/utils/common/index.js
+++ b/packages/bruno-app/src/utils/common/index.js
@@ -1,5 +1,6 @@
import { customAlphabet } from 'nanoid';
import xmlFormat from 'xml-formatter';
+import { format as jsoncFormat, applyEdits as jsoncApplyEdits } from 'jsonc-parser';
// a customized version of nanoid without using _ and -
export const uuid = () => {
@@ -26,6 +27,13 @@ export const waitForNextTick = () => {
});
};
+export const prettifyJson = (doc) => {
+ return jsoncApplyEdits(
+ doc,
+ jsoncFormat(doc, null, {insertSpaces: true, tabSize: 2})
+ );
+}
+
export const safeParseJSON = (str) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
@@ -94,6 +102,8 @@ export const getContentType = (headers) => {
if (contentType && contentType.length) {
if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(contentType[0])) {
return 'application/ld+json';
+ } else if (typeof contentType[0] === 'string' && /^image\/svg\+xml/i.test(contentType[0])) {
+ return 'image/svg+xml';
} else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) {
return 'application/xml';
}
@@ -174,3 +184,9 @@ export const generateUidBasedOnHash = (str) => {
};
export const stringifyIfNot = v => typeof v === 'string' ? v : String(v);
+
+export const getEncoding = (headers) => {
+ // Parse the charset from content type: https://stackoverflow.com/a/33192813
+ const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(headers['content-type'] || '');
+ return charsetMatch?.[1];
+}
diff --git a/packages/bruno-app/src/utils/common/ipc.js b/packages/bruno-app/src/utils/common/ipc.js
new file mode 100644
index 000000000..3559737f2
--- /dev/null
+++ b/packages/bruno-app/src/utils/common/ipc.js
@@ -0,0 +1,14 @@
+/**
+ * Wrapper for ipcRenderer.invoke that handles error cases
+ * @param {string} channel - The IPC channel name
+ * @param {...any} args - Arguments to pass to the channel
+ * @returns {Promise} - Resolves with the result or rejects with error
+ */
+export const callIpc = (channel, ...args) => {
+ const { ipcRenderer } = window;
+ if (!ipcRenderer) {
+ return Promise.reject(new Error('IPC Renderer not available'));
+ }
+
+ return ipcRenderer.invoke(channel, ...args);
+};
\ No newline at end of file
diff --git a/packages/bruno-app/src/utils/common/path.js b/packages/bruno-app/src/utils/common/path.js
new file mode 100644
index 000000000..f85a15d3c
--- /dev/null
+++ b/packages/bruno-app/src/utils/common/path.js
@@ -0,0 +1,12 @@
+import platform from 'platform';
+import path from 'path';
+
+const isWindowsOS = () => {
+ const os = platform.os;
+ const osFamily = os.family.toLowerCase();
+ return osFamily.includes('windows');
+};
+
+const brunoPath = isWindowsOS() ? path.win32 : path.posix;
+
+export default brunoPath;
diff --git a/packages/bruno-app/src/utils/common/platform.js b/packages/bruno-app/src/utils/common/platform.js
index ddfdb3a1f..dc1d7d984 100644
--- a/packages/bruno-app/src/utils/common/platform.js
+++ b/packages/bruno-app/src/utils/common/platform.js
@@ -1,7 +1,6 @@
import trim from 'lodash/trim';
-import path from 'path';
-import slash from './slash';
import platform from 'platform';
+import path from './path';
export const isElectron = () => {
if (!window) {
@@ -16,21 +15,11 @@ export const resolveRequestFilename = (name) => {
};
export const getSubdirectoriesFromRoot = (rootPath, pathname) => {
- // convert to unix style path
- pathname = slash(pathname);
- rootPath = slash(rootPath);
const relativePath = path.relative(rootPath, pathname);
return relativePath ? relativePath.split(path.sep) : [];
};
-export const getDirectoryName = (pathname) => {
- // convert to unix style path
- pathname = slash(pathname);
-
- return path.dirname(pathname);
-};
-
export const isWindowsOS = () => {
const os = platform.os;
const osFamily = os.family.toLowerCase();
@@ -45,8 +34,6 @@ export const isMacOS = () => {
return osFamily.includes('os x');
};
-export const PATH_SEPARATOR = isWindowsOS() ? '\\' : '/';
-
export const getAppInstallDate = () => {
let dateString = localStorage.getItem('bruno.installedOn');
diff --git a/packages/bruno-app/src/utils/common/regex.js b/packages/bruno-app/src/utils/common/regex.js
index 53f46741e..9338288f0 100644
--- a/packages/bruno-app/src/utils/common/regex.js
+++ b/packages/bruno-app/src/utils/common/regex.js
@@ -1 +1,55 @@
+const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g; // replace invalid characters with hyphens
+const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;
+const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start
+const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters
+const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed
+
export const variableNameRegex = /^[\w-.]*$/;
+
+export const sanitizeName = (name) => {
+ name = name
+ .replace(invalidCharacters, '-') // replace invalid characters with hyphens
+ .replace(/^[.\s-]+/, '') // remove leading dots, hyphens and spaces
+ .replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens)
+ return name;
+};
+
+export const validateName = (name) => {
+ if (!name) return false;
+ if (name.length > 255) return false; // max name length
+
+ if (reservedDeviceNames.test(name)) return false; // windows reserved names
+
+ return (
+ firstCharacter.test(name) &&
+ middleCharacters.test(name) &&
+ lastCharacter.test(name)
+ );
+};
+
+export const validateNameError = (name) => {
+ if (!name) return "Name cannot be empty.";
+ if (name.length > 255) {
+ return "Name cannot exceed 255 characters.";
+ }
+
+ if (reservedDeviceNames.test(name)) {
+ return "Name cannot be a reserved device name.";
+ }
+
+ if (!firstCharacter.test(name[0])) {
+ return "Invalid first character.";
+ }
+
+ for (let i = 1; i < name.length - 1; i++) {
+ if (!middleCharacters.test(name[i])) {
+ return `Invalid character '${name[i]}' at position ${i + 1}.`;
+ }
+ }
+
+ if (!lastCharacter.test(name[name.length - 1])) {
+ return "Invalid last character.";
+ }
+
+ return '';
+};
\ No newline at end of file
diff --git a/packages/bruno-app/src/utils/common/regex.spec.js b/packages/bruno-app/src/utils/common/regex.spec.js
new file mode 100644
index 000000000..e7a8b8d36
--- /dev/null
+++ b/packages/bruno-app/src/utils/common/regex.spec.js
@@ -0,0 +1,166 @@
+const { describe, it, expect } = require('@jest/globals');
+
+import { sanitizeName, validateName } from './regex';
+
+describe('regex validators', () => {
+ describe('sanitize name', () => {
+ it('should remove invalid characters', () => {
+ expect(sanitizeName('hello world')).toBe('hello world');
+ expect(sanitizeName('hello-world')).toBe('hello-world');
+ expect(sanitizeName('hello_world')).toBe('hello_world');
+ expect(sanitizeName('hello_world-')).toBe('hello_world-');
+ expect(sanitizeName('hello_world-123')).toBe('hello_world-123');
+ expect(sanitizeName('hello_world-123!@#$%^&*()')).toBe('hello_world-123!@#$%^&-()');
+ expect(sanitizeName('hello_world?')).toBe('hello_world-');
+ expect(sanitizeName('foo/bar/')).toBe('foo-bar-');
+ expect(sanitizeName('foo\\bar\\')).toBe('foo-bar-');
+ });
+
+ it('should remove leading hyphens', () => {
+ expect(sanitizeName('-foo')).toBe('foo');
+ expect(sanitizeName('---foo')).toBe('foo');
+ expect(sanitizeName('-foo-bar')).toBe('foo-bar');
+ });
+
+ it('should remove trailing periods', () => {
+ expect(sanitizeName('.file')).toBe('file');
+ expect(sanitizeName('.file.')).toBe('file');
+ expect(sanitizeName('file.')).toBe('file');
+ expect(sanitizeName('file.name.')).toBe('file.name');
+ expect(sanitizeName('hello world.')).toBe('hello world');
+ });
+
+ it('should handle filenames with only invalid characters', () => {
+ expect(sanitizeName('<>:"/\\|?*')).toBe('');
+ expect(sanitizeName('::::')).toBe('');
+ });
+
+ it('should handle filenames with a mix of valid and invalid characters', () => {
+ expect(sanitizeName('test<>:"/\\|?*')).toBe('test---------');
+ expect(sanitizeName('foo')).toBe('foo-bar-');
+ });
+
+ it('should remove control characters', () => {
+ expect(sanitizeName('foo\x00bar')).toBe('foo-bar');
+ expect(sanitizeName('file\x1Fname')).toBe('file-name');
+ });
+
+ it('should return an empty string if the name is empty or consists only of invalid characters', () => {
+ expect(sanitizeName('')).toBe('');
+ expect(sanitizeName('<>:"/\\|?*')).toBe('');
+ });
+
+ it('should handle filenames with multiple consecutive invalid characters', () => {
+ expect(sanitizeName('foo< {
+ expect(sanitizeName(' ')).toBe('');
+ });
+
+ it('should handle names with leading/trailing spaces', () => {
+ expect(sanitizeName(' foo bar ')).toBe('foo bar');
+ });
+
+ it('should preserve valid non-ASCII characters', () => {
+ expect(sanitizeName('brunó')).toBe('brunó');
+ expect(sanitizeName('文件')).toBe('文件');
+ expect(sanitizeName('brunfais')).toBe('brunfais');
+ expect(sanitizeName('brunai')).toBe('brunai');
+ expect(sanitizeName('brunsборка')).toBe('brunsборка');
+ expect(sanitizeName('brunпривет')).toBe('brunпривет');
+ expect(sanitizeName('🐶')).toBe('🐶');
+ expect(sanitizeName('brunfais🐶')).toBe('brunfais🐶');
+ expect(sanitizeName('file-🐶-bruno')).toBe('file-🐶-bruno');
+ expect(sanitizeName('helló')).toBe('helló');
+ });
+
+ it('should preserve case sensitivity', () => {
+ expect(sanitizeName('FileName')).toBe('FileName');
+ expect(sanitizeName('fileNAME')).toBe('fileNAME');
+ });
+
+ it('should handle filenames with multiple consecutive periods (only remove trailing)', () => {
+ expect(sanitizeName('file.name...')).toBe('file.name');
+ expect(sanitizeName('...file')).toBe('file');
+ expect(sanitizeName('file.name... ')).toBe('file.name');
+ expect(sanitizeName(' ...file')).toBe('file');
+ expect(sanitizeName(' ...file ')).toBe('file');
+ expect(sanitizeName(' ...file.... ')).toBe('file');
+ });
+
+ it('should handle very long filenames', () => {
+ const longName = 'a'.repeat(250) + '.txt';
+ expect(sanitizeName(longName)).toBe(longName);
+ });
+
+ it('should handle names with leading/trailing invalid characters', () => {
+ expect(sanitizeName('-foo/bar-')).toBe('foo-bar-');
+ expect(sanitizeName('/foo\\bar/')).toBe('foo-bar-');
+ });
+
+ it('should handle different language unicode characters', () => {
+ expect(sanitizeName('你好世界!?@#$%^&*()')).toBe('你好世界!-@#$%^&-()');
+ expect(sanitizeName('こんにちは世界!?@#$%^&*()')).toBe('こんにちは世界!-@#$%^&-()');
+ expect(sanitizeName('안녕하세요 세계!?@#$%^&*()')).toBe('안녕하세요 세계!-@#$%^&-()');
+ expect(sanitizeName('مرحبا بالعالم!?@#$%^&*()')).toBe('مرحبا بالعالم!-@#$%^&-()');
+ expect(sanitizeName('Здравствуй мир!?@#$%^&*()')).toBe('Здравствуй мир!-@#$%^&-()');
+ expect(sanitizeName('नमस्ते दुनिया!?@#$%^&*()')).toBe('नमस्ते दुनिया!-@#$%^&-()');
+ expect(sanitizeName('สวัสดีชาวโลก!?@#$%^&*()')).toBe('สวัสดีชาวโลก!-@#$%^&-()');
+ expect(sanitizeName('γειά σου κόσμος!?@#$%^&*()')).toBe('γειά σου κόσμος!-@#$%^&-()');
+ });
+
+ });
+});
+
+describe('sanitizeName and validateName', () => {
+ it('should sanitize and then validate valid names', () => {
+ const validNames = [
+ 'valid_filename.txt',
+ ' valid name ',
+ ' valid-name ',
+ 'valid<>name.txt',
+ 'file/with?invalid*chars'
+ ];
+
+ validNames.forEach(name => {
+ const sanitized = sanitizeName(name);
+ expect(validateName(sanitized)).toBe(true);
+ });
+ });
+
+ it('should sanitize and then validate names with reserved device names', () => {
+ const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'LPT2'];
+
+ reservedNames.forEach(name => {
+ const sanitized = sanitizeName(name);
+ expect(validateName(sanitized)).toBe(false);
+ });
+ });
+
+ it('should sanitize invalid names to empty strings', () => {
+ const invalidNames = [
+ ' <>:"/\\|?* ',
+ ' ... ',
+ ' ',
+ ];
+
+ invalidNames.forEach(name => {
+ const sanitized = sanitizeName(name);
+ expect(validateName(sanitized)).toBe(false);
+ });
+ });
+
+ it('should return false for reserved device names with leading/trailing spaces', () => {
+ const mixedNames = [
+ 'AUX ',
+ ' COM1 '
+ ];
+
+ mixedNames.forEach(name => {
+ const sanitized = sanitizeName(name);
+ expect(validateName(sanitized)).toBe(false);
+ });
+ });
+});
diff --git a/packages/bruno-app/src/utils/common/slash.js b/packages/bruno-app/src/utils/common/slash.js
deleted file mode 100644
index a2b39e94f..000000000
--- a/packages/bruno-app/src/utils/common/slash.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * MIT License
- *
- * Copyright (c) Sindre Sorhus (https://sindresorhus.com)
- * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
- * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
-const slash = (path) => {
- const isExtendedLengthPath = /^\\\\\?\\/.test(path);
-
- if (isExtendedLengthPath) {
- return path;
- }
-
- return path.replace(/\\/g, '/');
-};
-
-export default slash;
diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js
index c1398ab14..ea0ec2a05 100644
--- a/packages/bruno-app/src/utils/curl/curl-to-json.js
+++ b/packages/bruno-app/src/utils/curl/curl-to-json.js
@@ -99,9 +99,30 @@ function getMultipleDataString(request, parsedQueryString) {
function getFilesString(request) {
const data = {};
- data.files = {};
data.data = {};
+ if (request.isDataBinary) {
+ let filePath = '';
+
+ if (request.data.startsWith('@')) {
+ filePath = request.data.slice(1);
+ } else {
+ filePath = request.data;
+ }
+
+ data.data = [
+ {
+ filePath: repr(filePath),
+ contentType: request.headers['Content-Type'],
+ selected: true,
+ }
+ ];
+
+ return data;
+ }
+
+ data.files = {};
+
for (const multipartKey in request.multipartUploads) {
const multipartValue = request.multipartUploads[multipartKey];
if (multipartValue.startsWith('@')) {
@@ -140,6 +161,7 @@ const curlToJson = (curlCommand) => {
requestJson.url = request.urlWithoutQuery;
requestJson.raw_url = request.url;
requestJson.method = request.method;
+ requestJson.isDataBinary = request.isDataBinary;
if (request.cookies) {
const cookies = {};
@@ -161,12 +183,10 @@ const curlToJson = (curlCommand) => {
if (request.query) {
requestJson.queries = getQueries(request);
- }
-
- if (typeof request.data === 'string' || typeof request.data === 'number') {
- Object.assign(requestJson, getDataString(request));
- } else if (request.multipartUploads) {
+ } else if (request.multipartUploads || request.isDataBinary) {
Object.assign(requestJson, getFilesString(request));
+ } else if (typeof request.data === 'string' || typeof request.data === 'number') {
+ Object.assign(requestJson, getDataString(request));
}
if (request.insecure) {
diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
index 2d9785154..991150c57 100644
--- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
+++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
@@ -86,4 +86,38 @@ describe('curlToJson', () => {
method: 'get'
});
});
+
+ it('should return a parse a curl with a post body with binary file type', () => {
+ const curlCommand = `curl 'https://www.usebruno.com'
+ -H 'Accept: application/json, text/plain, */*'
+ -H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8'
+ -H 'Content-Type: application/json;charset=utf-8'
+ -H 'Origin: https://www.usebruno.com'
+ -H 'Referer: https://www.usebruno.com/'
+ --data-binary '@/path/to/file'
+ `;
+
+ const result = curlToJson(curlCommand);
+
+ expect(result).toEqual({
+ url: 'https://www.usebruno.com',
+ raw_url: 'https://www.usebruno.com',
+ method: 'post',
+ headers: {
+ Accept: 'application/json, text/plain, */*',
+ 'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',
+ 'Content-Type': 'application/json;charset=utf-8',
+ Origin: 'https://www.usebruno.com',
+ Referer: 'https://www.usebruno.com/'
+ },
+ isDataBinary: true,
+ data: [
+ {
+ filePath: '/path/to/file',
+ contentType: 'application/json;charset=utf-8',
+ selected: true
+ }
+ ]
+ });
+ });
});
diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js
index f486df56b..ad4f1edf6 100644
--- a/packages/bruno-app/src/utils/curl/index.js
+++ b/packages/bruno-app/src/utils/curl/index.js
@@ -50,14 +50,18 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque
sparql: null,
multipartForm: null,
formUrlEncoded: null,
- graphql: null
+ graphql: null,
+ file: null
};
if (parsedBody && contentType && typeof contentType === 'string') {
if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) {
body.mode = 'graphql';
body.graphql = parseGraphQL(parsedBody);
- } else if (contentType.includes('application/json')) {
+ } else if (requestType === 'http-request' && request.isDataBinary) {
+ body.mode = 'file';
+ body.file = parsedBody;
+ }else if (contentType.includes('application/json')) {
body.mode = 'json';
body.json = convertToCodeMirrorJson(parsedBody);
} else if (contentType.includes('xml')) {
diff --git a/packages/bruno-app/src/utils/exporters/postman-collection.js b/packages/bruno-app/src/utils/exporters/postman-collection.js
index 7bcd229c1..7f4b53026 100644
--- a/packages/bruno-app/src/utils/exporters/postman-collection.js
+++ b/packages/bruno-app/src/utils/exporters/postman-collection.js
@@ -11,7 +11,8 @@ import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from '../col
*/
export const transformUrl = (url, params) => {
if (typeof url !== 'string' || !url.trim()) {
- throw new Error("Invalid URL input");
+ url = "";
+ console.error("Invalid URL input:", url);
}
const urlRegexPatterns = {
diff --git a/packages/bruno-app/src/utils/importers/common.js b/packages/bruno-app/src/utils/importers/common.js
index 88c4c7872..64db764fb 100644
--- a/packages/bruno-app/src/utils/importers/common.js
+++ b/packages/bruno-app/src/utils/importers/common.js
@@ -35,6 +35,7 @@ export const updateUidsInCollection = (_collection) => {
each(get(item, 'request.assertions'), (a) => (a.uid = uuid()));
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
+ each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));
if (item.items && item.items.length) {
updateItemUids(item.items);
@@ -61,7 +62,6 @@ export const updateUidsInCollection = (_collection) => {
export const transformItemsInCollection = (collection) => {
const transformItems = (items = []) => {
each(items, (item) => {
- item.name = normalizeFileName(item.name);
if (['http', 'graphql'].includes(item.type)) {
item.type = `${item.type}-request`;
diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js
index d579402cd..4c4b87c1b 100644
--- a/packages/bruno-app/src/utils/importers/openapi-collection.js
+++ b/packages/bruno-app/src/utils/importers/openapi-collection.js
@@ -31,7 +31,7 @@ const readFile = (files) => {
};
const ensureUrl = (url) => {
- // emoving multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
+ // removing multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
return url.replace(/([^:])\/{2,}/g, '$1/');
};
@@ -229,26 +229,27 @@ const transformOpenapiRequestItem = (request) => {
return brunoRequestItem;
};
-const resolveRefs = (spec, components = spec?.components, visitedItems = new Set()) => {
+const resolveRefs = (spec, components = spec?.components, cache = new Map()) => {
if (!spec || typeof spec !== 'object') {
return spec;
}
+ if (cache.has(spec)) {
+ return cache.get(spec);
+ }
+
if (Array.isArray(spec)) {
- return spec.map((item) => resolveRefs(item, components, visitedItems));
+ return spec.map(item => resolveRefs(item, components, cache));
}
if ('$ref' in spec) {
const refPath = spec.$ref;
- if (visitedItems.has(refPath)) {
- return spec;
- } else {
- visitedItems.add(refPath);
+ if (cache.has(refPath)) {
+ return cache.get(refPath);
}
if (refPath.startsWith('#/components/')) {
- // Local reference within components
const refKeys = refPath.replace('#/components/', '').split('/');
let ref = components;
@@ -256,25 +257,26 @@ const resolveRefs = (spec, components = spec?.components, visitedItems = new Set
if (ref && ref[key]) {
ref = ref[key];
} else {
- // Handle invalid references gracefully?
return spec;
}
}
- return resolveRefs(ref, components, visitedItems);
- } else {
- // Handle external references (not implemented here)
- // You would need to fetch the external reference and resolve it.
- // Example: Fetch and resolve an external reference from a URL.
+ cache.set(refPath, {});
+ const resolved = resolveRefs(ref, components, cache);
+ cache.set(refPath, resolved);
+ return resolved;
}
+ return spec;
}
- // Recursively resolve references in nested objects
- for (const prop in spec) {
- spec[prop] = resolveRefs(spec[prop], components, new Set(visitedItems));
+ const resolved = {};
+ cache.set(spec, resolved);
+
+ for (const [key, value] of Object.entries(spec)) {
+ resolved[key] = resolveRefs(value, components, cache);
}
- return spec;
+ return resolved;
};
const groupRequestsByTags = (requests) => {
diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js
index e0b0a080f..65c26aa98 100644
--- a/packages/bruno-app/src/utils/importers/postman-collection.js
+++ b/packages/bruno-app/src/utils/importers/postman-collection.js
@@ -181,6 +181,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
brunoParent.items = brunoParent.items || [];
const folderMap = {};
const requestMap = {};
+ const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE']
each(item, (i) => {
if (isItemAFolder(i)) {
@@ -230,6 +231,11 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
} else {
if (i.request) {
+ if(!requestMethods.includes(i?.request?.method.toUpperCase())){
+ console.warn("Unexpected request.method")
+ return;
+ }
+
const baseRequestName = i.name;
let requestName = baseRequestName;
let count = 1;
@@ -422,7 +428,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
brunoRequestItem.request.auth.mode = 'apikey';
brunoRequestItem.request.auth.apikey = {
key: authValues.key,
- value: authValues.value,
+ value: authValues.value?.toString(), // Convert the value to a string as Postman's schema does not rigidly define the type of it,
placement: "header" //By default we are placing the apikey values in headers!
}
} else if (auth.type === 'oauth2'){
@@ -585,7 +591,9 @@ const parsePostmanCollection = (str, options) => {
let v2Schemas = [
'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',
- 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
+ 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
+ 'https://schema.postman.com/json/collection/v2.0.0/collection.json',
+ 'https://schema.postman.com/json/collection/v2.1.0/collection.json'
];
if (v2Schemas.includes(schema)) {
diff --git a/packages/bruno-app/src/utils/importers/translators/postman_translation.js b/packages/bruno-app/src/utils/importers/translators/postman_translation.js
index b386d719e..5d6573c16 100644
--- a/packages/bruno-app/src/utils/importers/translators/postman_translation.js
+++ b/packages/bruno-app/src/utils/importers/translators/postman_translation.js
@@ -24,6 +24,10 @@ const replacements = {
'postman\\.setEnvironmentVariable\\(': 'bru.setEnvVar(',
'postman\\.getEnvironmentVariable\\(': 'bru.getEnvVar(',
'postman\\.clearEnvironmentVariable\\(': 'bru.deleteEnvVar(',
+ 'pm\\.execution\\.skipRequest\\(\\)': 'bru.runner.skipRequest()',
+ 'pm\\.execution\\.skipRequest': 'bru.runner.skipRequest',
+ 'pm\\.execution\\.setNextRequest\\(null\\)': 'bru.runner.stopExecution()',
+ 'pm\\.execution\\.setNextRequest\\(\'null\'\\)': 'bru.runner.stopExecution()',
};
const extendedReplacements = Object.keys(replacements).reduce((acc, key) => {
@@ -50,7 +54,7 @@ export const postmanTranslation = (script, logCallback) => {
}
if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) {
modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1');
- logCallback?.();
+ //logCallback?.();
}
return modifiedScript;
} catch (e) {
diff --git a/packages/bruno-app/src/utils/tabs/index.js b/packages/bruno-app/src/utils/tabs/index.js
index a6fa29dd7..b5cfe2b18 100644
--- a/packages/bruno-app/src/utils/tabs/index.js
+++ b/packages/bruno-app/src/utils/tabs/index.js
@@ -11,3 +11,10 @@ export const isItemAFolder = (item) => {
export const itemIsOpenedInTabs = (item, tabs) => {
return find(tabs, (t) => t.uid === item.uid);
};
+
+export const scrollToTheActiveTab = () => {
+ const activeTab = document.querySelector('.request-tab.active');
+ if (activeTab) {
+ activeTab.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+};
\ No newline at end of file
diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json
index 8353e8ba8..2de5c7142 100644
--- a/packages/bruno-cli/package.json
+++ b/packages/bruno-cli/package.json
@@ -46,13 +46,13 @@
"package.json"
],
"dependencies": {
- "@aws-sdk/credential-providers": "3.658.1",
+ "@aws-sdk/credential-providers": "3.750.0",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
"@usebruno/vm2": "^3.9.13",
"aws4-axios": "^3.3.0",
- "axios": "1.7.5",
+ "axios": "^1.8.3",
"axios-ntlm": "^1.4.2",
"chai": "^4.3.7",
"chalk": "^3.0.0",
@@ -66,7 +66,6 @@
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"tough-cookie": "^4.1.3",
- "@usebruno/vm2": "^3.9.13",
"xmlbuilder": "^15.1.1",
"yargs": "^17.6.2"
}
diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js
index 29edf63b9..13106de96 100644
--- a/packages/bruno-cli/src/commands/run.js
+++ b/packages/bruno-cli/src/commands/run.js
@@ -11,6 +11,7 @@ const { rpad } = require('../utils/common');
const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
const { dotenvToJson } = require('@usebruno/lang');
const constants = require('../constants');
+const { findItemInCollection } = require('../utils/collection');
const command = 'run [filename]';
const desc = 'Run a request';
@@ -18,6 +19,7 @@ const printRunSummary = (results) => {
let totalRequests = 0;
let passedRequests = 0;
let failedRequests = 0;
+ let skippedRequests = 0;
let totalAssertions = 0;
let passedAssertions = 0;
let failedAssertions = 0;
@@ -49,7 +51,10 @@ const printRunSummary = (results) => {
failedAssertions += 1;
}
}
- if (!hasAnyTestsOrAssertions && result.error) {
+ if (!hasAnyTestsOrAssertions && result.skipped) {
+ skippedRequests += 1;
+ }
+ else if (!hasAnyTestsOrAssertions && result.error) {
failedRequests += 1;
} else {
passedRequests += 1;
@@ -62,6 +67,9 @@ const printRunSummary = (results) => {
if (failedRequests > 0) {
requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`;
}
+ if (skippedRequests > 0) {
+ requestSummary += `, ${chalk.magenta(`${skippedRequests} skipped`)}`;
+ }
requestSummary += `, ${totalRequests} total`;
let assertSummary = `${rpad('Tests:', maxLength)} ${chalk.green(`${passedTests} passed`)}`;
@@ -84,6 +92,7 @@ const printRunSummary = (results) => {
totalRequests,
passedRequests,
failedRequests,
+ skippedRequests,
totalAssertions,
passedAssertions,
failedAssertions,
@@ -144,7 +153,7 @@ const createCollectionFromPath = (collectionPath) => {
});
}
}
- return currentDirItems
+ return currentDirItems;
};
collection.items = traverse(collectionPath);
return collection;
@@ -285,7 +294,7 @@ const builder = async (yargs) => {
type: 'string'
})
.option('sandbox', {
- describe: 'Javscript sandbox to use; available sandboxes are "developer" (default) or "safe"',
+ describe: 'Javascript sandbox to use; available sandboxes are "developer" (default) or "safe"',
default: 'developer',
type: 'string'
})
@@ -338,6 +347,10 @@ const builder = async (yargs) => {
type: 'string',
description: 'Path to the Client certificate config file used for securing the connection in the request'
})
+ .option('delay', {
+ type:"number",
+ description: "Delay between each requests (in miliseconds)"
+ })
.example('$0 run request.bru', 'Run a request')
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
@@ -378,7 +391,8 @@ const builder = async (yargs) => {
'$0 run folder --cacert myCustomCA.pem --ignore-truststore',
'Use a custom CA certificate exclusively when validating the peers of the requests in the specified folder.'
)
- .example('$0 run --client-cert-config client-cert-config.json', 'Run a request with Client certificate configurations');
+ .example('$0 run --client-cert-config client-cert-config.json', 'Run a request with Client certificate configurations')
+ .example('$0 run folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.');
};
const handler = async function (argv) {
@@ -402,7 +416,8 @@ const handler = async function (argv) {
bail,
reporterSkipAllHeaders,
reporterSkipHeaders,
- clientCertConfig
+ clientCertConfig,
+ delay
} = argv;
const collectionPath = process.cwd();
@@ -634,6 +649,34 @@ const handler = async function (argv) {
}
const runtime = getJsSandboxRuntime(sandbox);
+
+ const runSingleRequestByPathname = async (relativeItemPathname) => {
+ return new Promise(async (resolve, reject) => {
+ let itemPathname = path.join(collectionPath, relativeItemPathname);
+ if (itemPathname && !itemPathname?.endsWith('.bru')) {
+ itemPathname = `${itemPathname}.bru`;
+ }
+ const bruJson = cloneDeep(findItemInCollection(collection, itemPathname));
+ if (bruJson) {
+ const res = await runSingleRequest(
+ itemPathname,
+ bruJson,
+ collectionPath,
+ runtimeVariables,
+ envVars,
+ processEnvVars,
+ brunoConfig,
+ collectionRoot,
+ runtime,
+ collection,
+ runSingleRequestByPathname
+ );
+ resolve(res?.response);
+ }
+ reject(`bru.runRequest: invalid request path - ${itemPathname}`);
+ });
+ }
+
let currentRequestIndex = 0;
let nJumps = 0; // count the number of jumps to avoid infinite loops
while (currentRequestIndex < bruJsons.length) {
@@ -651,9 +694,21 @@ const handler = async function (argv) {
brunoConfig,
collectionRoot,
runtime,
- collection
+ collection,
+ runSingleRequestByPathname
);
+ const isLastRun = currentRequestIndex === bruJsons.length - 1;
+ const isValidDelay = !Number.isNaN(delay) && delay > 0;
+ if(isValidDelay && !isLastRun){
+ console.log(chalk.yellow(`Waiting for ${delay}ms or ${(delay/1000).toFixed(3)}s before next request.`));
+ await new Promise((resolve) => setTimeout(resolve, delay));
+ }
+
+ if(Number.isNaN(delay) && !isLastRun){
+ console.log(chalk.red(`Ignoring delay because it's not a valid number.`));
+ }
+
results.push({
...result,
runtime: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9,
@@ -701,11 +756,16 @@ const handler = async function (argv) {
// determine next request
const nextRequestName = result?.nextRequestName;
+
+ if (result?.shouldStopRunnerExecution) {
+ break;
+ }
+
if (nextRequestName !== undefined) {
nJumps++;
if (nJumps > 10000) {
console.error(chalk.red(`Too many jumps, possible infinite loop`));
- process.exit(constants.EXIT_STATUS.ERROR_INFINTE_LOOP);
+ process.exit(constants.EXIT_STATUS.ERROR_INFINITE_LOOP);
}
if (nextRequestName === null) {
break;
diff --git a/packages/bruno-cli/src/constants.js b/packages/bruno-cli/src/constants.js
index e7de8a6e0..cdc0d47aa 100644
--- a/packages/bruno-cli/src/constants.js
+++ b/packages/bruno-cli/src/constants.js
@@ -10,7 +10,7 @@ const EXIT_STATUS = {
// The specified output dir does not exist
ERROR_MISSING_OUTPUT_DIR: 2,
// request chain caused an endless loop
- ERROR_INFINTE_LOOP: 3,
+ ERROR_INFINITE_LOOP: 3,
// bru was called outside of a collection root
ERROR_NOT_IN_COLLECTION: 4,
// The specified file was not found
diff --git a/packages/bruno-cli/src/reporters/junit.js b/packages/bruno-cli/src/reporters/junit.js
index 30fb51939..e4a622722 100644
--- a/packages/bruno-cli/src/reporters/junit.js
+++ b/packages/bruno-cli/src/reporters/junit.js
@@ -62,7 +62,10 @@ const makeJUnitOutput = async (results, outputPath) => {
suite.testcase.push(testcase);
});
- if (result.error) {
+ if (result?.skipped) {
+ suite['@skipped'] = 1;
+ }
+ else if (result.error) {
suite['@errors'] = 1;
suite['@tests'] = 1;
suite.testcase = [
diff --git a/packages/bruno-cli/src/runner/awsv4auth-helper.js b/packages/bruno-cli/src/runner/awsv4auth-helper.js
index 4a2ff5aa2..8714ae39c 100644
--- a/packages/bruno-cli/src/runner/awsv4auth-helper.js
+++ b/packages/bruno-cli/src/runner/awsv4auth-helper.js
@@ -10,7 +10,8 @@ async function resolveAwsV4Credentials(request) {
if (isStrPresent(awsv4.profileName)) {
try {
credentialsProvider = fromIni({
- profile: awsv4.profileName
+ profile: awsv4.profileName,
+ ignoreCache: true
});
credentials = await credentialsProvider();
awsv4.accessKeyId = credentials.accessKeyId;
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index b2bbc3795..c8e137b7b 100644
--- a/packages/bruno-cli/src/runner/run-single-request.js
+++ b/packages/bruno-cli/src/runner/run-single-request.js
@@ -40,11 +40,13 @@ const runSingleRequest = async function (
brunoConfig,
collectionRoot,
runtime,
- collection
+ collection,
+ runSingleRequestByPathname
) {
try {
let request;
let nextRequestName;
+ let shouldStopRunnerExecution = false;
let item = {
pathname: path.join(collectionPath, filename),
...bruJson
@@ -68,11 +70,41 @@ const runSingleRequest = async function (
collectionPath,
onConsoleLog,
processEnvVars,
- scriptingConfig
+ scriptingConfig,
+ runSingleRequestByPathname
);
if (result?.nextRequestName !== undefined) {
nextRequestName = result.nextRequestName;
}
+
+ if (result?.stopExecution) {
+ shouldStopRunnerExecution = true;
+ }
+
+ if (result?.skipRequest) {
+ return {
+ test: {
+ filename: filename
+ },
+ request: {
+ method: request.method,
+ url: request.url,
+ headers: request.headers,
+ data: request.data
+ },
+ response: {
+ status: 'skipped',
+ statusText: 'request skipped via pre-request script',
+ data: null,
+ responseTime: 0
+ },
+ error: 'Request has been skipped from pre-request script',
+ skipped: true,
+ assertionResults: [],
+ testResults: [],
+ shouldStopRunnerExecution
+ };
+ }
}
// interpolate variables inside request
@@ -233,7 +265,30 @@ const runSingleRequest = async function (
if (!options.disableCookies) {
const cookieString = getCookieStringForUrl(request.url);
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
- request.headers['cookie'] = cookieString;
+ const existingCookieHeaderName = Object.keys(request.headers).find(
+ name => name.toLowerCase() === 'cookie'
+ );
+ const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';
+
+ // Helper function to parse cookies into an object
+ const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {
+ const [name, ...rest] = cookie.split('=');
+ if (name && name.trim()) {
+ cookies[name.trim()] = rest.join('=').trim();
+ }
+ return cookies;
+ }, {});
+
+ const mergedCookies = {
+ ...parseCookies(existingCookieString),
+ ...parseCookies(cookieString),
+ };
+
+ const combinedCookieString = Object.entries(mergedCookies)
+ .map(([name, value]) => `${name}=${value}`)
+ .join('; ');
+
+ request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;
}
}
@@ -323,7 +378,8 @@ const runSingleRequest = async function (
error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!',
assertionResults: [],
testResults: [],
- nextRequestName: nextRequestName
+ nextRequestName: nextRequestName,
+ shouldStopRunnerExecution
};
}
}
@@ -363,11 +419,16 @@ const runSingleRequest = async function (
collectionPath,
null,
processEnvVars,
- scriptingConfig
+ scriptingConfig,
+ runSingleRequestByPathname
);
if (result?.nextRequestName !== undefined) {
nextRequestName = result.nextRequestName;
}
+
+ if (result?.stopExecution) {
+ shouldStopRunnerExecution = true;
+ }
}
// run assertions
@@ -408,13 +469,18 @@ const runSingleRequest = async function (
collectionPath,
null,
processEnvVars,
- scriptingConfig
+ scriptingConfig,
+ runSingleRequestByPathname
);
testResults = get(result, 'results', []);
if (result?.nextRequestName !== undefined) {
nextRequestName = result.nextRequestName;
}
+
+ if (result?.stopExecution) {
+ shouldStopRunnerExecution = true;
+ }
}
if (testResults?.length) {
@@ -447,7 +513,8 @@ const runSingleRequest = async function (
error: null,
assertionResults,
testResults,
- nextRequestName: nextRequestName
+ nextRequestName: nextRequestName,
+ shouldStopRunnerExecution
};
} catch (err) {
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
diff --git a/packages/bruno-cli/src/utils/collection.js b/packages/bruno-cli/src/utils/collection.js
index 365732c48..64e17cb39 100644
--- a/packages/bruno-cli/src/utils/collection.js
+++ b/packages/bruno-cli/src/utils/collection.js
@@ -204,5 +204,6 @@ module.exports = {
mergeHeaders,
mergeVars,
mergeScripts,
+ findItemInCollection,
getTreePathFromCollectionToItem
}
\ No newline at end of file
diff --git a/packages/bruno-cli/src/utils/common.js b/packages/bruno-cli/src/utils/common.js
index d2eeec366..7a2ae3bf2 100644
--- a/packages/bruno-cli/src/utils/common.js
+++ b/packages/bruno-cli/src/utils/common.js
@@ -34,14 +34,10 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
// Filter out ZWNBSP character
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
data = data.replace(/^\uFEFF/, '');
-
- // If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed
- if (!disableParsingResponseJson && !(typeof data === 'string' && data.startsWith('"') && data.endsWith('"'))) {
+ if (!disableParsingResponseJson) {
data = JSON.parse(data);
}
- } catch {
-
- }
+ } catch { }
return { data, dataBuffer };
};
diff --git a/packages/bruno-common/src/interpolate/index.spec.ts b/packages/bruno-common/src/interpolate/index.spec.ts
index adfdf54cd..9dc76b7f1 100644
--- a/packages/bruno-common/src/interpolate/index.spec.ts
+++ b/packages/bruno-common/src/interpolate/index.spec.ts
@@ -297,46 +297,46 @@ describe('interpolate - recursive', () => {
expect(result).toBe('{{recursion3}}');
});
- it('should replace repetead placeholders with 1 level of recursion with values from the object', () => {
- const inputString = '{{repetead}}';
+ it('should replace repeated placeholders with 1 level of recursion with values from the object', () => {
+ const inputString = '{{repeated}}';
const inputObject = {
- repetead: '{{repetead2}} {{repetead2}}',
- repetead2: 'repetead2'
+ repeated: '{{repeated2}} {{repeated2}}',
+ repeated2: 'repeated2'
};
const result = interpolate(inputString, inputObject);
- expect(result).toBe(new Array(2).fill('repetead2').join(' '));
+ expect(result).toBe(new Array(2).fill('repeated2').join(' '));
});
- it('should replace repetead placeholders with 2 level of recursion with values from the object', () => {
- const inputString = '{{repetead}}';
+ it('should replace repeated placeholders with 2 level of recursion with values from the object', () => {
+ const inputString = '{{repeated}}';
const inputObject = {
- repetead: '{{repetead2}} {{repetead2}}',
- repetead2: '{{repetead3}} {{repetead3}} {{repetead3}}',
- repetead3: 'repetead3'
+ repeated: '{{repeated2}} {{repeated2}}',
+ repeated2: '{{repeated3}} {{repeated3}} {{repeated3}}',
+ repeated3: 'repeated3'
};
const result = interpolate(inputString, inputObject);
- expect(result).toBe(new Array(6).fill('repetead3').join(' '));
+ expect(result).toBe(new Array(6).fill('repeated3').join(' '));
});
- it('should replace repetead placeholders with 3 level of recursion with values from the object', () => {
- const inputString = '{{repetead}}';
+ it('should replace repeated placeholders with 3 level of recursion with values from the object', () => {
+ const inputString = '{{repeated}}';
const inputObject = {
- repetead: '{{repetead2}} {{repetead2}}',
- repetead2: '{{repetead3}} {{repetead3}} {{repetead3}}',
- repetead3: '{{repetead4}} {{repetead4}} {{repetead4}} {{repetead4}}',
- repetead4: 'repetead4'
+ repeated: '{{repeated2}} {{repeated2}}',
+ repeated2: '{{repeated3}} {{repeated3}} {{repeated3}}',
+ repeated3: '{{repeated4}} {{repeated4}} {{repeated4}} {{repeated4}}',
+ repeated4: 'repeated4'
};
const result = interpolate(inputString, inputObject);
- expect(result).toBe(new Array(24).fill('repetead4').join(' '));
+ expect(result).toBe(new Array(24).fill('repeated4').join(' '));
});
- it('should replace mutiple interdependent variables in the same input string', () => {
+ it('should replace multiple interdependent variables in the same input string', () => {
const inputString = `{
"x": "{{v2}} {{v1}}"
}`;
diff --git a/packages/bruno-docs/package.json b/packages/bruno-docs/package.json
index fc144d697..77aa04c0f 100644
--- a/packages/bruno-docs/package.json
+++ b/packages/bruno-docs/package.json
@@ -8,4 +8,4 @@
],
"dependencies": {
}
-}
+}
\ No newline at end of file
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index 16a032b3a..cc960f2fe 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -9,6 +9,7 @@
"scripts": {
"clean": "rimraf dist",
"dev": "electron .",
+ "debug": "electron . --inspect=9229",
"dist:mac": "electron-builder --mac --config electron-builder-config.js",
"dist:win": "electron-builder --win --config electron-builder-config.js",
"dist:linux": "electron-builder --linux AppImage --config electron-builder-config.js",
@@ -24,7 +25,7 @@
]
},
"dependencies": {
- "@aws-sdk/credential-providers": "3.658.1",
+ "@aws-sdk/credential-providers": "3.750.0",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
@@ -33,7 +34,7 @@
"@usebruno/vm2": "^3.9.13",
"about-window": "^1.15.2",
"aws4-axios": "^3.3.0",
- "axios": "1.7.5",
+ "axios": "^1.8.3",
"axios-ntlm": "^1.4.2",
"chai": "^4.3.7",
"chokidar": "^3.5.3",
diff --git a/packages/bruno-electron/src/app/collections.js b/packages/bruno-electron/src/app/collections.js
index 5c9889e13..a6b7a178c 100644
--- a/packages/bruno-electron/src/app/collections.js
+++ b/packages/bruno-electron/src/app/collections.js
@@ -2,7 +2,7 @@ const fs = require('fs');
const path = require('path');
const { dialog, ipcMain } = require('electron');
const Yup = require('yup');
-const { isDirectory, normalizeAndResolvePath } = require('../utils/filesystem');
+const { isDirectory, normalizeAndResolvePath, getCollectionStats } = require('../utils/filesystem');
const { generateUidBasedOnHash } = require('../utils/common');
// todo: bruno.json config schema validation errors must be propagated to the UI
@@ -45,9 +45,8 @@ const openCollectionDialog = async (win, watcher) => {
const { filePaths } = await dialog.showOpenDialog(win, {
properties: ['openDirectory', 'createDirectory']
});
-
if (filePaths && filePaths[0]) {
- const resolvedPath = normalizeAndResolvePath(filePaths[0]);
+ const resolvedPath = path.resolve(filePaths[0]);
if (isDirectory(resolvedPath)) {
openCollection(win, watcher, resolvedPath);
} else {
@@ -59,7 +58,7 @@ const openCollectionDialog = async (win, watcher) => {
const openCollection = async (win, watcher, collectionPath, options = {}) => {
if (!watcher.hasWatcher(collectionPath)) {
try {
- const brunoConfig = await getCollectionConfigFile(collectionPath);
+ let brunoConfig = await getCollectionConfigFile(collectionPath);
const uid = generateUidBasedOnHash(collectionPath);
if (!brunoConfig.ignore || brunoConfig.ignore.length === 0) {
@@ -70,6 +69,10 @@ const openCollection = async (win, watcher, collectionPath, options = {}) => {
brunoConfig.ignore = ['node_modules', '.git'];
}
+ const { size, filesCount } = await getCollectionStats(collectionPath);
+ brunoConfig.size = size;
+ brunoConfig.filesCount = filesCount;
+
win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', win, collectionPath, uid, brunoConfig);
} catch (err) {
diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js
index 43d01153d..39c22bb1a 100644
--- a/packages/bruno-electron/src/app/watcher.js
+++ b/packages/bruno-electron/src/app/watcher.js
@@ -2,8 +2,8 @@ const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const chokidar = require('chokidar');
-const { hasBruExtension, isWSLPath, normalizeAndResolvePath, normalizeWslPath } = require('../utils/filesystem');
-const { bruToEnvJson, bruToJson, collectionBruToJson } = require('../bru');
+const { hasBruExtension, isWSLPath, normalizeAndResolvePath, sizeInMB } = require('../utils/filesystem');
+const { bruToEnvJson, bruToJson, bruToJsonViaWorker, collectionBruToJson } = require('../bru');
const { dotenvToJson } = require('@usebruno/lang');
const { uuid } = require('../utils/common');
@@ -13,6 +13,9 @@ const { setDotEnvVars } = require('../store/process-env');
const { setBrunoConfig } = require('../store/bruno-config');
const EnvironmentSecretsStore = require('../store/env-secrets');
const UiStateSnapshot = require('../store/ui-state-snapshot');
+const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection');
+
+const MAX_FILE_SIZE = 2.5 * 1024 * 1024;
const environmentSecretsStore = new EnvironmentSecretsStore();
@@ -44,28 +47,6 @@ const isCollectionRootBruFile = (pathname, collectionPath) => {
return dirname === collectionPath && basename === 'collection.bru';
};
-const hydrateRequestWithUuid = (request, pathname) => {
- request.uid = getRequestUid(pathname);
-
- const params = _.get(request, 'request.params', []);
- const headers = _.get(request, 'request.headers', []);
- const requestVars = _.get(request, 'request.vars.req', []);
- const responseVars = _.get(request, 'request.vars.res', []);
- const assertions = _.get(request, 'request.assertions', []);
- const bodyFormUrlEncoded = _.get(request, 'request.body.formUrlEncoded', []);
- const bodyMultipartForm = _.get(request, 'request.body.multipartForm', []);
-
- params.forEach((param) => (param.uid = uuid()));
- headers.forEach((header) => (header.uid = uuid()));
- requestVars.forEach((variable) => (variable.uid = uuid()));
- responseVars.forEach((variable) => (variable.uid = uuid()));
- assertions.forEach((assertion) => (assertion.uid = uuid()));
- bodyFormUrlEncoded.forEach((param) => (param.uid = uuid()));
- bodyMultipartForm.forEach((param) => (param.uid = uuid()));
-
- return request;
-};
-
const hydrateBruCollectionFileWithUuid = (collectionRoot) => {
const params = _.get(collectionRoot, 'request.params', []);
const headers = _.get(collectionRoot, 'request.headers', []);
@@ -99,7 +80,7 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath)
let bruContent = fs.readFileSync(pathname, 'utf8');
- file.data = bruToEnvJson(bruContent);
+ file.data = await bruToEnvJson(bruContent);
file.data.name = basename.substring(0, basename.length - 4);
file.data.uid = getRequestUid(pathname);
@@ -134,7 +115,7 @@ const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPat
};
const bruContent = fs.readFileSync(pathname, 'utf8');
- file.data = bruToEnvJson(bruContent);
+ file.data = await bruToEnvJson(bruContent);
file.data.name = basename.substring(0, basename.length - 4);
file.data.uid = getRequestUid(pathname);
_.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));
@@ -179,7 +160,7 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
}
};
-const add = async (win, pathname, collectionUid, collectionPath) => {
+const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread) => {
console.log(`watcher add: ${pathname}`);
if (isBrunoConfigFile(pathname, collectionPath)) {
@@ -228,7 +209,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
try {
let bruContent = fs.readFileSync(pathname, 'utf8');
- file.data = collectionBruToJson(bruContent);
+ file.data = await collectionBruToJson(bruContent);
hydrateBruCollectionFileWithUuid(file.data);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
@@ -241,7 +222,6 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
// Is this a folder.bru file?
if (path.basename(pathname) === 'folder.bru') {
- console.log('folder.bru file detected');
const file = {
meta: {
collectionUid,
@@ -254,7 +234,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
try {
let bruContent = fs.readFileSync(pathname, 'utf8');
- file.data = collectionBruToJson(bruContent);
+ file.data = await collectionBruToJson(bruContent);
hydrateBruCollectionFileWithUuid(file.data);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
@@ -274,33 +254,89 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
}
};
+ const fileStats = fs.statSync(pathname);
+ let bruContent = fs.readFileSync(pathname, 'utf8');
+ // If worker thread is not used, we can directly parse the file
+ if (!useWorkerThread) {
+ try {
+ file.data = await bruToJson(bruContent);
+ file.partial = false;
+ file.loading = false;
+ file.size = sizeInMB(fileStats?.size);
+ hydrateRequestWithUuid(file.data, pathname);
+ win.webContents.send('main:collection-tree-updated', 'addFile', file);
+ } catch (error) {
+ console.error(error);
+ }
+ return;
+ }
+
try {
- let bruContent = fs.readFileSync(pathname, 'utf8');
-
- file.data = bruToJson(bruContent);
+ // we need to send a partial file info to the UI
+ // so that the UI can display the file in the collection tree
+ file.data = {
+ name: path.basename(pathname),
+ type: 'http-request'
+ };
+ const metaJson = await bruToJson(parseBruFileMeta(bruContent), true);
+ file.data = metaJson;
+ file.partial = true;
+ file.loading = false;
+ file.size = sizeInMB(fileStats?.size);
+ hydrateRequestWithUuid(file.data, pathname);
+ win.webContents.send('main:collection-tree-updated', 'addFile', file);
+
+ if (fileStats.size < MAX_FILE_SIZE) {
+ // This is to update the loading indicator in the UI
+ file.data = metaJson;
+ file.partial = false;
+ file.loading = true;
+ hydrateRequestWithUuid(file.data, pathname);
+ win.webContents.send('main:collection-tree-updated', 'addFile', file);
+
+ // This is to update the file info in the UI
+ file.data = await bruToJsonViaWorker(bruContent);
+ file.partial = false;
+ file.loading = false;
+ hydrateRequestWithUuid(file.data, pathname);
+ win.webContents.send('main:collection-tree-updated', 'addFile', file);
+ }
+ } catch(error) {
+ file.data = {
+ name: path.basename(pathname),
+ type: 'http-request'
+ };
+ file.error = {
+ message: error?.message
+ };
+ file.partial = true;
+ file.loading = false;
+ file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
- } catch (err) {
- console.error(err);
}
}
};
-const addDirectory = (win, pathname, collectionUid, collectionPath) => {
+const addDirectory = async (win, pathname, collectionUid, collectionPath) => {
const envDirectory = path.join(collectionPath, 'environments');
if (pathname === envDirectory) {
return;
}
+ let name = path.basename(pathname);
+
const directory = {
meta: {
collectionUid,
pathname,
- name: path.basename(pathname)
+ name
}
};
+
+
win.webContents.send('main:collection-tree-updated', 'addDir', directory);
};
@@ -357,7 +393,31 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
try {
let bruContent = fs.readFileSync(pathname, 'utf8');
- file.data = collectionBruToJson(bruContent);
+ file.data = await collectionBruToJson(bruContent);
+ hydrateBruCollectionFileWithUuid(file.data);
+ win.webContents.send('main:collection-tree-updated', 'change', file);
+ return;
+ } catch (err) {
+ console.error(err);
+ return;
+ }
+ }
+
+ if (path.basename(pathname) === 'folder.bru') {
+ const file = {
+ meta: {
+ collectionUid,
+ pathname,
+ name: path.basename(pathname),
+ folderRoot: true
+ }
+ };
+
+ try {
+ let bruContent = fs.readFileSync(pathname, 'utf8');
+
+ file.data = await collectionBruToJson(bruContent);
+
hydrateBruCollectionFileWithUuid(file.data);
win.webContents.send('main:collection-tree-updated', 'change', file);
return;
@@ -378,7 +438,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
};
const bru = fs.readFileSync(pathname, 'utf8');
- file.data = bruToJson(bru);
+ file.data = await bruToJson(bru);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'change', file);
@@ -407,27 +467,38 @@ const unlink = (win, pathname, collectionUid, collectionPath) => {
}
};
-const unlinkDir = (win, pathname, collectionUid, collectionPath) => {
+const unlinkDir = async (win, pathname, collectionUid, collectionPath) => {
const envDirectory = path.join(collectionPath, 'environments');
if (pathname === envDirectory) {
return;
}
+
+ const folderBruFilePath = path.join(pathname, `folder.bru`);
+
+ let name = path.basename(pathname);
+
+ if (fs.existsSync(folderBruFilePath)) {
+ let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8');
+ let folderBruData = await collectionBruToJson(folderBruFileContent);
+ name = folderBruData?.meta?.name || name;
+ }
+
const directory = {
meta: {
collectionUid,
pathname,
- name: path.basename(pathname)
+ name
}
};
win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);
};
-const onWatcherSetupComplete = (win, collectionPath) => {
+const onWatcherSetupComplete = (win, watchPath) => {
const UiStateSnapshotStore = new UiStateSnapshot();
const collectionsSnapshotState = UiStateSnapshotStore.getCollections();
- const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == collectionPath);
+ const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == watchPath);
win.webContents.send('main:hydrate-app-with-ui-state-snapshot', collectionSnapshotState);
};
@@ -436,7 +507,7 @@ class Watcher {
this.watchers = {};
}
- addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false) {
+ addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false, useWorkerThread) {
if (this.watchers[watchPath]) {
this.watchers[watchPath].close();
}
@@ -445,14 +516,13 @@ class Watcher {
setTimeout(() => {
const watcher = chokidar.watch(watchPath, {
ignoreInitial: false,
- usePolling: watchPath.startsWith('\\\\') || forcePolling ? true : false,
+ usePolling: isWSLPath(watchPath) || forcePolling ? true : false,
ignored: (filepath) => {
- const normalizedPath = isWSLPath(filepath) ? normalizeWslPath(filepath) : normalizeAndResolvePath(filepath);
+ const normalizedPath = normalizeAndResolvePath(filepath);
const relativePath = path.relative(watchPath, normalizedPath);
return ignores.some((ignorePattern) => {
- const normalizedIgnorePattern = isWSLPath(ignorePattern) ? normalizeWslPath(ignorePattern) : ignorePattern.replace(/\\/g, '/');
- return relativePath === normalizedIgnorePattern || relativePath.startsWith(normalizedIgnorePattern);
+ return relativePath === ignorePattern || relativePath.startsWith(ignorePattern);
});
},
persistent: true,
@@ -467,7 +537,7 @@ class Watcher {
let startedNewWatcher = false;
watcher
.on('ready', () => onWatcherSetupComplete(win, watchPath))
- .on('add', (pathname) => add(win, pathname, collectionUid, watchPath))
+ .on('add', (pathname) => add(win, pathname, collectionUid, watchPath, useWorkerThread))
.on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))
.on('change', (pathname) => change(win, pathname, collectionUid, watchPath))
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
@@ -488,7 +558,7 @@ class Watcher {
'Update you system config to allow more concurrently watched files with:',
'"echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"'
);
- this.addWatcher(win, watchPath, collectionUid, brunoConfig, true);
+ this.addWatcher(win, watchPath, collectionUid, brunoConfig, true, useWorkerThread);
} else {
console.error(`An error occurred in the watcher for: ${watchPath}`, error);
}
diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js
index 6b7d016fc..d41c980d7 100644
--- a/packages/bruno-electron/src/bru/index.js
+++ b/packages/bruno-electron/src/bru/index.js
@@ -7,10 +7,13 @@ const {
collectionBruToJson: _collectionBruToJson,
jsonToCollectionBru: _jsonToCollectionBru
} = require('@usebruno/lang');
+const BruParserWorker = require('./workers');
-const collectionBruToJson = (bru) => {
+const bruParserWorker = new BruParserWorker();
+
+const collectionBruToJson = async (data, parsed = false) => {
try {
- const json = _collectionBruToJson(bru);
+ const json = parsed ? data : _collectionBruToJson(data);
const transformedJson = {
request: {
@@ -38,7 +41,7 @@ const collectionBruToJson = (bru) => {
}
};
-const jsonToCollectionBru = (json, isFolder) => {
+const jsonToCollectionBru = async (json, isFolder) => {
try {
const collectionBruJson = {
headers: _.get(json, 'request.headers', []),
@@ -70,7 +73,7 @@ const jsonToCollectionBru = (json, isFolder) => {
}
};
-const bruToEnvJson = (bru) => {
+const bruToEnvJson = async (bru) => {
try {
const json = bruToEnvJsonV2(bru);
@@ -87,7 +90,7 @@ const bruToEnvJson = (bru) => {
}
};
-const envJsonToBru = (json) => {
+const envJsonToBru = async (json) => {
try {
const bru = envJsonToBruV2(json);
return bru;
@@ -102,12 +105,12 @@ const envJsonToBru = (json) => {
* We map the json response from the bru lang and transform it into the DSL
* format that the app uses
*
- * @param {string} bru The BRU file content.
+ * @param {string} data The BRU file content.
* @returns {object} The JSON representation of the BRU file.
*/
-const bruToJson = (bru) => {
+const bruToJson = (data, parsed = false) => {
try {
- const json = bruToJsonV2(bru);
+ const json = parsed ? data : bruToJsonV2(data);
let requestType = _.get(json, 'meta.type');
if (requestType === 'http') {
@@ -146,6 +149,16 @@ const bruToJson = (bru) => {
return Promise.reject(e);
}
};
+
+const bruToJsonViaWorker = async (data) => {
+ try {
+ const json = await bruParserWorker?.bruToJson(data);
+ return bruToJson(json, true);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+};
+
/**
* The transformer function for converting a JSON to BRU file.
*
@@ -155,7 +168,7 @@ const bruToJson = (bru) => {
* @param {object} json The JSON representation of the BRU file.
* @returns {string} The BRU file content.
*/
-const jsonToBru = (json) => {
+const jsonToBru = async (json) => {
let type = _.get(json, 'type');
if (type === 'http-request') {
type = 'http';
@@ -192,14 +205,59 @@ const jsonToBru = (json) => {
docs: _.get(json, 'request.docs', '')
};
- return jsonToBruV2(bruJson);
+ const bru = jsonToBruV2(bruJson);
+ return bru;
};
+const jsonToBruViaWorker = async (json) => {
+ let type = _.get(json, 'type');
+ if (type === 'http-request') {
+ type = 'http';
+ } else if (type === 'graphql-request') {
+ type = 'graphql';
+ } else {
+ type = 'http';
+ }
+
+ const sequence = _.get(json, 'seq');
+ const bruJson = {
+ meta: {
+ name: _.get(json, 'name'),
+ type: type,
+ seq: !isNaN(sequence) ? Number(sequence) : 1
+ },
+ http: {
+ method: _.lowerCase(_.get(json, 'request.method')),
+ url: _.get(json, 'request.url'),
+ auth: _.get(json, 'request.auth.mode', 'none'),
+ body: _.get(json, 'request.body.mode', 'none')
+ },
+ params: _.get(json, 'request.params', []),
+ headers: _.get(json, 'request.headers', []),
+ auth: _.get(json, 'request.auth', {}),
+ body: _.get(json, 'request.body', {}),
+ script: _.get(json, 'request.script', {}),
+ vars: {
+ req: _.get(json, 'request.vars.req', []),
+ res: _.get(json, 'request.vars.res', [])
+ },
+ assertions: _.get(json, 'request.assertions', []),
+ tests: _.get(json, 'request.tests', ''),
+ docs: _.get(json, 'request.docs', '')
+ };
+
+ const bru = await bruParserWorker?.jsonToBru(bruJson)
+ return bru;
+};
+
+
module.exports = {
bruToJson,
+ bruToJsonViaWorker,
jsonToBru,
bruToEnvJson,
envJsonToBru,
collectionBruToJson,
- jsonToCollectionBru
+ jsonToCollectionBru,
+ jsonToBruViaWorker
};
diff --git a/packages/bruno-electron/src/bru/workers/index.js b/packages/bruno-electron/src/bru/workers/index.js
new file mode 100644
index 000000000..51030b9ed
--- /dev/null
+++ b/packages/bruno-electron/src/bru/workers/index.js
@@ -0,0 +1,64 @@
+const { sizeInMB } = require("../../utils/filesystem");
+const WorkerQueue = require("../../workers");
+const path = require("path");
+
+const getSize = (data) => {
+ return sizeInMB(typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : Buffer.byteLength(JSON.stringify(data), 'utf8'));
+}
+
+/**
+ * Lanes are used to determine which worker queue to use based on the size of the data.
+ *
+ * The first lane is for smaller files (<0.1MB), the second lane is for larger files (>=0.1MB).
+ * This helps with parsing performance.
+ */
+const LANES = [{
+ maxSize: 0.005
+},{
+ maxSize: 0.1
+},{
+ maxSize: 1
+},{
+ maxSize: 10
+},{
+ maxSize: 100
+}];
+
+class BruParserWorker {
+ constructor() {
+ this.workerQueues = LANES?.map(lane => ({
+ maxSize: lane?.maxSize,
+ workerQueue: new WorkerQueue()
+ }));
+ }
+
+ getWorkerQueue(size) {
+ // Find the first queue that can handle the given size
+ // or fallback to the last queue for largest files
+ const queueForSize = this.workerQueues.find((queue) =>
+ queue.maxSize >= size
+ );
+
+ return queueForSize?.workerQueue ?? this.workerQueues.at(-1).workerQueue;
+ }
+
+ async enqueueTask({data, scriptFile }) {
+ const size = getSize(data);
+ const workerQueue = this.getWorkerQueue(size);
+ return workerQueue.enqueue({
+ data,
+ priority: size,
+ scriptPath: path.join(__dirname, `./scripts/${scriptFile}.js`)
+ });
+ }
+
+ async bruToJson(data) {
+ return this.enqueueTask({ data, scriptFile: `bru-to-json` });
+ }
+
+ async jsonToBru(data) {
+ return this.enqueueTask({ data, scriptFile: `json-to-bru` });
+ }
+}
+
+module.exports = BruParserWorker;
\ No newline at end of file
diff --git a/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js b/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js
new file mode 100644
index 000000000..92086c4b6
--- /dev/null
+++ b/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js
@@ -0,0 +1,16 @@
+const { parentPort } = require('worker_threads');
+const {
+ bruToJsonV2,
+} = require('@usebruno/lang');
+
+parentPort.on('message', (workerData) => {
+ try {
+ const bru = workerData;
+ const json = bruToJsonV2(bru);
+ parentPort.postMessage(json);
+ }
+ catch(error) {
+ console.error(error);
+ parentPort.postMessage({ error: error?.message });
+ }
+});
\ No newline at end of file
diff --git a/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js b/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js
new file mode 100644
index 000000000..c2a4f88e4
--- /dev/null
+++ b/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js
@@ -0,0 +1,16 @@
+const { parentPort } = require('worker_threads');
+const {
+ jsonToBruV2,
+} = require('@usebruno/lang');
+
+parentPort.on('message', (workerData) => {
+ try {
+ const json = workerData;
+ const bru = jsonToBruV2(json);
+ parentPort.postMessage(bru);
+ }
+ catch(error) {
+ console.error(error);
+ parentPort.postMessage({ error: error?.message });
+ }
+});
\ No newline at end of file
diff --git a/packages/bruno-electron/src/cache/requestUids.js b/packages/bruno-electron/src/cache/requestUids.js
index 55f7fc291..c6c3bfe7c 100644
--- a/packages/bruno-electron/src/cache/requestUids.js
+++ b/packages/bruno-electron/src/cache/requestUids.js
@@ -6,7 +6,7 @@
* In the past, we used to generate unique ids based on the
* pathname of the request, but we faced problems when implementing
* functionality where the user can move the request to a different
- * location. In that case, the uid would change, and the we would
+ * location. In that case, the uid would change, and we would
* lose the request's draft state if the user has made some changes
*/
diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js
index 4b6494b2f..522df6c68 100644
--- a/packages/bruno-electron/src/index.js
+++ b/packages/bruno-electron/src/index.js
@@ -30,9 +30,9 @@ const lastOpenedCollections = new LastOpenedCollections();
// Reference: https://content-security-policy.com/
const contentSecurityPolicy = [
"default-src 'self'",
- "script-src * 'unsafe-inline' 'unsafe-eval'",
- "connect-src * 'unsafe-inline'",
+ "connect-src 'self' https://*.posthog.com",
"font-src 'self' https:",
+ "frame-src data:",
// this has been commented out to make oauth2 work
// "form-action 'none'",
// we make an exception and allow http for images so that
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index fc435c7ec..5b8318ed6 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -4,10 +4,9 @@ const fsExtra = require('fs-extra');
const os = require('os');
const path = require('path');
const { ipcMain, shell, dialog, app } = require('electron');
-const { envJsonToBru, bruToJson, jsonToBru, jsonToCollectionBru } = require('../bru');
+const { envJsonToBru, bruToJson, jsonToBru, jsonToBruViaWorker, collectionBruToJson, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru');
const {
- isValidPathname,
writeFile,
hasBruExtension,
isDirectory,
@@ -15,20 +14,20 @@ const {
browseFiles,
createDirectory,
searchForBruFiles,
- sanitizeDirectoryName,
+ sanitizeName,
isWSLPath,
- normalizeWslPath,
- normalizeAndResolvePath,
safeToRename,
- sanitizeCollectionName,
isWindowsOS,
- isValidFilename,
+ validateName,
hasSubDirectories,
+ getCollectionStats,
+ sizeInMB,
+ safeWriteFileSync
} = require('../utils/filesystem');
const { openCollectionDialog } = require('../app/collections');
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
-const { deleteCookiesForDomain, getDomainsWithCookies } = require('../utils/cookies');
+const { deleteCookiesForDomain, getDomainsWithCookies, addCookieForDomain, modifyCookieForDomain, parseCookieString, createCookieString, deleteCookie } = require('../utils/cookies');
const EnvironmentSecretsStore = require('../store/env-secrets');
const CollectionSecurityStore = require('../store/collection-security');
const UiStateSnapshotStore = require('../store/ui-state-snapshot');
@@ -38,11 +37,17 @@ const { getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../u
const { getProcessEnvVars } = require('../store/process-env');
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, refreshOauth2Token } = require('../utils/oauth2');
const { configureRequestWithCertsAndProxy } = require('./network');
+const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection');
const environmentSecretsStore = new EnvironmentSecretsStore();
const collectionSecurityStore = new CollectionSecurityStore();
const uiStateSnapshotStore = new UiStateSnapshotStore();
+// size and file count limits to determine whether the bru files in the collection should be loaded asynchronously or not.
+const MAX_COLLECTION_SIZE_IN_MB = 20;
+const MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB = 5;
+const MAX_COLLECTION_FILES_COUNT = 2000;
+
const envHasSecrets = (environment = {}) => {
const secrets = _.filter(environment.variables, (v) => v.secret);
@@ -60,13 +65,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
});
// browse directory for file
- ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters) => {
+ ipcMain.handle('renderer:browse-files', async (_, filters, properties) => {
try {
- const filePaths = await browseFiles(mainWindow, filters);
-
- return filePaths;
+ return await browseFiles(mainWindow, filters, properties);
} catch (error) {
- return Promise.reject(error);
+ throw error;
}
});
@@ -75,8 +78,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
'renderer:create-collection',
async (event, collectionName, collectionFolderName, collectionLocation) => {
try {
- collectionFolderName = sanitizeDirectoryName(collectionFolderName);
- collectionName = sanitizeCollectionName(collectionName);
+ collectionFolderName = sanitizeName(collectionFolderName);
const dirPath = path.join(collectionLocation, collectionFolderName);
if (fs.existsSync(dirPath)) {
const files = fs.readdirSync(dirPath);
@@ -85,7 +87,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`collection: ${dirPath} already exists and is not empty`);
}
}
- if (!isValidPathname(path.basename(dirPath))) {
+
+ if (!validateName(path.basename(dirPath))) {
throw new Error(`collection: invalid pathname - ${dirPath}`);
}
@@ -103,6 +106,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const content = await stringifyJson(brunoConfig);
await writeFile(path.join(dirPath, 'bruno.json'), content);
+ const { size, filesCount } = await getCollectionStats(dirPath);
+ brunoConfig.size = size;
+ brunoConfig.filesCount = filesCount;
+
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid, brunoConfig);
} catch (error) {
@@ -114,13 +121,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle(
'renderer:clone-collection',
async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => {
- collectionFolderName = sanitizeCollectionName(collectionFolderName);
+ collectionFolderName = sanitizeName(collectionFolderName);
const dirPath = path.join(collectionLocation, collectionFolderName);
if (fs.existsSync(dirPath)) {
throw new Error(`collection: ${dirPath} already exists`);
}
- if (!isValidPathname(path.basename(dirPath))) {
+ if (!validateName(path.basename(dirPath))) {
throw new Error(`collection: invalid pathname - ${dirPath}`);
}
@@ -132,15 +139,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
const brunoJsonFilePath = path.join(previousPath, 'bruno.json');
const content = fs.readFileSync(brunoJsonFilePath, 'utf8');
- //Change new name of collection
- let json = JSON.parse(content);
- json.name = collectionName;
- const cont = await stringifyJson(json);
+ // Change new name of collection
+ let brunoConfig = JSON.parse(content);
+ brunoConfig.name = collectionName;
+ const cont = await stringifyJson(brunoConfig);
// write the bruno.json to new dir
await writeFile(path.join(dirPath, 'bruno.json'), cont);
- // Now copy all the files with extension name .bru along with there dir
+ // Now copy all the files with extension name .bru along with the dir
const files = searchForBruFiles(previousPath);
for (const sourceFilePath of files) {
@@ -153,14 +160,17 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
fs.copyFileSync(sourceFilePath, newFilePath);
}
- mainWindow.webContents.send('main:collection-opened', dirPath, uid, json);
+ const { size, filesCount } = await getCollectionStats(dirPath);
+ brunoConfig.size = size;
+ brunoConfig.filesCount = filesCount;
+
+ mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
}
);
// rename collection
ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => {
try {
- newName = sanitizeCollectionName(newName);
const brunoJsonFilePath = path.join(collectionPathname, 'bruno.json');
const content = fs.readFileSync(brunoJsonFilePath, 'utf8');
const json = JSON.parse(content);
@@ -190,7 +200,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
name: folderName
};
- const content = jsonToCollectionBru(
+ const content = await jsonToCollectionBru(
folderRoot,
true // isFolder
);
@@ -203,7 +213,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
try {
const collectionBruFilePath = path.join(collectionPathname, 'collection.bru');
- const content = jsonToCollectionBru(collectionRoot);
+ const content = await jsonToCollectionBru(collectionRoot);
await writeFile(collectionBruFilePath, content);
} catch (error) {
return Promise.reject(error);
@@ -216,10 +226,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
if (fs.existsSync(pathname)) {
throw new Error(`path: ${pathname} already exists`);
}
- if (!isValidFilename(request.name)) {
- throw new Error(`path: ${request.name}.bru is not a valid filename`);
+ // For the actual filename part, we want to be strict
+ if (!validateName(request?.filename)) {
+ throw new Error(`${request.filename}.bru is not a valid filename`);
}
- const content = jsonToBru(request);
+ const content = await jsonToBruViaWorker(request);
await writeFile(pathname, content);
} catch (error) {
return Promise.reject(error);
@@ -233,7 +244,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`path: ${pathname} does not exist`);
}
- const content = jsonToBru(request);
+ const content = await jsonToBruViaWorker(request);
await writeFile(pathname, content);
} catch (error) {
return Promise.reject(error);
@@ -251,7 +262,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`path: ${pathname} does not exist`);
}
- const content = jsonToBru(request);
+ const content = await jsonToBruViaWorker(request);
await writeFile(pathname, content);
}
} catch (error) {
@@ -281,7 +292,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
}
- const content = envJsonToBru(environment);
+ const content = await envJsonToBru(environment);
await writeFile(envFilePath, content);
} catch (error) {
@@ -306,7 +317,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);
}
- const content = envJsonToBru(environment);
+ const content = await envJsonToBru(environment);
await writeFile(envFilePath, content);
} catch (error) {
return Promise.reject(error);
@@ -353,18 +364,53 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
});
// rename item
- ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
- const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`);
- // const parentDir = path.dirname(oldPath);
- const isWindowsOSAndNotWSLAndItemHasSubDirectories = isDirectory(oldPath) && isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath);
- // let parentDirUnwatched = false;
- // let parentDirRewatched = false;
-
+ ipcMain.handle('renderer:rename-item-name', async (event, { itemPath, newName }) => {
try {
- // Normalize paths if they are WSL paths
- oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath);
- newPath = isWSLPath(newPath) ? normalizeWslPath(newPath) : normalizeAndResolvePath(newPath);
+ if (!fs.existsSync(itemPath)) {
+ throw new Error(`path: ${itemPath} does not exist`);
+ }
+
+ if (isDirectory(itemPath)) {
+ const folderBruFilePath = path.join(itemPath, 'folder.bru');
+ let folderBruFileJsonContent;
+ if (fs.existsSync(folderBruFilePath)) {
+ const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8');
+ folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent);
+ } else {
+ folderBruFileJsonContent = {};
+ }
+
+ folderBruFileJsonContent.meta = {
+ name: newName,
+ };
+
+ const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true);
+ await writeFile(folderBruFilePath, folderBruFileContent);
+
+ return;
+ }
+
+ const isBru = hasBruExtension(itemPath);
+ if (!isBru) {
+ throw new Error(`path: ${itemPath} is not a bru file`);
+ }
+
+ const data = fs.readFileSync(itemPath, 'utf8');
+ const jsonData = await bruToJson(data);
+ jsonData.name = newName;
+ const content = await jsonToBru(jsonData);
+ await writeFile(itemPath, content);
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
+ // rename item
+ ipcMain.handle('renderer:rename-item-filename', async (event, { oldPath, newPath, newName, newFilename }) => {
+ const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`);
+ const isWindowsOSAndNotWSLPathAndItemHasSubDirectories = isDirectory(oldPath) && isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath);
+ try {
// Check if the old path exists
if (!fs.existsSync(oldPath)) {
throw new Error(`path: ${oldPath} does not exist`);
@@ -375,6 +421,22 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
if (isDirectory(oldPath)) {
+ const folderBruFilePath = path.join(oldPath, 'folder.bru');
+ let folderBruFileJsonContent;
+ if (fs.existsSync(folderBruFilePath)) {
+ const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8');
+ folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent);
+ } else {
+ folderBruFileJsonContent = {};
+ }
+
+ folderBruFileJsonContent.meta = {
+ name: newName,
+ };
+
+ const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true);
+ await writeFile(folderBruFilePath, folderBruFileContent);
+
const bruFilesAtSource = await searchForBruFiles(oldPath);
for (let bruFile of bruFilesAtSource) {
@@ -382,19 +444,16 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
moveRequestUid(bruFile, newBruFilePath);
}
- // watcher.unlinkItemPathInWatcher(parentDir);
- // parentDirUnwatched = true;
-
/**
* If it is windows OS
- * And it is not WSL path (meaning its not linux running on windows using WSL)
+ * And it is not a WSL path (meaning it is not running in WSL (linux pathtype))
* And it has sub directories
* Only then we need to use the temp dir approach to rename the folder
- *
+ *
* Windows OS would sometimes throw error when renaming a folder with sub directories
- * This is a alternative approach to avoid that error
+ * This is an alternative approach to avoid that error
*/
- if (isWindowsOSAndNotWSLAndItemHasSubDirectories) {
+ if (isWindowsOSAndNotWSLPathAndItemHasSubDirectories) {
await fsExtra.copy(oldPath, tempDir);
await fsExtra.remove(oldPath);
await fsExtra.move(tempDir, newPath, { overwrite: true });
@@ -402,8 +461,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
} else {
await fs.renameSync(oldPath, newPath);
}
- // watcher.addItemPathInWatcher(parentDir);
- // parentDirRewatched = true;
return newPath;
}
@@ -412,31 +469,25 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`path: ${oldPath} is not a bru file`);
}
- if (!isValidFilename(newName)) {
- throw new Error(`path: ${newName} is not a valid filename`);
+ if (!validateName(newFilename)) {
+ throw new Error(`path: ${newFilename} is not a valid filename`);
}
// update name in file and save new copy, then delete old copy
const data = await fs.promises.readFile(oldPath, 'utf8'); // Use async read
- const jsonData = bruToJson(data);
+ const jsonData = await bruToJsonViaWorker(data);
jsonData.name = newName;
moveRequestUid(oldPath, newPath);
- const content = jsonToBru(jsonData);
+ const content = await jsonToBruViaWorker(jsonData);
await fs.promises.unlink(oldPath);
await writeFile(newPath, content);
return newPath;
} catch (error) {
- // in case an error occurs during the rename file operations after unlinking the parent dir
- // and the rewatch fails, we need to add it back to watcher
- // if (parentDirUnwatched && !parentDirRewatched) {
- // watcher.addItemPathInWatcher(parentDir);
- // }
-
// in case the rename file operations fails, and we see that the temp dir exists
// and the old path does not exist, we need to restore the data from the temp dir to the old path
- if (isWindowsOSAndNotWSLAndItemHasSubDirectories) {
+ if (isWindowsOSAndNotWSLPathAndItemHasSubDirectories) {
if (fsExtra.pathExistsSync(tempDir) && !fsExtra.pathExistsSync(oldPath)) {
try {
await fsExtra.copy(tempDir, oldPath);
@@ -452,12 +503,20 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
});
// new folder
- ipcMain.handle('renderer:new-folder', async (event, pathname) => {
- const resolvedFolderName = sanitizeDirectoryName(path.basename(pathname));
+ ipcMain.handle('renderer:new-folder', async (event, pathname, folderName) => {
+ const resolvedFolderName = sanitizeName(path.basename(pathname));
pathname = path.join(path.dirname(pathname), resolvedFolderName);
try {
if (!fs.existsSync(pathname)) {
fs.mkdirSync(pathname);
+ const folderBruFilePath = path.join(pathname, 'folder.bru');
+ let data = {
+ meta: {
+ name: folderName,
+ }
+ };
+ const content = await jsonToCollectionBru(data, true); // isFolder flag
+ await writeFile(folderBruFilePath, content);
} else {
return Promise.reject(new Error('The directory already exists'));
}
@@ -511,9 +570,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
+ ipcMain.handle('renderer:update-collection-paths', async (_, collectionPaths) => {
+ lastOpenedCollections.update(collectionPaths);
+ })
+
ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => {
try {
- let collectionName = sanitizeCollectionName(collection.name);
+ let collectionName = sanitizeName(collection.name);
let collectionPath = path.join(collectionLocation, collectionName);
if (fs.existsSync(collectionPath)) {
@@ -522,24 +585,25 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// Recursive function to parse the collection items and create files/folders
const parseCollectionItems = (items = [], currentPath) => {
- items.forEach((item) => {
+ items.forEach(async (item) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
- const content = jsonToBru(item);
- const filePath = path.join(currentPath, `${item.name}.bru`);
- fs.writeFileSync(filePath, content);
+ let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.bru`);
+ const content = await jsonToBruViaWorker(item);
+ const filePath = path.join(currentPath, sanitizedFilename);
+ safeWriteFileSync(filePath, content);
}
if (item.type === 'folder') {
- item.name = sanitizeDirectoryName(item.name);
- const folderPath = path.join(currentPath, item.name);
+ let sanitizedFolderName = sanitizeName(item?.filename || item?.name);
+ const folderPath = path.join(currentPath, sanitizedFolderName);
fs.mkdirSync(folderPath);
if (item?.root?.meta?.name) {
const folderBruFilePath = path.join(folderPath, 'folder.bru');
- const folderContent = jsonToCollectionBru(
+ const folderContent = await jsonToCollectionBru(
item.root,
true // isFolder
);
- fs.writeFileSync(folderBruFilePath, folderContent);
+ safeWriteFileSync(folderBruFilePath, folderContent);
}
if (item.items && item.items.length) {
@@ -548,8 +612,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
// Handle items of type 'js'
if (item.type === 'js') {
- const filePath = path.join(currentPath, `${item.name}.js`);
- fs.writeFileSync(filePath, item.fileContent);
+ let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.js`);
+ const filePath = path.join(currentPath, sanitizedFilename);
+ safeWriteFileSync(filePath, item.fileContent);
}
});
};
@@ -560,10 +625,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
fs.mkdirSync(envDirPath);
}
- environments.forEach((env) => {
- const content = envJsonToBru(env);
- const filePath = path.join(envDirPath, `${env.name}.bru`);
- fs.writeFileSync(filePath, content);
+ environments.forEach(async (env) => {
+ const content = await envJsonToBru(env);
+ let sanitizedEnvFilename = sanitizeName(`${env.name}.bru`);
+ const filePath = path.join(envDirPath, sanitizedEnvFilename);
+ safeWriteFileSync(filePath, content);
});
};
@@ -585,15 +651,19 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
await createDirectory(collectionPath);
const uid = generateUidBasedOnHash(collectionPath);
- const brunoConfig = getBrunoJsonConfig(collection);
+ let brunoConfig = getBrunoJsonConfig(collection);
const stringifiedBrunoConfig = await stringifyJson(brunoConfig);
// Write the Bruno configuration to a file
await writeFile(path.join(collectionPath, 'bruno.json'), stringifiedBrunoConfig);
- const collectionContent = jsonToCollectionBru(collection.root);
+ const collectionContent = await jsonToCollectionBru(collection.root);
await writeFile(path.join(collectionPath, 'collection.bru'), collectionContent);
+ const { size, filesCount } = await getCollectionStats(collectionPath);
+ brunoConfig.size = size;
+ brunoConfig.filesCount = filesCount;
+
mainWindow.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);
ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid, brunoConfig);
@@ -615,22 +685,23 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// Recursive function to parse the folder and create files/folders
const parseCollectionItems = (items = [], currentPath) => {
- items.forEach((item) => {
+ items.forEach(async (item) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
- const content = jsonToBru(item);
- const filePath = path.join(currentPath, `${item.name}.bru`);
- fs.writeFileSync(filePath, content);
+ const content = await jsonToBruViaWorker(item);
+ const filePath = path.join(currentPath, item.filename);
+ safeWriteFileSync(filePath, content);
}
if (item.type === 'folder') {
- const folderPath = path.join(currentPath, item.name);
+ const folderPath = path.join(currentPath, item.filename);
fs.mkdirSync(folderPath);
// If folder has a root element, then I should write its folder.bru file
if (item.root) {
- const folderContent = jsonToCollectionBru(item.root, true);
+ const folderContent = await jsonToCollectionBru(item.root, true);
+ folderContent.name = item.name;
if (folderContent) {
const bruFolderPath = path.join(folderPath, `folder.bru`);
- fs.writeFileSync(bruFolderPath, folderContent);
+ safeWriteFileSync(bruFolderPath, folderContent);
}
}
@@ -645,10 +716,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// If initial folder has a root element, then I should write its folder.bru file
if (itemFolder.root) {
- const folderContent = jsonToCollectionBru(itemFolder.root, true);
+ const folderContent = await jsonToCollectionBru(itemFolder.root, true);
if (folderContent) {
const bruFolderPath = path.join(collectionPath, `folder.bru`);
- fs.writeFileSync(bruFolderPath, folderContent);
+ safeWriteFileSync(bruFolderPath, folderContent);
}
}
@@ -661,13 +732,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:resequence-items', async (event, itemsToResequence) => {
try {
- for (let item of itemsToResequence) {
+ for await (let item of itemsToResequence) {
const bru = fs.readFileSync(item.pathname, 'utf8');
- const jsonData = bruToJson(bru);
+ const jsonData = await bruToJsonViaWorker(bru);
if (jsonData.seq !== item.seq) {
jsonData.seq = item.seq;
- const content = jsonToBru(jsonData);
+ const content = await jsonToBruViaWorker(jsonData);
await writeFile(item.pathname, content);
}
}
@@ -684,7 +755,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
moveRequestUid(itemPath, newItemPath);
fs.unlinkSync(itemPath);
- fs.writeFileSync(newItemPath, itemContent);
+ safeWriteFileSync(newItemPath, itemContent);
} catch (error) {
return Promise.reject(error);
}
@@ -757,6 +828,54 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
+ ipcMain.handle('renderer:delete-cookie', async (event, domain, path, cookieKey) => {
+ try {
+ await deleteCookie(domain, path, cookieKey);
+ const domainsWithCookies = await getDomainsWithCookies();
+ mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
+ // add cookie
+ ipcMain.handle('renderer:add-cookie', async (event, domain, cookie) => {
+ try {
+ await addCookieForDomain(domain, cookie);
+ const domainsWithCookies = await getDomainsWithCookies();
+ mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
+ // modify cookie
+ ipcMain.handle('renderer:modify-cookie', async (event, domain, oldCookie, cookie) => {
+ try {
+ await modifyCookieForDomain(domain, oldCookie, cookie);
+ const domainsWithCookies = await getDomainsWithCookies();
+ mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
+ ipcMain.handle('renderer:get-parsed-cookie', async (event, cookieStr) => {
+ try {
+ return parseCookieString(cookieStr);
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
+ ipcMain.handle('renderer:create-cookie-string', async (event, cookie) => {
+ try {
+ return createCookieString(cookie);
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
ipcMain.handle('renderer:save-collection-security-config', async (event, collectionPath, securityConfig) => {
try {
collectionSecurityStore.setSecurityConfigForCollection(collectionPath, {
@@ -840,6 +959,55 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
+ ipcMain.handle('renderer:load-request-via-worker', async (event, { collectionUid, pathname }) => {
+ let fileStats;
+ try {
+ fileStats = fs.statSync(pathname);
+ if (hasBruExtension(pathname)) {
+ const file = {
+ meta: {
+ collectionUid,
+ pathname,
+ name: path.basename(pathname)
+ }
+ };
+ let bruContent = fs.readFileSync(pathname, 'utf8');
+ const metaJson = await bruToJson(parseBruFileMeta(bruContent), true);
+ file.data = metaJson;
+ file.loading = true;
+ file.partial = true;
+ file.size = sizeInMB(fileStats?.size);
+ hydrateRequestWithUuid(file.data, pathname);
+ mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
+ file.data = await bruToJsonViaWorker(bruContent);
+ file.partial = false;
+ file.loading = true;
+ file.size = sizeInMB(fileStats?.size);
+ hydrateRequestWithUuid(file.data, pathname);
+ mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
+ }
+ } catch (error) {
+ if (hasBruExtension(pathname)) {
+ const file = {
+ meta: {
+ collectionUid,
+ pathname,
+ name: path.basename(pathname)
+ }
+ };
+ let bruContent = fs.readFileSync(pathname, 'utf8');
+ const metaJson = await bruToJson(parseBruFileMeta(bruContent), true);
+ file.data = metaJson;
+ file.partial = true;
+ file.loading = false;
+ file.size = sizeInMB(fileStats?.size);
+ hydrateRequestWithUuid(file.data, pathname);
+ mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
+ }
+ return Promise.reject(error);
+ }
+ });
+
ipcMain.handle('renderer:refresh-oauth2-credentials', async (event, { request, collection }) => {
try {
if (request.oauth2) {
@@ -865,6 +1033,82 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
return Promise.reject(error);
}
});
+
+ ipcMain.handle('renderer:load-request', async (event, { collectionUid, pathname }) => {
+ let fileStats;
+ try {
+ fileStats = fs.statSync(pathname);
+ if (hasBruExtension(pathname)) {
+ const file = {
+ meta: {
+ collectionUid,
+ pathname,
+ name: path.basename(pathname)
+ }
+ };
+ let bruContent = fs.readFileSync(pathname, 'utf8');
+ const metaJson = await bruToJson(parseBruFileMeta(bruContent), true);
+ file.data = metaJson;
+ file.loading = true;
+ file.partial = true;
+ file.size = sizeInMB(fileStats?.size);
+ hydrateRequestWithUuid(file.data, pathname);
+ mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
+ file.data = bruToJson(bruContent);
+ file.partial = false;
+ file.loading = true;
+ file.size = sizeInMB(fileStats?.size);
+ hydrateRequestWithUuid(file.data, pathname);
+ mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
+ }
+ } catch (error) {
+ if (hasBruExtension(pathname)) {
+ const file = {
+ meta: {
+ collectionUid,
+ pathname,
+ name: path.basename(pathname)
+ }
+ };
+ let bruContent = fs.readFileSync(pathname, 'utf8');
+ const metaJson = await bruToJson(parseBruFileMeta(bruContent), true);
+ file.data = metaJson;
+ file.partial = true;
+ file.loading = false;
+ file.size = sizeInMB(fileStats?.size);
+ hydrateRequestWithUuid(file.data, pathname);
+ mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
+ }
+ return Promise.reject(error);
+ }
+ });
+
+ ipcMain.handle('renderer:mount-collection', async (event, { collectionUid, collectionPathname, brunoConfig }) => {
+ const {
+ size,
+ filesCount,
+ maxFileSize
+ } = await getCollectionStats(collectionPathname);
+
+ const shouldLoadCollectionAsync =
+ (size > MAX_COLLECTION_SIZE_IN_MB) ||
+ (filesCount > MAX_COLLECTION_FILES_COUNT) ||
+ (maxFileSize > MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB);
+
+ watcher.addWatcher(mainWindow, collectionPathname, collectionUid, brunoConfig, false, shouldLoadCollectionAsync);
+ });
+
+ ipcMain.handle('renderer:show-in-folder', async (event, filePath) => {
+ try {
+ if (!filePath) {
+ throw new Error('File path is required');
+ }
+ shell.showItemInFolder(filePath);
+ } catch (error) {
+ console.error('Error in show-in-folder: ', error);
+ throw error;
+ }
+ });
};
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
@@ -879,8 +1123,7 @@ const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) =
shell.openExternal(docsURL);
});
- ipcMain.on('main:collection-opened', (win, pathname, uid, brunoConfig) => {
- watcher.addWatcher(win, pathname, uid, brunoConfig);
+ ipcMain.on('main:collection-opened', async (win, pathname, uid, brunoConfig) => {
lastOpenedCollections.add(pathname);
app.addRecentDocument(pathname);
});
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index e68c0affc..4d491873e 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -54,37 +54,6 @@ const getJsSandboxRuntime = (collection) => {
return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2';
};
-const parseDataFromResponse = (response, disableParsingResponseJson = false) => {
- // Parse the charset from content type: https://stackoverflow.com/a/33192813
- const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || '');
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals
- const charsetValue = charsetMatch?.[1];
- const dataBuffer = Buffer.from(response.data);
- // Overwrite the original data for backwards compatibility
- let data;
- if (iconv.encodingExists(charsetValue)) {
- data = iconv.decode(dataBuffer, charsetValue);
- } else {
- data = iconv.decode(dataBuffer, 'utf-8');
- }
- // Try to parse response to JSON, this can quietly fail
- try {
- // Filter out ZWNBSP character
- // https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
- data = data.replace(/^\uFEFF/, '');
-
- // If the response is a string and starts and ends with double quotes, it's a stringified JSON and should not be parsed
- if ( !disableParsingResponseJson && ! (typeof data === 'string' && data.startsWith("\"") && data.endsWith("\""))) {
- data = Buffer?.isBuffer(data)? JSON.parse(data?.toString()) : JSON.parse(data);
- }
- } catch(error) {
- console.error(error);
- console.log('Failed to parse response data as JSON');
- }
-
- return { data, dataBuffer };
-};
-
const configureRequestWithCertsAndProxy = async ({
collectionUid,
request,
@@ -311,7 +280,30 @@ const configureRequest = async (
if (preferencesUtil.shouldSendCookies()) {
const cookieString = getCookieStringForUrl(request.url);
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
- request.headers['cookie'] = cookieString;
+ const existingCookieHeaderName = Object.keys(request.headers).find(
+ name => name.toLowerCase() === 'cookie'
+ );
+ const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';
+
+ // Helper function to parse cookies into an object
+ const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {
+ const [name, ...rest] = cookie.split('=');
+ if (name && name.trim()) {
+ cookies[name.trim()] = rest.join('=').trim();
+ }
+ return cookies;
+ }, {});
+
+ const mergedCookies = {
+ ...parseCookies(existingCookieString),
+ ...parseCookies(cookieString),
+ };
+
+ const combinedCookieString = Object.entries(mergedCookies)
+ .map(([name, value]) => `${name}=${value}`)
+ .join('; ');
+
+ request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;
}
}
@@ -336,6 +328,33 @@ const configureRequest = async (
return axiosInstance;
};
+const parseDataFromResponse = (response, disableParsingResponseJson = false) => {
+ // Parse the charset from content type: https://stackoverflow.com/a/33192813
+ const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || '');
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals
+ const charsetValue = charsetMatch?.[1];
+ const dataBuffer = Buffer.from(response.data);
+ // Overwrite the original data for backwards compatibility
+ let data;
+ if (iconv.encodingExists(charsetValue)) {
+ data = iconv.decode(dataBuffer, charsetValue);
+ } else {
+ data = iconv.decode(dataBuffer, 'utf-8');
+ }
+ // Try to parse response to JSON, this can quietly fail
+ try {
+ // Filter out ZWNBSP character
+ // https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
+ data = data.replace(/^\uFEFF/, '');
+ if (!disableParsingResponseJson) {
+ data = JSON.parse(data);
+ }
+ } catch { }
+
+ return { data, dataBuffer };
+};
+
+
const registerNetworkIpc = (mainWindow) => {
const onConsoleLog = (type, args) => {
console[type](...args);
@@ -351,7 +370,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
- collectionRoot,
+ collection,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -385,6 +404,8 @@ const registerNetworkIpc = (mainWindow) => {
mainWindow.webContents.send('main:global-environment-variables-update', {
globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
});
+
+ collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
}
// interpolate variables inside request
@@ -418,7 +439,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
- collectionRoot,
+ collection,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -455,6 +476,8 @@ const registerNetworkIpc = (mainWindow) => {
if (result?.error) {
mainWindow.webContents.send('main:display-error', result.error);
}
+
+ collection.globalEnvironmentVariables = result.globalEnvironmentVariables;
}
// run post-response script
@@ -485,11 +508,13 @@ const registerNetworkIpc = (mainWindow) => {
mainWindow.webContents.send('main:global-environment-variables-update', {
globalEnvironmentVariables: scriptResult.globalEnvironmentVariables
});
+
+ collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
}
return scriptResult;
};
- const runRequest = async ({ item, collection, environment, runtimeVariables, runInBackground = false }) => {
+ const runRequest = async ({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground = false }) => {
const collectionUid = collection.uid;
const collectionPath = collection.pathname;
const cancelTokenUid = uuid();
@@ -501,9 +526,9 @@ const registerNetworkIpc = (mainWindow) => {
if (itemPathname && !itemPathname?.endsWith('.bru')) {
itemPathname = `${itemPathname}.bru`;
}
- const _item = findItemInCollectionByPathname(collection, itemPathname);
+ const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));
if(_item) {
- const res = await runRequest({ item: _item, collection, environment, runtimeVariables, runInBackground: true });
+ const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
resolve(res);
}
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
@@ -518,34 +543,50 @@ const registerNetworkIpc = (mainWindow) => {
cancelTokenUid
});
- const collectionRoot = get(collection, 'root', {});
-
- const request = prepareRequest(item, collection);
+ const abortController = new AbortController();
+ const request = await prepareRequest(item, collection, abortController);
request.__bruno__executionMode = 'standalone';
- const envVars = getEnvVars(environment);
- const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
try {
- const controller = new AbortController();
- request.signal = controller.signal;
- saveCancelToken(cancelTokenUid, controller);
+ request.signal = abortController.signal;
+ saveCancelToken(cancelTokenUid, abortController);
- await runPreRequest(
- request,
- requestUid,
- envVars,
- collectionPath,
- collectionRoot,
- collectionUid,
- runtimeVariables,
- processEnvVars,
- scriptingConfig,
- runRequestByItemPathname
- );
+
+ try {
+ await runPreRequest(
+ request,
+ requestUid,
+ envVars,
+ collectionPath,
+ collection,
+ collectionUid,
+ runtimeVariables,
+ processEnvVars,
+ scriptingConfig,
+ runRequestByItemPathname
+ );
+ !runInBackground && mainWindow.webContents.send('main:run-request-event', {
+ type: 'pre-request-script-execution',
+ requestUid,
+ collectionUid,
+ itemUid: item.uid,
+ errorMessage: null,
+ });
+
+ } catch (error) {
+ !runInBackground && mainWindow.webContents.send('main:run-request-event', {
+ type: 'pre-request-script-execution',
+ requestUid,
+ collectionUid,
+ itemUid: item.uid,
+ errorMessage: error?.message || 'An error occurred in pre-request script',
+ });
+ return Promise.reject(error);
+ }
const axiosInstance = await configureRequest(
collectionUid,
request,
@@ -561,7 +602,7 @@ const registerNetworkIpc = (mainWindow) => {
url: request.url,
method: request.method,
headers: request.headers,
- data: safeParseJSON(safeStringifyJSON(request.data)),
+ data: request.mode == 'file'? "": safeParseJSON(safeStringifyJSON(request.data)) ,
timestamp: Date.now()
},
collectionUid,
@@ -628,19 +669,41 @@ const registerNetworkIpc = (mainWindow) => {
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
- await runPostResponse(
- request,
- response,
- requestUid,
- envVars,
- collectionPath,
- collectionRoot,
- collectionUid,
- runtimeVariables,
- processEnvVars,
- scriptingConfig,
- runRequestByItemPathname
- );
+ try {
+ await runPostResponse(
+ request,
+ response,
+ requestUid,
+ envVars,
+ collectionPath,
+ collection,
+ collectionUid,
+ runtimeVariables,
+ processEnvVars,
+ scriptingConfig,
+ runRequestByItemPathname
+ );
+ !runInBackground && mainWindow.webContents.send('main:run-request-event', {
+ type: 'post-response-script-execution',
+ requestUid,
+ collectionUid,
+ errorMessage: null,
+ itemUid: item.uid,
+ });
+ } catch (error) {
+ console.error('Post-response script error:', error);
+
+ // Format a more readable error message
+ const errorMessage = error?.message || 'An error occurred in post-response script';
+
+ !runInBackground && mainWindow.webContents.send('main:run-request-event', {
+ type: 'post-response-script-execution',
+ requestUid,
+ errorMessage,
+ collectionUid,
+ itemUid: item.uid,
+ });
+ }
// run assertions
const assertions = get(request, 'assertions');
@@ -719,7 +782,10 @@ const registerNetworkIpc = (mainWindow) => {
// handler for sending http request
ipcMain.handle('send-http-request', async (event, item, collection, environment, runtimeVariables) => {
- return await runRequest({ item, collection, environment, runtimeVariables });
+ const collectionUid = collection.uid;
+ const envVars = getEnvVars(environment);
+ const processEnvVars = getProcessEnvVars(collectionUid);
+ return await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false });
});
ipcMain.handle('clear-oauth2-cache', async (event, uid, url, credentialsId) => {
@@ -785,7 +851,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
- collectionRoot,
+ collection,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -809,7 +875,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
- collectionRoot,
+ collection,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -846,7 +912,8 @@ const registerNetworkIpc = (mainWindow) => {
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
- const collectionRoot = get(collection, 'root', {});
+ const envVars = getEnvVars(environment);
+ const processEnvVars = getProcessEnvVars(collectionUid);
let stopRunnerExecution = false;
const abortController = new AbortController();
@@ -858,9 +925,9 @@ const registerNetworkIpc = (mainWindow) => {
if (itemPathname && !itemPathname?.endsWith('.bru')) {
itemPathname = `${itemPathname}.bru`;
}
- const _item = findItemInCollectionByPathname(collection, itemPathname);
+ const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));
if(_item) {
- const res = await runRequest({ item: _item, collection, environment, runtimeVariables, runInBackground: true });
+ const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
resolve(res);
}
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
@@ -880,7 +947,6 @@ const registerNetworkIpc = (mainWindow) => {
});
try {
- const envVars = getEnvVars(environment);
let folderRequests = [];
if (recursive) {
@@ -928,11 +994,10 @@ const registerNetworkIpc = (mainWindow) => {
...eventData
});
- const request = prepareRequest(item, collection);
+ const request = await prepareRequest(item, collection, abortController);
request.__bruno__executionMode = 'runner';
const requestUid = uuid();
- const processEnvVars = getProcessEnvVars(collectionUid);
try {
const preRequestScriptResult = await runPreRequest(
@@ -940,7 +1005,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
- collectionRoot,
+ collection,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -1087,7 +1152,7 @@ const registerNetworkIpc = (mainWindow) => {
requestUid,
envVars,
collectionPath,
- collectionRoot,
+ collection,
collectionUid,
runtimeVariables,
processEnvVars,
@@ -1273,7 +1338,7 @@ const registerNetworkIpc = (mainWindow) => {
if (encoding === 'utf-8') {
await writeFile(filePath, data);
} else {
- await writeBinaryFile(filePath, data);
+ await writeFile(filePath, data, true);
}
}
} catch (error) {
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
index d7ab410e9..f4fb13bd5 100644
--- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js
+++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js
@@ -67,7 +67,11 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
const contentType = getContentType(request.headers);
- if (contentType.includes('json')) {
+ /*
+ We explicitly avoid interpolating buffer values because the file content is read as a buffer object in raw body mode.
+ Even if the selected file's content type is JSON, this prevents the buffer object from being interpolated.
+ */
+ if (contentType.includes('json') && !Buffer.isBuffer(request.data)) {
if (typeof request.data === 'string') {
if (request.data.length) {
request.data = _interpolate(request.data);
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index 18436d423..efc08cce6 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -1,8 +1,10 @@
-const { each, get, filter } = require('lodash');
+const { get, each, filter, find } = require('lodash');
const decomment = require('decomment');
const crypto = require('node:crypto');
+const fs = require('node:fs/promises');
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars, getFormattedCollectionOauth2Credentials, mergeAuth } = require('../../utils/collection');
-const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data');
+const { buildFormUrlEncodedPayload } = require('../../utils/form-data');
+const path = require('node:path');
const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
const collectionAuth = get(collectionRoot, 'request.auth');
@@ -258,10 +260,10 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
return axiosRequest;
};
-const prepareRequest = (item, collection) => {
+const prepareRequest = async (item, collection = {}, abortController) => {
const request = item.draft ? item.draft.request : item.request;
const collectionRoot = collection?.draft ? get(collection, 'draft', {}) : get(collection, 'root', {});
- const collectionPath = collection.pathname;
+ const collectionPath = collection?.pathname;
const headers = {};
let contentTypeDefined = false;
let url = request.url;
@@ -273,7 +275,7 @@ const prepareRequest = (item, collection) => {
}
});
- const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich';
+ const scriptFlow = collection?.brunoConfig?.scripts?.flow ?? 'sandwich';
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
if (requestTreePath && requestTreePath.length > 0) {
mergeHeaders(collection, request, requestTreePath);
@@ -337,6 +339,31 @@ const prepareRequest = (item, collection) => {
axiosRequest.data = request.body.sparql;
}
+ if (request.body.mode === 'file') {
+ if (!contentTypeDefined) {
+ axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads
+ }
+
+ const bodyFile = find(request.body.file, (param) => param.selected);
+ if (bodyFile) {
+ let { filePath, contentType } = bodyFile;
+
+ axiosRequest.headers['content-type'] = contentType;
+ if (filePath) {
+ if (!path.isAbsolute(filePath)) {
+ filePath = path.join(collectionPath, filePath);
+ }
+
+ try {
+ const fileContent = await fs.readFile(filePath);
+ axiosRequest.data = fileContent;
+ } catch (error) {
+ console.error('Error reading file:', error);
+ }
+ }
+ }
+ }
+
if (request.body.mode === 'formUrlEncoded') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
diff --git a/packages/bruno-electron/src/store/bruno-config.js b/packages/bruno-electron/src/store/bruno-config.js
index a092e9357..8942dd833 100644
--- a/packages/bruno-electron/src/store/bruno-config.js
+++ b/packages/bruno-electron/src/store/bruno-config.js
@@ -4,7 +4,7 @@
const config = {};
-// collectionUid is a hash based on the collection path)
+// collectionUid is a hash based on the collection path
const getBrunoConfig = (collectionUid) => {
return config[collectionUid] || {};
};
diff --git a/packages/bruno-electron/src/store/last-opened-collections.js b/packages/bruno-electron/src/store/last-opened-collections.js
index 546b73b57..72452eef3 100644
--- a/packages/bruno-electron/src/store/last-opened-collections.js
+++ b/packages/bruno-electron/src/store/last-opened-collections.js
@@ -16,18 +16,20 @@ class LastOpenedCollections {
}
add(collectionPath) {
- const collections = this.store.get('lastOpenedCollections') || [];
+ const collections = this.getAll();
- if (isDirectory(collectionPath)) {
- if (!collections.includes(collectionPath)) {
- collections.push(collectionPath);
- this.store.set('lastOpenedCollections', collections);
- }
+ if (isDirectory(collectionPath) && !collections.includes(collectionPath)) {
+ collections.push(collectionPath);
+ this.store.set('lastOpenedCollections', collections);
}
}
+ update(collectionPaths) {
+ this.store.set('lastOpenedCollections', collectionPaths);
+ }
+
remove(collectionPath) {
- let collections = this.store.get('lastOpenedCollections') || [];
+ let collections = this.getAll();
if (collections.includes(collectionPath)) {
collections = _.filter(collections, (c) => c !== collectionPath);
@@ -36,7 +38,7 @@ class LastOpenedCollections {
}
removeAll() {
- return this.store.set('lastOpenedCollections', []);
+ this.store.set('lastOpenedCollections', []);
}
}
diff --git a/packages/bruno-electron/src/store/process-env.js b/packages/bruno-electron/src/store/process-env.js
index 578d8df71..084187d2d 100644
--- a/packages/bruno-electron/src/store/process-env.js
+++ b/packages/bruno-electron/src/store/process-env.js
@@ -11,7 +11,7 @@
const dotEnvVars = {};
-// collectionUid is a hash based on the collection path)
+// collectionUid is a hash based on the collection path
const getProcessEnvVars = (collectionUid) => {
// if there are no .env vars for this collection, return the process.env
if (!dotEnvVars[collectionUid]) {
diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js
index 6cedc7e91..82b37f43d 100644
--- a/packages/bruno-electron/src/utils/collection.js
+++ b/packages/bruno-electron/src/utils/collection.js
@@ -1,4 +1,7 @@
const { get, each, find, compact, filter } = require('lodash');
+const fs = require('fs');
+const { getRequestUid } = require('../cache/requestUids');
+const { uuid } = require('./common');
const os = require('os');
const mergeHeaders = (collection, request, requestTreePath) => {
@@ -7,7 +10,7 @@ const mergeHeaders = (collection, request, requestTreePath) => {
let collectionHeaders = get(collection, 'root.request.headers', []);
collectionHeaders.forEach((header) => {
if (header.enabled) {
- headers.set(header.name, header.value);
+ headers.set(header.name?.toLowerCase?.(), header.value);
if (header?.name?.toLowerCase() === 'content-type') {
contentTypeDefined = true;
}
@@ -19,14 +22,14 @@ const mergeHeaders = (collection, request, requestTreePath) => {
let _headers = get(i, 'root.request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
- headers.set(header.name, header.value);
+ headers.set(header.name?.toLowerCase?.(), header.value);
}
});
} else {
const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
- headers.set(header.name, header.value);
+ headers.set(header.name?.toLowerCase?.(), header.value);
}
});
}
@@ -203,16 +206,55 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
return path;
};
-const slash = (path) => {
- const isExtendedLengthPath = /^\\\\\?\\/.test(path);
- if (isExtendedLengthPath) {
- return path;
+const parseBruFileMeta = (data) => {
+ try {
+ const metaRegex = /meta\s*{\s*([\s\S]*?)\s*}/;
+ const match = data?.match?.(metaRegex);
+ if (match) {
+ const metaContent = match[1].trim();
+ const lines = metaContent.replace(/\r\n/g, '\n').split('\n');
+ const metaJson = {};
+ lines.forEach(line => {
+ const [key, value] = line.split(':').map(str => str.trim());
+ if (key && value) {
+ metaJson[key] = isNaN(value) ? value : Number(value);
+ }
+ });
+ return { meta: metaJson };
+ } else {
+ console.log('No "meta" block found in the file.');
+ }
+ } catch (err) {
+ console.error('Error reading file:', err);
}
- return path?.replace?.(/\\/g, '/');
+}
+
+const hydrateRequestWithUuid = (request, pathname) => {
+ request.uid = getRequestUid(pathname);
+
+ const params = get(request, 'request.params', []);
+ const headers = get(request, 'request.headers', []);
+ const requestVars = get(request, 'request.vars.req', []);
+ const responseVars = get(request, 'request.vars.res', []);
+ const assertions = get(request, 'request.assertions', []);
+ const bodyFormUrlEncoded = get(request, 'request.body.formUrlEncoded', []);
+ const bodyMultipartForm = get(request, 'request.body.multipartForm', []);
+ const file = get(request, 'request.body.file', []);
+
+ params.forEach((param) => (param.uid = uuid()));
+ headers.forEach((header) => (header.uid = uuid()));
+ requestVars.forEach((variable) => (variable.uid = uuid()));
+ responseVars.forEach((variable) => (variable.uid = uuid()));
+ assertions.forEach((assertion) => (assertion.uid = uuid()));
+ bodyFormUrlEncoded.forEach((param) => (param.uid = uuid()));
+ bodyMultipartForm.forEach((param) => (param.uid = uuid()));
+ file.forEach((param) => (param.uid = uuid()));
+
+ return request;
};
const findItemByPathname = (items = [], pathname) => {
- return find(items, (i) => slash(i.pathname) === slash(pathname));
+ return find(items, (i) => i.pathname === pathname);
};
const findItemInCollectionByPathname = (collection, pathname) => {
@@ -353,13 +395,17 @@ module.exports = {
mergeScripts,
mergeAuth,
getTreePathFromCollectionToItem,
- slash,
- findItemByPathname,
+ flattenItems,
+ findItem,
findItemInCollection,
+ findItemByPathname,
findItemInCollectionByPathname,
+ findParentItemInCollection,
+ parseBruFileMeta,
sortCollection,
sortFolder,
getAllRequestsInFolderRecursively,
getEnvVars,
- getFormattedCollectionOauth2Credentials
-}
\ No newline at end of file
+ getFormattedCollectionOauth2Credentials,
+ hydrateRequestWithUuid
+};
\ No newline at end of file
diff --git a/packages/bruno-electron/src/utils/cookies.js b/packages/bruno-electron/src/utils/cookies.js
index 5b4d7fc7c..7585e9a8a 100644
--- a/packages/bruno-electron/src/utils/cookies.js
+++ b/packages/bruno-electron/src/utils/cookies.js
@@ -1,5 +1,6 @@
const { Cookie, CookieJar } = require('tough-cookie');
const each = require('lodash/each');
+const moment = require('moment');
const cookieJar = new CookieJar();
@@ -64,22 +65,130 @@ const getDomainsWithCookies = () => {
});
};
-const deleteCookiesForDomain = (domain) => {
+const deleteCookie = (domain, path, cookieKey) => {
return new Promise((resolve, reject) => {
+ cookieJar.store.removeCookie(domain, path, cookieKey, (err) => {
+ if (err) {
+ return reject(err);
+ }
+ return resolve();
+ });
+ });
+};
+
+const deleteCookiesForDomain = (domain) => {
+ return new Promise((resolve, reject) => {
cookieJar.store.removeCookies(domain, null, (err) => {
if (err) {
return reject(err);
}
-
return resolve();
});
});
};
+const updateCookieObj = (cookieObj, oldCookie) => {
+ return {
+ ...cookieObj,
+ // Preserve immutable properties from old cookie
+ path: oldCookie.path,
+ key: oldCookie.key,
+ domain: oldCookie.domain,
+ // Handle other mutable properties
+ expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
+ creation: oldCookie?.creation && moment(oldCookie.creation).isValid() ? new Date(oldCookie.creation) : new Date(),
+ lastAccessed:
+ oldCookie?.lastAccessed && moment(oldCookie.lastAccessed).isValid()
+ ? new Date(oldCookie.lastAccessed)
+ : new Date()
+ };
+};
+
+const createCookieObj = (cookieObj) => {
+ return {
+ ...cookieObj,
+ path: cookieObj.path || '/',
+ expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
+ creation: cookieObj?.creation && moment(cookieObj.creation).isValid() ? new Date(cookieObj.creation) : new Date(),
+ lastAccessed:
+ cookieObj?.lastAccessed && moment(cookieObj.lastAccessed).isValid()
+ ? new Date(cookieObj.lastAccessed)
+ : new Date()
+ };
+};
+
+const addCookieForDomain = (domain, cookieObj) => {
+ return new Promise((resolve, reject) => {
+ try {
+ const cookie = new Cookie(createCookieObj(cookieObj));
+ cookieJar.store.putCookie(cookie, (err) => {
+ if (err) {
+ return reject(err);
+ }
+ return resolve();
+ });
+ } catch (err) {
+ reject(err);
+ }
+ });
+};
+
+const modifyCookieForDomain = (domain, oldCookieObj, cookieObj) => {
+ return new Promise((resolve, reject) => {
+ try {
+ const oldCookie = new Cookie(createCookieObj(oldCookieObj));
+ const newCookie = new Cookie(updateCookieObj(cookieObj, oldCookie));
+ cookieJar.store.updateCookie(oldCookie, newCookie, (removeErr) => {
+ if (removeErr) {
+ return reject(removeErr);
+ }
+ return resolve();
+ });
+ } catch (err) {
+ reject(err);
+ }
+ });
+};
+
+const parseCookieString = (cookieStr) => {
+ try {
+ const cookie = Cookie.parse(cookieStr);
+ if (!cookie) return null;
+
+ return {
+ ...cookie,
+ expires: cookie.expires === Infinity ? null : cookie.expires
+ };
+ } catch (err) {
+ throw new Error(err);
+ }
+};
+
+const createCookieString = (cookieObj) => {
+ const cookie = new Cookie(createCookieObj(cookieObj));
+
+ // cookie.toString() omits the domain
+ let cookieString = cookie.toString();
+
+ // Manually append domain and hostOnly if they exist
+ if (cookieObj.hostOnly && !cookieString.includes('Domain=')) {
+ cookieString += `; Domain=${cookieObj.domain}`;
+ }
+
+ return cookieString;
+};
+
module.exports = {
addCookieToJar,
getCookiesForUrl,
getCookieStringForUrl,
getDomainsWithCookies,
- deleteCookiesForDomain
+ deleteCookie,
+ deleteCookiesForDomain,
+ addCookieForDomain,
+ modifyCookieForDomain,
+ parseCookieString,
+ createCookieString,
+ updateCookieObj,
+ createCookieObj
};
diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js
index d2f74d10e..aaff867b1 100644
--- a/packages/bruno-electron/src/utils/filesystem.js
+++ b/packages/bruno-electron/src/utils/filesystem.js
@@ -44,6 +44,11 @@ const hasSubDirectories = (dir) => {
};
const normalizeAndResolvePath = (pathname) => {
+
+ if (isWSLPath(pathname)) {
+ return normalizeWSLPath(pathname);
+ }
+
if (isSymbolicLink(pathname)) {
const absPath = path.dirname(pathname);
const targetPath = path.resolve(absPath, fs.readlinkSync(pathname));
@@ -59,29 +64,24 @@ const normalizeAndResolvePath = (pathname) => {
function isWSLPath(pathname) {
// Check if the path starts with the WSL prefix
// eg. "\\wsl.localhost\Ubuntu\home\user\bruno\collection\scripting\api\req\getHeaders.bru"
- return pathname.startsWith('/wsl.localhost/') || pathname.startsWith('\\wsl.localhost\\');
+ return pathname.startsWith('\\\\') || pathname.startsWith('//') || pathname.startsWith('/wsl.localhost/') || pathname.startsWith('\\wsl.localhost');
+
}
-function normalizeWslPath(pathname) {
+function normalizeWSLPath(pathname) {
// Replace the WSL path prefix and convert forward slashes to backslashes
// This is done to achieve WSL paths (linux style) to Windows UNC equivalent (Universal Naming Conversion)
return pathname.replace(/^\/wsl.localhost/, '\\\\wsl.localhost').replace(/\//g, '\\');
}
-const writeFile = async (pathname, content) => {
+
+const writeFile = async (pathname, content, isBinary = false) => {
try {
- fs.writeFileSync(pathname, content, {
- encoding: 'utf8'
+ await safeWriteFile(pathname, content, {
+ encoding: !isBinary ? "utf-8" : null
});
} catch (err) {
- return Promise.reject(err);
- }
-};
-
-const writeBinaryFile = async (pathname, content) => {
- try {
- fs.writeFileSync(pathname, content);
- } catch (err) {
+ console.error(`Error writing file at ${pathname}:`, err);
return Promise.reject(err);
}
};
@@ -117,13 +117,13 @@ const browseDirectory = async (win) => {
return false;
}
- const resolvedPath = normalizeAndResolvePath(filePaths[0]);
+ const resolvedPath = path.resolve(filePaths[0]);
return isDirectory(resolvedPath) ? resolvedPath : false;
};
-const browseFiles = async (win, filters) => {
+const browseFiles = async (win, filters = [], properties = []) => {
const { filePaths } = await dialog.showOpenDialog(win, {
- properties: ['openFile', 'multiSelections'],
+ properties: ['openFile', ...properties],
filters
});
@@ -131,7 +131,7 @@ const browseFiles = async (win, filters) => {
return [];
}
- return filePaths.map((path) => normalizeAndResolvePath(path)).filter((path) => isFile(path));
+ return filePaths.map((path) => path.resolve(path)).filter((path) => isFile(path));
};
const chooseFileToSave = async (win, preferredFileName = '') => {
@@ -161,32 +161,36 @@ const searchForBruFiles = (dir) => {
return searchForFiles(dir, '.bru');
};
-const sanitizeCollectionName = (name) => {
- return name.trim();
-}
-
-const sanitizeDirectoryName = (name) => {
- return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-').trim();
+const sanitizeName = (name) => {
+ const invalidCharacters = /[<>:"/\\|?*\x00-\x1F]/g;
+ name = name
+ .replace(invalidCharacters, '-') // replace invalid characters with hyphens
+ .replace(/^[.\s]+/, '') // remove leading dots and and spaces
+ .replace(/[.\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens)
+ return name;
};
const isWindowsOS = () => {
return os.platform() === 'win32';
}
-const isValidFilename = (fileName) => {
- const inValidChars = /[\\/:*?"<>|]/;
+const validateName = (name) => {
+ const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;
+ const firstCharacter = /^[^.\s\-\<>:"/\\|?*\x00-\x1F]/; // no dot, space, or hyphen at start
+ const middleCharacters = /^[^<>:"/\\|?*\x00-\x1F]*$/; // no invalid characters
+ const lastCharacter = /[^.\s]$/; // no dot or space at end, hyphen allowed
+ if (name.length > 255) return false; // max name length
- if (!fileName || inValidChars.test(fileName)) {
- return false;
- }
+ if (reservedDeviceNames.test(name)) return false; // windows reserved names
- if (fileName.endsWith(' ') || fileName.endsWith('.') || fileName.startsWith('.')) {
- return false;
- }
-
- return true;
+ return (
+ firstCharacter.test(name) &&
+ middleCharacters.test(name) &&
+ lastCharacter.test(name)
+ );
};
+
const safeToRename = (oldPath, newPath) => {
try {
// If the new path doesn't exist, it's safe to rename
@@ -211,6 +215,73 @@ const safeToRename = (oldPath, newPath) => {
}
};
+const getCollectionStats = async (directoryPath) => {
+ let size = 0;
+ let filesCount = 0;
+ let maxFileSize = 0;
+
+ async function calculateStats(directory) {
+ const entries = await fsPromises.readdir(directory, { withFileTypes: true });
+
+ const tasks = entries.map(async (entry) => {
+ const fullPath = path.join(directory, entry.name);
+
+ if (entry.isDirectory()) {
+ if (['node_modules', '.git'].includes(entry.name)) {
+ return;
+ }
+
+ await calculateStats(fullPath);
+ }
+
+ if (path.extname(fullPath) === '.bru') {
+ const stats = await fsPromises.stat(fullPath);
+ size += stats?.size;
+ if (maxFileSize < stats?.size) {
+ maxFileSize = stats?.size;
+ }
+ filesCount += 1;
+ }
+ });
+
+ await Promise.all(tasks);
+ }
+
+ await calculateStats(directoryPath);
+
+ size = sizeInMB(size);
+ maxFileSize = sizeInMB(maxFileSize);
+
+ return { size, filesCount, maxFileSize };
+}
+
+const sizeInMB = (size) => {
+ return size / (1024 * 1024);
+}
+
+const getSafePathToWrite = (filePath) => {
+ const MAX_FILENAME_LENGTH = 255; // Common limit on most filesystems
+ let dir = path.dirname(filePath);
+ let ext = path.extname(filePath);
+ let base = path.basename(filePath, ext);
+ if (base.length + ext.length > MAX_FILENAME_LENGTH) {
+ base = sanitizeName(base);
+ base = base.slice(0, MAX_FILENAME_LENGTH - ext.length);
+ }
+ let safePath = path.join(dir, base + ext);
+ return safePath;
+}
+
+async function safeWriteFile(filePath, data, options) {
+ const safePath = getSafePathToWrite(filePath);
+ await fs.writeFile(safePath, data, options);
+}
+
+function safeWriteFileSync(filePath, data) {
+ const safePath = getSafePathToWrite(filePath);
+ fs.writeFileSync(safePath, data);
+}
+
module.exports = {
isValidPathname,
exists,
@@ -219,9 +290,8 @@ module.exports = {
isDirectory,
normalizeAndResolvePath,
isWSLPath,
- normalizeWslPath,
+ normalizeWSLPath,
writeFile,
- writeBinaryFile,
hasJsonExtension,
hasBruExtension,
createDirectory,
@@ -230,10 +300,13 @@ module.exports = {
chooseFileToSave,
searchForFiles,
searchForBruFiles,
- sanitizeDirectoryName,
- sanitizeCollectionName,
+ sanitizeName,
isWindowsOS,
safeToRename,
- isValidFilename,
- hasSubDirectories
+ validateName,
+ hasSubDirectories,
+ getCollectionStats,
+ sizeInMB,
+ safeWriteFile,
+ safeWriteFileSync
};
diff --git a/packages/bruno-electron/src/utils/filesystem.test.js b/packages/bruno-electron/src/utils/filesystem.test.js
index 62d7b502f..a6e2db53a 100644
--- a/packages/bruno-electron/src/utils/filesystem.test.js
+++ b/packages/bruno-electron/src/utils/filesystem.test.js
@@ -1,26 +1,84 @@
-const { sanitizeDirectoryName } = require('./filesystem.js');
+const { sanitizeName, isWSLPath, normalizeWSLPath, normalizeAndResolvePath } = require('./filesystem.js');
-describe('sanitizeDirectoryName', () => {
+describe('sanitizeName', () => {
it('should replace invalid characters with hyphens', () => {
- const input = '<>:"/\\|?*\x00-\x1F';
- const expectedOutput = '---';
- expect(sanitizeDirectoryName(input)).toEqual(expectedOutput);
+ const input = '<>:"/\|?*\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F';
+ const expectedOutput = '----------------------------------------';
+ expect(sanitizeName(input)).toEqual(expectedOutput);
});
it('should not modify valid directory names', () => {
const input = 'my-directory';
- expect(sanitizeDirectoryName(input)).toEqual(input);
+ expect(sanitizeName(input)).toEqual(input);
});
it('should replace multiple invalid characters with a single hyphen', () => {
const input = 'my<>invalid?directory';
- const expectedOutput = 'my-invalid-directory';
- expect(sanitizeDirectoryName(input)).toEqual(expectedOutput);
+ const expectedOutput = 'my--invalid-directory';
+ expect(sanitizeName(input)).toEqual(expectedOutput);
});
it('should handle names with slashes', () => {
const input = 'my/invalid/directory';
const expectedOutput = 'my-invalid-directory';
- expect(sanitizeDirectoryName(input)).toEqual(expectedOutput);
+ expect(sanitizeName(input)).toEqual(expectedOutput);
+ });
+});
+
+describe('WSL Path Utilities', () => {
+ describe('isWSLPath', () => {
+ it('should identify WSL paths starting with double backslash', () => {
+ expect(isWSLPath('\\\\wsl.localhost\\Ubuntu\\home\\user')).toBe(true);
+ });
+
+ it('should identify WSL paths starting with double forward slash', () => {
+ expect(isWSLPath('//wsl.localhost/Ubuntu/home/user')).toBe(true);
+ });
+
+ it('should identify WSL paths starting with /wsl.localhost/', () => {
+ expect(isWSLPath('/wsl.localhost/Ubuntu/home/user')).toBe(true);
+ });
+
+ it('should identify WSL paths starting with \\wsl.localhost', () => {
+ expect(isWSLPath('\\wsl.localhost\\Ubuntu\\home\\user')).toBe(true);
+ });
+
+ it('should return false for non-WSL paths', () => {
+ expect(isWSLPath('C:\\Users\\user\\Documents')).toBe(false);
+ expect(isWSLPath('/home/user/documents')).toBe(false);
+ expect(isWSLPath('relative/path')).toBe(false);
+ });
+ });
+
+ describe('normalizeWSLPath', () => {
+ it('should convert forward slash WSL paths to backslash format', () => {
+ const input = '/wsl.localhost/Ubuntu/home/user/file.txt';
+ const expected = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt';
+ expect(normalizeWSLPath(input)).toBe(expected);
+ });
+
+ it('should handle paths already in backslash format', () => {
+ const input = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt';
+ expect(normalizeWSLPath(input)).toBe(input);
+ });
+
+ it('should convert mixed slash formats to backslash format', () => {
+ const input = '/wsl.localhost\\Ubuntu/home\\user/file.txt';
+ const expected = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt';
+ expect(normalizeWSLPath(input)).toBe(expected);
+ });
+ });
+
+ describe('normalizeAndResolvePath with WSL paths', () => {
+ it('should normalize WSL paths', () => {
+ const input = '/wsl.localhost/Ubuntu/home/user/file.txt';
+ const expected = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt';
+ expect(normalizeAndResolvePath(input)).toBe(expected);
+ });
+
+ it('should handle already normalized WSL paths', () => {
+ const input = '\\\\wsl.localhost\\Ubuntu\\home\\user\\file.txt';
+ expect(normalizeAndResolvePath(input)).toBe(input);
+ });
});
});
diff --git a/packages/bruno-electron/src/utils/form-data.js b/packages/bruno-electron/src/utils/form-data.js
index f20371128..dc27b2577 100644
--- a/packages/bruno-electron/src/utils/form-data.js
+++ b/packages/bruno-electron/src/utils/form-data.js
@@ -5,7 +5,7 @@ const path = require('path');
/**
* @param {Array.} params The request body Array
- * @returns {object} Returns an obj with repeating key as a array of values
+ * @returns {object} Returns an obj with repeating key as an array of values
* {item: 2, item: 3, item1: 4} becomes {item: [2,3], item1: 4}
*/
const buildFormUrlEncodedPayload = (params) => {
diff --git a/packages/bruno-electron/src/workers/index.js b/packages/bruno-electron/src/workers/index.js
new file mode 100644
index 000000000..d1d1a1b74
--- /dev/null
+++ b/packages/bruno-electron/src/workers/index.js
@@ -0,0 +1,68 @@
+const { Worker } = require('worker_threads');
+
+class WorkerQueue {
+ constructor() {
+ this.queue = [];
+ this.isProcessing = false;
+ this.workers = {};
+ }
+
+ async getWorkerForScriptPath(scriptPath) {
+ if (!this.workers) this.workers = {};
+ let worker = this.workers[scriptPath];
+ if (!worker || worker.threadId === -1) {
+ this.workers[scriptPath] = worker = new Worker(scriptPath);
+ }
+ return worker;
+ }
+
+ async enqueue(task) {
+ const { priority, scriptPath, data } = task;
+
+ return new Promise((resolve, reject) => {
+ this.queue.push({ priority, scriptPath, data, resolve, reject });
+ this.queue?.sort((taskX, taskY) => taskX?.priority - taskY?.priority);
+ this.processQueue();
+ });
+ }
+
+ async processQueue() {
+ if (this.isProcessing || this.queue.length === 0){
+ return;
+ }
+
+ this.isProcessing = true;
+ const { scriptPath, data, resolve, reject } = this.queue.shift();
+
+ try {
+ const result = await this.runWorker({ scriptPath, data });
+ resolve(result);
+ } catch (error) {
+ reject(error);
+ } finally {
+ this.isProcessing = false;
+ this.processQueue();
+ }
+ }
+
+ async runWorker({ scriptPath, data }) {
+ return new Promise(async (resolve, reject) => {
+ let worker = await this.getWorkerForScriptPath(scriptPath);
+ worker.postMessage(data);
+ worker.on('message', (data) => {
+ if (data?.error) {
+ reject(new Error(data?.error));
+ }
+ resolve(data);
+ });
+ worker.on('error', (error) => {
+ reject(error);
+ });
+ worker.on('exit', (code) => {
+ reject(new Error(`stopped with ${code} exit code`));
+ });
+ });
+ }
+}
+
+module.exports = WorkerQueue;
diff --git a/packages/bruno-electron/tests/network/prepare-request.spec.js b/packages/bruno-electron/tests/network/prepare-request.spec.js
index a624d3ea5..34bedcc90 100644
--- a/packages/bruno-electron/tests/network/prepare-request.spec.js
+++ b/packages/bruno-electron/tests/network/prepare-request.spec.js
@@ -7,15 +7,17 @@ describe('prepare-request: prepareRequest', () => {
describe('Decomments request body', () => {
it('If request body is valid JSON', async () => {
const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' };
- const expected = '{\n"test": "{{someVar}}" \n}';
- const result = prepareRequest({ request: { body } }, {});
+ const expected = `{
+\"test\": \"{{someVar}}\"
+}`;
+ const result = await prepareRequest({ request: { body }, collection: { pathname: '' } });
expect(result.data).toEqual(expected);
});
it('If request body is not valid JSON', async () => {
const body = { mode: 'json', json: '{\n"test": {{someVar}} // comment\n}' };
const expected = '{\n"test": {{someVar}} \n}';
- const result = prepareRequest({ request: { body } }, {});
+ const result = await prepareRequest({ request: { body }, collection: { pathname: '' } });
expect(result.data).toEqual(expected);
});
diff --git a/packages/bruno-electron/tests/utils/collection.spec.js b/packages/bruno-electron/tests/utils/collection.spec.js
new file mode 100644
index 000000000..4efc9c002
--- /dev/null
+++ b/packages/bruno-electron/tests/utils/collection.spec.js
@@ -0,0 +1,121 @@
+const { parseBruFileMeta } = require("../../src/utils/collection");
+
+describe('parseBruFileMeta', () => {
+ test('parses valid meta block correctly', () => {
+ const data = `meta {
+ name: 0.2_mb
+ type: http
+ seq: 1
+ }`;
+
+ const result = parseBruFileMeta(data);
+
+ expect(result).toEqual({
+ meta: {
+ name: '0.2_mb',
+ type: 'http',
+ seq: 1,
+ },
+ });
+ });
+
+ test('returns undefined for missing meta block', () => {
+ const data = `someOtherBlock {
+ key: value
+ }`;
+
+ const result = parseBruFileMeta(data);
+
+ expect(result).toBeUndefined();
+ });
+
+ test('handles empty meta block gracefully', () => {
+ const data = `meta {}`;
+
+ const result = parseBruFileMeta(data);
+
+ expect(result).toEqual({ meta: {} });
+ });
+
+ test('ignores invalid lines in meta block', () => {
+ const data = `meta {
+ name: 0.2_mb
+ invalidLine
+ seq: 1
+ }`;
+
+ const result = parseBruFileMeta(data);
+
+ expect(result).toEqual({
+ meta: {
+ name: '0.2_mb',
+ seq: 1,
+ },
+ });
+ });
+
+ test('handles unexpected input gracefully', () => {
+ const data = null;
+
+ const result = parseBruFileMeta(data);
+
+ expect(result).toBeUndefined();
+ });
+
+ test('handles missing colon gracefully', () => {
+ const data = `meta {
+ name 0.2_mb
+ seq: 1
+ }`;
+
+ const result = parseBruFileMeta(data);
+
+ expect(result).toEqual({
+ meta: {
+ seq: 1,
+ },
+ });
+ });
+
+ test('parses numeric values correctly', () => {
+ const data = `meta {
+ numValue: 1234
+ floatValue: 12.34
+ strValue: some_text
+ }`;
+
+ const result = parseBruFileMeta(data);
+
+ expect(result).toEqual({
+ meta: {
+ numValue: 1234,
+ floatValue: 12.34,
+ strValue: 'some_text',
+ },
+ });
+ });
+
+ test('handles syntax error in meta block 1', () => {
+ const data = `meta
+ name: 0.2_mb
+ type: http
+ seq: 1
+ }`;
+
+ const result = parseBruFileMeta(data);
+
+ expect(result).toBeUndefined();
+ });
+
+ test('handles syntax error in meta block 2', () => {
+ const data = `meta {
+ name: 0.2_mb
+ type: http
+ seq: 1
+ `;
+
+ const result = parseBruFileMeta(data);
+
+ expect(result).toBeUndefined();
+ });
+});
diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json
index 8ad1d8e46..d9b4e16e9 100644
--- a/packages/bruno-js/package.json
+++ b/packages/bruno-js/package.json
@@ -21,10 +21,11 @@
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"atob": "^2.1.2",
- "axios": "1.7.5",
+ "axios": "^1.8.3",
"btoa": "^1.2.1",
"chai": "^4.3.7",
"chai-string": "^1.5.0",
+ "cheerio": "^1.0.0",
"crypto-js": "^4.1.1",
"json-query": "^2.2.2",
"lodash": "^4.17.21",
@@ -34,7 +35,8 @@
"node-vault": "^0.10.2",
"path": "^0.12.7",
"quickjs-emscripten": "^0.29.2",
- "uuid": "^9.0.0"
+ "uuid": "^9.0.0",
+ "xml2js": "^0.6.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
diff --git a/packages/bruno-js/src/bruno-response.js b/packages/bruno-js/src/bruno-response.js
index 9e68045d9..0ad8eae2e 100644
--- a/packages/bruno-js/src/bruno-response.js
+++ b/packages/bruno-js/src/bruno-response.js
@@ -1,3 +1,5 @@
+const { get } = require('@usebruno/query');
+
class BrunoResponse {
constructor(res) {
this.res = res;
@@ -6,12 +8,23 @@ class BrunoResponse {
this.headers = res ? res.headers : null;
this.body = res ? res.data : null;
this.responseTime = res ? res.responseTime : null;
+
+ // Make the instance callable
+ const callable = (...args) => get(this.body, ...args);
+ Object.setPrototypeOf(callable, this.constructor.prototype);
+ Object.assign(callable, this);
+
+ return callable;
}
getStatus() {
return this.res ? this.res.status : null;
}
+ getStatusText() {
+ return this.res ? this.res.statusText : null;
+ }
+
getHeader(name) {
return this.res && this.res.headers ? this.res.headers[name] : null;
}
diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js
index fdbdf01c1..73057aac5 100644
--- a/packages/bruno-js/src/runtime/script-runtime.js
+++ b/packages/bruno-js/src/runtime/script-runtime.js
@@ -28,6 +28,8 @@ const fetch = require('node-fetch');
const chai = require('chai');
const CryptoJS = require('crypto-js');
const NodeVault = require('node-vault');
+const xml2js = require('xml2js');
+const cheerio = require('cheerio');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
class ScriptRuntime {
@@ -147,6 +149,8 @@ class ScriptRuntime {
chai,
'node-fetch': fetch,
'crypto-js': CryptoJS,
+ 'xml2js': xml2js,
+ cheerio,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault
diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js
index 71db9d83e..e2d1f4865 100644
--- a/packages/bruno-js/src/runtime/test-runtime.js
+++ b/packages/bruno-js/src/runtime/test-runtime.js
@@ -30,6 +30,8 @@ const axios = require('axios');
const fetch = require('node-fetch');
const CryptoJS = require('crypto-js');
const NodeVault = require('node-vault');
+const xml2js = require('xml2js');
+const cheerio = require('cheerio');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
const getResultsSummary = (results) => {
@@ -205,6 +207,8 @@ class TestRuntime {
chai,
'node-fetch': fetch,
'crypto-js': CryptoJS,
+ 'xml2js': xml2js,
+ cheerio,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault
diff --git a/packages/bruno-js/src/sandbox/quickjs/index.js b/packages/bruno-js/src/sandbox/quickjs/index.js
index 58ccd885d..2c83c0e3f 100644
--- a/packages/bruno-js/src/sandbox/quickjs/index.js
+++ b/packages/bruno-js/src/sandbox/quickjs/index.js
@@ -22,6 +22,12 @@ const toNumber = (value) => {
return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(value);
};
+const removeQuotes = (str) => {
+ if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
+ return str.slice(1, -1);
+ }
+ return str;
+};
const executeQuickJsVm = ({ script: externalScript, context: externalContext, scriptType = 'template-literal' }) => {
if (!externalScript?.length || typeof externalScript !== 'string') {
@@ -29,16 +35,26 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
}
externalScript = externalScript?.trim();
- if (!isNaN(Number(externalScript))) {
- return Number(externalScript);
+ if(scriptType === 'template-literal') {
+ if (!isNaN(Number(externalScript))) {
+ const number = Number(externalScript);
+
+ // Check if the number is too high. Too high number might get altered, see #1000
+ if (number > Number.MAX_SAFE_INTEGER) {
+ return externalScript;
+ }
+
+ return toNumber(externalScript);
+ }
+
+ if (externalScript === 'true') return true;
+ if (externalScript === 'false') return false;
+ if (externalScript === 'null') return null;
+ if (externalScript === 'undefined') return undefined;
+
+ externalScript = removeQuotes(externalScript);
}
- if (externalScript === 'true') return true;
- if (externalScript === 'false') return false;
- if (externalScript === 'null') return null;
- if (externalScript === 'undefined') return undefined;
-
-
const vm = QuickJSSyncContext;
try {
@@ -78,16 +94,6 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
}
externalScript = externalScript?.trim();
- if (!isNaN(Number(externalScript))) {
- return toNumber(externalScript);
- }
-
- if (externalScript === 'true') return true;
- if (externalScript === 'false') return false;
- if (externalScript === 'null') return null;
- if (externalScript === 'undefined') return undefined;
-
-
try {
const module = await newQuickJSWASMModule();
const vm = module.newContext();
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js
index bacab783f..b5b807ec2 100644
--- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js
@@ -195,8 +195,8 @@ const addBruShimToContext = (vm, bru) => {
const promise = vm.newPromise();
bru.runRequest(vm.dump(args))
.then((response) => {
- const { status, headers, data, dataBuffer, size } = response || {};
- promise.resolve(marshallToVm(cleanJson({ status, headers, data, dataBuffer, size }), vm));
+ const { status, headers, data, dataBuffer, size, statusText } = response || {};
+ promise.resolve(marshallToVm(cleanJson({ status, statusText, headers, data, dataBuffer, size }), vm));
})
.catch((err) => {
promise.resolve(
diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js
index fb2ae6888..6b9501876 100644
--- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js
+++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js
@@ -6,11 +6,13 @@ const addBrunoResponseShimToContext = (vm, res) => {
});
const status = marshallToVm(res?.status, vm);
+ const statusText = marshallToVm(res?.statusText, vm);
const headers = marshallToVm(res?.headers, vm);
const body = marshallToVm(res?.body, vm);
const responseTime = marshallToVm(res?.responseTime, vm);
vm.setProp(resFn, 'status', status);
+ vm.setProp(resFn, 'statusText', statusText);
vm.setProp(resFn, 'headers', headers);
vm.setProp(resFn, 'body', body);
vm.setProp(resFn, 'responseTime', responseTime);
@@ -19,6 +21,13 @@ const addBrunoResponseShimToContext = (vm, res) => {
headers.dispose();
body.dispose();
responseTime.dispose();
+ statusText.dispose();
+
+ let getStatusText = vm.newFunction('getStatusText', function () {
+ return marshallToVm(res.getStatusText(), vm);
+ });
+ vm.setProp(resFn, 'getStatusText', getStatusText);
+ getStatusText.dispose();
let getStatus = vm.newFunction('getStatus', function () {
return marshallToVm(res.getStatus(), vm);
diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js
index 6b5ecacb5..55b454d02 100644
--- a/packages/bruno-js/src/utils.js
+++ b/packages/bruno-js/src/utils.js
@@ -85,6 +85,14 @@ const evaluateJsTemplateLiteral = (templateLiteral, context) => {
return undefined;
}
+ if (templateLiteral.startsWith('"') && templateLiteral.endsWith('"')) {
+ return templateLiteral.slice(1, -1);
+ }
+
+ if (templateLiteral.startsWith("'") && templateLiteral.endsWith("'")) {
+ return templateLiteral.slice(1, -1);
+ }
+
if (!isNaN(templateLiteral)) {
const number = Number(templateLiteral);
// Check if the number is too high. Too high number might get altered, see #1000
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index 104fefc22..8243704d3 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -25,7 +25,7 @@ const grammar = ohm.grammar(`Bru {
BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
- bodyforms = bodyformurlencoded | bodymultipart
+ bodyforms = bodyformurlencoded | bodymultipart | bodyfile
params = paramspath | paramsquery
nl = "\\r"? "\\n"
@@ -102,7 +102,8 @@ const grammar = ohm.grammar(`Bru {
bodyformurlencoded = "body:form-urlencoded" dictionary
bodymultipart = "body:multipart-form" dictionary
-
+ bodyfile = "body:file" dictionary
+
script = scriptreq | scriptres
scriptreq = "script:pre-request" st* "{" nl* textblock tagend
scriptres = "script:post-response" st* "{" nl* textblock tagend
@@ -173,6 +174,19 @@ const multipartExtractContentType = (pair) => {
}
};
+const fileExtractContentType = (pair) => {
+ if (_.isString(pair.value)) {
+ const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/);
+ if (match && match.length > 2) {
+ pair.value = match[1].trim();
+ pair.contentType = match[2].trim();
+ } else {
+ pair.contentType = '';
+ }
+ }
+};
+
+
const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => {
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
@@ -190,6 +204,27 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) =
});
};
+const mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => {
+ const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
+ return pairs.map((pair) => {
+ fileExtractContentType(pair);
+
+ if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {
+ let filePath = pair.value.replace(/^@file\(/, '').replace(/\)$/, '');
+ pair.filePath = filePath;
+ pair.selected = pair.enabled
+
+ // Remove pair.value as it only contains the file path reference
+ delete pair.value;
+ // Remove pair.name as it is auto-generated (e.g., file1, file2, file3, etc.)
+ delete pair.name;
+ delete pair.enabled;
+ }
+
+ return pair;
+ });
+};
+
const concatArrays = (objValue, srcValue) => {
if (_.isArray(objValue) && _.isArray(srcValue)) {
return objValue.concat(srcValue);
@@ -606,6 +641,13 @@ const sem = grammar.createSemantics().addAttribute('ast', {
}
};
},
+ bodyfile(_1, dictionary) {
+ return {
+ body: {
+ file: mapPairListToKeyValPairsFile(dictionary.ast)
+ }
+ };
+ },
body(_1, _2, _3, _4, textblock, _5) {
return {
http: {
@@ -740,3 +782,4 @@ const parser = (input) => {
};
module.exports = parser;
+
\ No newline at end of file
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index de4193473..51f01e88a 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -2,8 +2,8 @@ const _ = require('lodash');
const { indentString } = require('../../v1/src/utils');
-const enabled = (items = []) => items.filter((item) => item.enabled);
-const disabled = (items = []) => items.filter((item) => !item.enabled);
+const enabled = (items = [], key = "enabled") => items.filter((item) => item[key]);
+const disabled = (items = [], key = "enabled") => items.filter((item) => !item[key]);
// remove the last line if two new lines are found
const stripLastLine = (text) => {
@@ -343,6 +343,30 @@ ${indentString(body.sparql)}
bru += '\n}\n\n';
}
+
+ if (body && body.file && body.file.length) {
+ bru += `body:file {`;
+ const files = enabled(body.file, "selected").concat(disabled(body.file, "selected"));
+
+ if (files.length) {
+ bru += `\n${indentString(
+ files
+ .map((item) => {
+ const selected = item.selected ? '' : '~';
+ const contentType =
+ item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';
+ const filePath = item.filePath || '';
+ const value = `@file(${filePath})`;
+ const itemName = "file";
+ return `${selected}${itemName}: ${value}${contentType}`;
+ })
+ .join('\n')
+ )}`;
+ }
+
+ bru += '\n}\n\n';
+ }
+
if (body && body.graphql && body.graphql.query) {
bru += `body:graphql {\n`;
bru += `${indentString(body.graphql.query)}`;
@@ -469,4 +493,4 @@ ${indentString(docs)}
module.exports = jsonToBru;
-// alternative to writing the below code to avoif undefined
+// alternative to writing the below code to avoid undefined
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru
index 1a3efeab7..ad66c64e8 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.bru
+++ b/packages/bruno-lang/v2/tests/fixtures/request.bru
@@ -102,6 +102,12 @@ body:multipart-form {
~message: hello
}
+body:file {
+ file: @file(path/to/file.json) @contentType(application/json)
+ file: @file(path/to/file.json) @contentType(application/json)
+ ~file: @file(path/to/file2.json) @contentType(application/json)
+}
+
body:graphql {
{
launchesPast {
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json
index 9c8ed143d..1cfe98809 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.json
+++ b/packages/bruno-lang/v2/tests/fixtures/request.json
@@ -137,6 +137,23 @@
"enabled": false,
"type": "text"
}
+ ],
+ "file" : [
+ {
+ "filePath": "path/to/file.json",
+ "contentType": "application/json",
+ "selected": true
+ },
+ {
+ "filePath": "path/to/file.json",
+ "contentType": "application/json",
+ "selected": true
+ },
+ {
+ "filePath": "path/to/file2.json",
+ "contentType": "application/json",
+ "selected": false
+ }
]
},
"vars": {
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 8b8ed47c5..a4883b460 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -74,9 +74,19 @@ const multipartFormSchema = Yup.object({
.noUnknown(true)
.strict();
+
+const fileSchema = Yup.object({
+ uid: uidSchema,
+ filePath: Yup.string().nullable(),
+ contentType: Yup.string().nullable(),
+ selected: Yup.boolean()
+})
+ .noUnknown(true)
+ .strict();
+
const requestBodySchema = Yup.object({
mode: Yup.string()
- .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql'])
+ .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'file'])
.required('mode is required'),
json: Yup.string().nullable(),
text: Yup.string().nullable(),
@@ -84,7 +94,8 @@ const requestBodySchema = Yup.object({
sparql: Yup.string().nullable(),
formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),
multipartForm: Yup.array().of(multipartFormSchema).nullable(),
- graphql: graphqlBodySchema.nullable()
+ graphql: graphqlBodySchema.nullable(),
+ file: Yup.array().of(fileSchema).nullable()
})
.noUnknown(true)
.strict();
diff --git a/packages/bruno-tests/collection/asserts/test-assert-combinations.bru b/packages/bruno-tests/collection/asserts/test-assert-combinations.bru
new file mode 100644
index 000000000..3ad85765a
--- /dev/null
+++ b/packages/bruno-tests/collection/asserts/test-assert-combinations.bru
@@ -0,0 +1,74 @@
+meta {
+ name: test-assert-combinations
+ type: http
+ seq: 1
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "type": "application/json",
+ "contentJSON": {
+ "string": "foo",
+ "stringWithSQuotes": "'foo'",
+ "stringWithDQuotes": "\"foo\"",
+ "number": 123,
+ "numberAsString": "123",
+ "numberAsStringWithSQuotes": "'123'",
+ "numberAsStringWithDQuotes": "\"123\"",
+ "numberAsStringWithLeadingZero": "0123",
+ "numberBig": 9007199254740992000,
+ "numberBigAsString": "9007199254740991999",
+ "null": null,
+ "nullAsString": "null",
+ "nullAsStringWithSQuotes": "'null'",
+ "nullAsStringWithDQuotes": "\"null\"",
+ "true": true,
+ "trueAsString": "true",
+ "trueAsStringWithSQuotes": "'true'",
+ "trueAsStringWithDQuotes": "\"true\"",
+ "false": false,
+ "falseAsString": "false",
+ "falseAsStringWithSQuotes": "'false'",
+ "falseAsStringWithDQuotes": "\"false\"",
+ "stringWithCurlyBraces": "{foo}",
+ "stringWithDoubleCurlyBraces": "{{foobar}}"
+ }
+ }
+}
+
+assert {
+ res.body.string: eq foo
+ res.body.string: eq 'foo'
+ res.body.string: eq "foo"
+ res.body.stringWithSQuotes: eq "'foo'"
+ res.body.stringWithDQuotes: eq '"foo"'
+ res.body.number: eq 123
+ res.body.numberAsString: eq '123'
+ res.body.numberAsString: eq "123"
+ res.body.numberAsStringWithSQuotes: eq "'123'"
+ res.body.numberAsStringWithDQuotes: eq '"123"'
+ res.body.numberAsStringWithLeadingZero: eq "0123"
+ res.body.numberBig.toString(): eq '9007199254740992000'
+ res.body.numberBigAsString: eq "9007199254740991999"
+ res.body.null: eq null
+ res.body.nullAsString: eq "null"
+ res.body.nullAsStringWithSQuotes: eq "'null'"
+ res.body.nullAsStringWithDQuotes: eq '"null"'
+ res.body.true: eq true
+ res.body.trueAsString: eq "true"
+ res.body.trueAsStringWithSQuotes: eq "'true'"
+ res.body.trueAsStringWithDQuotes: eq '"true"'
+ res.body.false: eq false
+ res.body.falseAsString: eq "false"
+ res.body.falseAsStringWithSQuotes: eq "'false'"
+ res.body.falseAsStringWithDQuotes: eq '"false"'
+ res.body.nonexistent: eq undefined
+ res.body.stringWithCurlyBraces: eq "{foo}"
+ res.body.stringWithDoubleCurlyBraces: eq "{{foobar}}"
+}
diff --git a/packages/bruno-tests/collection/echo/multiline/echo binary.bru b/packages/bruno-tests/collection/echo/multiline/echo binary.bru
new file mode 100644
index 000000000..704419886
--- /dev/null
+++ b/packages/bruno-tests/collection/echo/multiline/echo binary.bru
@@ -0,0 +1,15 @@
+meta {
+ name: echo binary
+ type: http
+ seq: 1
+}
+
+post {
+ url: {{echo-host}}
+ body: file
+ auth: none
+}
+
+body:file {
+ file: @file(bruno.png) @contentType(image/png)
+}
diff --git a/packages/bruno-tests/collection/echo/test echo any.bru b/packages/bruno-tests/collection/echo/test echo any.bru
new file mode 100644
index 000000000..b78014dd9
--- /dev/null
+++ b/packages/bruno-tests/collection/echo/test echo any.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test echo any
+ type: http
+ seq: 11
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "text/plain" },
+ "content": "hello"
+ }
+}
+
+assert {
+ res.body: eq hello
+}
diff --git a/packages/bruno-tests/collection/echo/test echo-any json.bru b/packages/bruno-tests/collection/echo/test echo-any json.bru
new file mode 100644
index 000000000..2f3a7e5f4
--- /dev/null
+++ b/packages/bruno-tests/collection/echo/test echo-any json.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test echo-any json
+ type: http
+ seq: 12
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "type": "application/json",
+ "contentJSON": {"x": 42}
+ }
+}
+
+assert {
+ res.body.x: eq 42
+}
diff --git a/packages/bruno-tests/collection/environments/Local.bru b/packages/bruno-tests/collection/environments/Local.bru
index 991077d97..a7ac3f541 100644
--- a/packages/bruno-tests/collection/environments/Local.bru
+++ b/packages/bruno-tests/collection/environments/Local.bru
@@ -1,7 +1,14 @@
vars {
host: http://localhost:8080
+ httpfaker: https://www.httpfaker.org
bearer_auth_token: your_secret_token
basic_auth_password: della
+ env.var1: envVar1
+ env-var2: envVar2
+ bark: {{process.env.PROC_ENV_VAR}}
+ foo: bar
+ testSetEnvVar: bruno-29653
+ echo-host: https://echo.usebruno.com
client_id: client_id_1
client_secret: client_secret_1
auth_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize
diff --git a/packages/bruno-tests/collection/environments/Prod.bru b/packages/bruno-tests/collection/environments/Prod.bru
index ce8fa60cc..f33c1bb05 100644
--- a/packages/bruno-tests/collection/environments/Prod.bru
+++ b/packages/bruno-tests/collection/environments/Prod.bru
@@ -1,5 +1,6 @@
vars {
host: https://testbench-sanity.usebruno.com
+ httpfaker: https://www.httpfaker.org
bearer_auth_token: your_secret_token
basic_auth_password: della
env.var1: envVar1
diff --git a/packages/bruno-tests/collection/file.txt b/packages/bruno-tests/collection/file.txt
new file mode 100644
index 000000000..0a1443d43
--- /dev/null
+++ b/packages/bruno-tests/collection/file.txt
@@ -0,0 +1,3 @@
+file.txt
+
+hello, bruno
diff --git a/packages/bruno-tests/collection/ping.bru b/packages/bruno-tests/collection/ping.bru
index ed9412899..8f4f3c6f7 100644
--- a/packages/bruno-tests/collection/ping.bru
+++ b/packages/bruno-tests/collection/ping.bru
@@ -8,4 +8,8 @@ get {
url: {{host}}/ping
body: none
auth: none
-}
\ No newline at end of file
+}
+
+script:pre-request {
+ bru.runner.stopExecution();
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test JSON false response.bru b/packages/bruno-tests/collection/response-parsing/test JSON false response.bru
new file mode 100644
index 000000000..a507434e5
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test JSON false response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test JSON false response
+ type: http
+ seq: 11
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "application/json" },
+ "content": "false"
+ }
+}
+
+assert {
+ res.body: eq false
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test JSON null response.bru b/packages/bruno-tests/collection/response-parsing/test JSON null response.bru
new file mode 100644
index 000000000..156aa7fbe
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test JSON null response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test JSON null response
+ type: http
+ seq: 6
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "application/json" },
+ "content": "null"
+ }
+}
+
+assert {
+ res.body: eq null
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test JSON number response.bru b/packages/bruno-tests/collection/response-parsing/test JSON number response.bru
new file mode 100644
index 000000000..8f995b395
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test JSON number response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test JSON number response
+ type: http
+ seq: 12
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "application/json" },
+ "content": "3.1"
+ }
+}
+
+assert {
+ res.body: eq 3.1
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test JSON response.bru b/packages/bruno-tests/collection/response-parsing/test JSON response.bru
new file mode 100644
index 000000000..018cce86d
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test JSON response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test JSON response
+ type: http
+ seq: 2
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "application/json" },
+ "contentJSON": { "message": "hello" }
+ }
+}
+
+assert {
+ res.body.message: eq hello
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test JSON string response.bru b/packages/bruno-tests/collection/response-parsing/test JSON string response.bru
new file mode 100644
index 000000000..18a0e3909
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test JSON string response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test JSON string response
+ type: http
+ seq: 7
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "application/json" },
+ "content": "\"ok\""
+ }
+}
+
+assert {
+ res.body: eq ok
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test JSON string with quotes response.bru b/packages/bruno-tests/collection/response-parsing/test JSON string with quotes response.bru
new file mode 100644
index 000000000..d262ff084
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test JSON string with quotes response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test JSON string with quotes response
+ type: http
+ seq: 8
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "application/json" },
+ "contentJSON": "\"ok\""
+ }
+}
+
+assert {
+ res.body: eq '"ok"'
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test JSON true response.bru b/packages/bruno-tests/collection/response-parsing/test JSON true response.bru
new file mode 100644
index 000000000..7f4c0bd65
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test JSON true response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test JSON true response
+ type: http
+ seq: 10
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "application/json" },
+ "content": "true"
+ }
+}
+
+assert {
+ res.body: eq true
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test JSON unsafe-int response.bru b/packages/bruno-tests/collection/response-parsing/test JSON unsafe-int response.bru
new file mode 100644
index 000000000..db4fe3bcc
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test JSON unsafe-int response.bru
@@ -0,0 +1,26 @@
+meta {
+ name: test JSON unsafe-int response
+ type: http
+ seq: 13
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "application/json" },
+ "content": "90071992547409919876"
+ }
+}
+
+assert {
+ res.body.toString(): eq 90071992547409920000
+}
+
+docs {
+ Note: This test is not perfect, we should match the unparsed raw-response with the expected string version of the unsafe-integer
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test binary response.bru b/packages/bruno-tests/collection/response-parsing/test binary response.bru
new file mode 100644
index 000000000..53e6e3436
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test binary response.bru
@@ -0,0 +1,34 @@
+meta {
+ name: test binary response
+ type: http
+ seq: 4
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "type": "application/octet-stream",
+ "contentBase64": "+Z1P82iH1wmbILfvnhvjQVbVAktP4TzltpxYD74zNyA="
+ }
+}
+
+tests {
+ test("response matches the expectation after utf-8 decoding(needs improvement)", function () {
+ expect(res.getStatus()).to.equal(200);
+ const dataBinary = Buffer.from("+Z1P82iH1wmbILfvnhvjQVbVAktP4TzltpxYD74zNyA=", "base64");
+ expect(res.body).to.equal(dataBinary.toString("utf-8"));
+ });
+}
+
+docs {
+ Note:
+
+ This test is not perfect and needs to be improved by direclty matching expected binary data with raw-response.
+
+ Currently res.body is decoded with `utf-8` by default and looses data in the process. We need some property in `res` which gives access to raw-data/Buffer.
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test html response.bru b/packages/bruno-tests/collection/response-parsing/test html response.bru
new file mode 100644
index 000000000..48bf15310
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test html response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test html response
+ type: http
+ seq: 5
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "text/html" },
+ "content": "hello "
+ }
+}
+
+assert {
+ res.body: eq hello
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test image response.bru b/packages/bruno-tests/collection/response-parsing/test image response.bru
new file mode 100644
index 000000000..4ca65adab
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test image response.bru
@@ -0,0 +1,18 @@
+meta {
+ name: test image response
+ type: http
+ seq: 3
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "type": "image/png",
+ "contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAQMAAABKLAcXAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGUExURQCqAP///59OGOoAAAABYktHRAH/Ai3eAAAAB3RJTUUH6QMHCwUNKHvFmgAAABRJREFUOMtjYBgFo2AUjIJRQE8AAAV4AAEpcbn8AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI1LTAzLTA3VDExOjA1OjEzKzAwOjAwQkgGWgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNS0wMy0wN1QxMTowNToxMyswMDowMDMVvuYAAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjUtMDMtMDdUMTE6MDU6MTMrMDA6MDBkAJ85AAAAAElFTkSuQmCC"
+ }
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test invalid JSON response with formatting.bru b/packages/bruno-tests/collection/response-parsing/test invalid JSON response with formatting.bru
new file mode 100644
index 000000000..57e2a6872
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test invalid JSON response with formatting.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test invalid JSON response with formatting
+ type: http
+ seq: 19
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "application/json" },
+ "content": "hello\n\tworld"
+ }
+}
+
+assert {
+ res.body: eq hello\n\tworld
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test plain text response with formatting.bru b/packages/bruno-tests/collection/response-parsing/test plain text response with formatting.bru
new file mode 100644
index 000000000..af9a87144
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test plain text response with formatting.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test plain text response with formatting
+ type: http
+ seq: 18
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "text/plain" },
+ "content": "hello\n\tworld"
+ }
+}
+
+assert {
+ res.body: eq hello\n\tworld
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test plain text response.bru b/packages/bruno-tests/collection/response-parsing/test plain text response.bru
new file mode 100644
index 000000000..fbb884483
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test plain text response.bru
@@ -0,0 +1,23 @@
+meta {
+ name: test plain text response
+ type: http
+ seq: 1
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "text/plain" },
+ "content": "hello"
+ }
+}
+
+assert {
+ res.body: eq hello
+}
+
diff --git a/packages/bruno-tests/collection/response-parsing/test plain text utf16 response.bru b/packages/bruno-tests/collection/response-parsing/test plain text utf16 response.bru
new file mode 100644
index 000000000..096985f0a
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test plain text utf16 response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test plain text utf16 response
+ type: http
+ seq: 14
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "text/plain; charset=utf-16" },
+ "contentBase64": "dABoAGkAcwAgAGkAcwAgAGUAbgBjAG8AZABlAGQAIAB3AGkAdABoACAAdQB0AGYAMQA2AA=="
+ }
+}
+
+assert {
+ res.body: eq "this is encoded with utf16"
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test plain text utf16-be with BOM response.bru b/packages/bruno-tests/collection/response-parsing/test plain text utf16-be with BOM response.bru
new file mode 100644
index 000000000..cffe54a97
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test plain text utf16-be with BOM response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test plain text utf16-be with BOM response
+ type: http
+ seq: 15
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "text/plain; charset=utf-16" },
+ "contentBase64": "/v8AdABoAGkAcwAgAGkAcwAgAGUAbgBjAG8AZABlAGQAIAB3AGkAdABoACAAdQB0AGYAMQA2AC0AYgBlACAAdwBpAHQAaAAgAEIATwBN"
+ }
+}
+
+assert {
+ res.body: eq "this is encoded with utf16-be with BOM"
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test plain text utf16-le with BOM response.bru b/packages/bruno-tests/collection/response-parsing/test plain text utf16-le with BOM response.bru
new file mode 100644
index 000000000..5d29a2701
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test plain text utf16-le with BOM response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test plain text utf16-le with BOM response
+ type: http
+ seq: 16
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "text/plain; charset=utf-16" },
+ "contentBase64": "//50AGgAaQBzACAAaQBzACAAZQBuAGMAbwBkAGUAZAAgAHcAaQB0AGgAIAB1AHQAZgAxADYALQBsAGUAIAB3AGkAdABoACAAQgBPAE0A"
+ }
+}
+
+assert {
+ res.body: eq "this is encoded with utf16-le with BOM"
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test plain text utf8 with BOM response.bru b/packages/bruno-tests/collection/response-parsing/test plain text utf8 with BOM response.bru
new file mode 100644
index 000000000..055386d79
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test plain text utf8 with BOM response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test plain text utf8 with BOM response
+ type: http
+ seq: 17
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "text/plain; charset=utf8" },
+ "contentBase64": "77u/dGhpcyBpcyB1dGY4IGVuY29kZWQgd2l0aCBCT00sIHdoeSBub3Q/"
+ }
+}
+
+assert {
+ res.body: eq "this is utf8 encoded with BOM, why not?"
+}
diff --git a/packages/bruno-tests/collection/response-parsing/test xml response.bru b/packages/bruno-tests/collection/response-parsing/test xml response.bru
new file mode 100644
index 000000000..5e562ada2
--- /dev/null
+++ b/packages/bruno-tests/collection/response-parsing/test xml response.bru
@@ -0,0 +1,22 @@
+meta {
+ name: test xml response
+ type: http
+ seq: 9
+}
+
+post {
+ url: {{httpfaker}}/api/echo/custom
+ body: json
+ auth: none
+}
+
+body:json {
+ {
+ "headers": { "content-type": "application/xml" },
+ "content": "hello "
+ }
+}
+
+assert {
+ res.body: eq hello
+}
diff --git a/packages/bruno-tests/collection/scripting/api/bru/runRequest-1.bru b/packages/bruno-tests/collection/scripting/api/bru/runRequest-1.bru
new file mode 100644
index 000000000..95b87239f
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/bru/runRequest-1.bru
@@ -0,0 +1,60 @@
+meta {
+ name: runRequest-1
+ type: http
+ seq: 10
+}
+
+post {
+ url: {{echo-host}}
+ body: text
+ auth: none
+}
+
+body:text {
+ bruno
+}
+
+script:pre-request {
+ // reset values
+ bru.setVar('run-request-runtime-var', null);
+ bru.setEnvVar('run-request-env-var', null);
+ bru.setGlobalEnvVar('run-request-global-env-var', null);
+
+ // the above vars will be set in the below request
+ const resp = await bru.runRequest('scripting/api/bru/runRequest-2');
+
+ bru.setVar('run-request-resp', {
+ data: resp?.data,
+ statusText: resp?.statusText,
+ status: resp?.status
+ });
+}
+
+tests {
+ test("should get runtime var set in runRequest-2", function() {
+ const val = bru.getVar("run-request-runtime-var");
+ expect(val).to.equal("run-request-runtime-var-value");
+ });
+
+ test("should get env var set in runRequest-2", function() {
+ const val = bru.getEnvVar("run-request-env-var");
+ expect(val).to.equal("run-request-env-var-value");
+ });
+
+ test("should get global env var set in runRequest-2", function() {
+ const val = bru.getGlobalEnvVar("run-request-global-env-var");
+ const executionMode = req.getExecutionMode();
+ if (executionMode == 'runner') {
+ expect(val).to.equal("run-request-global-env-var-value");
+ }
+ });
+
+ test("should get response of runRequest-2", function() {
+ const val = bru.getVar('run-request-resp');
+ expect(JSON.stringify(val)).to.equal(JSON.stringify({
+ "data": "bruno",
+ "statusText": "OK",
+ "status": 200
+ }));
+ });
+}
diff --git a/packages/bruno-tests/collection/scripting/api/bru/runRequest-2.bru b/packages/bruno-tests/collection/scripting/api/bru/runRequest-2.bru
new file mode 100644
index 000000000..7a5f4d08d
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/bru/runRequest-2.bru
@@ -0,0 +1,21 @@
+meta {
+ name: runRequest-2
+ type: http
+ seq: 11
+}
+
+post {
+ url: {{echo-host}}
+ body: text
+ auth: none
+}
+
+body:text {
+ bruno
+}
+
+script:pre-request {
+ bru.setVar('run-request-runtime-var', 'run-request-runtime-var-value');
+ bru.setEnvVar('run-request-env-var', 'run-request-env-var-value');
+ bru.setGlobalEnvVar('run-request-global-env-var', 'run-request-global-env-var-value');
+}
diff --git a/packages/bruno-tests/collection/scripting/api/bru/runRequest.bru b/packages/bruno-tests/collection/scripting/api/bru/runRequest.bru
new file mode 100644
index 000000000..7eb0e332c
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/bru/runRequest.bru
@@ -0,0 +1,96 @@
+meta {
+ name: runRequest
+ type: http
+ seq: 2
+}
+
+post {
+ url: {{host}}/api/echo/json
+ body: json
+ auth: none
+}
+
+headers {
+ foo: bar
+}
+
+auth:basic {
+ username: asd
+ password: j
+}
+
+auth:bearer {
+ token:
+}
+
+body:json {
+ {
+ "hello": "bruno"
+ }
+}
+
+assert {
+ res.status: eq 200
+}
+
+script:pre-request {
+ bru.setVar("runRequest-ping-res-1", null);
+ bru.setVar("runRequest-ping-res-2", null);
+ bru.setVar("runRequest-ping-res-3", null);
+
+ let pingRes = await bru.runRequest('ping');
+ bru.setVar('runRequest-ping-res-1', {
+ data: pingRes?.data,
+ statusText: pingRes?.statusText,
+ status: pingRes?.status
+ });
+}
+
+script:post-response {
+ let pingRes = await bru.runRequest('ping');
+ bru.setVar('runRequest-ping-res-2', {
+ data: pingRes?.data,
+ statusText: pingRes?.statusText,
+ status: pingRes?.status
+ });
+}
+
+tests {
+ const pingRes = await bru.runRequest('ping');
+ bru.setVar('runRequest-ping-res-3', {
+ data: pingRes?.data,
+ statusText: pingRes?.statusText,
+ status: pingRes?.status
+ });
+
+ test("should run request and return valid response in pre-request script", function() {
+ const expectedPingRes = {
+ data: "pong",
+ statusText: "OK",
+ status: 200
+ };
+ const pingRes = bru.getVar('runRequest-ping-res-1');
+ expect(pingRes).to.eql(expectedPingRes);
+ });
+
+ test("should run request and return valid response in post-response script", function() {
+ const expectedPingRes = {
+ data: "pong",
+ statusText: "OK",
+ status: 200
+ };
+ const pingRes = bru.getVar('runRequest-ping-res-2');
+ expect(pingRes).to.eql(expectedPingRes);
+ });
+
+ test("should run request and return valid response in tests script", function() {
+ const expectedPingRes = {
+ data: "pong",
+ statusText: "OK",
+ status: 200
+ };
+ const pingRes = bru.getVar('runRequest-ping-res-3');
+ expect(pingRes).to.eql(expectedPingRes);
+ });
+
+}
diff --git a/packages/bruno-tests/collection/scripting/api/bru/runner/1.bru b/packages/bruno-tests/collection/scripting/api/bru/runner/1.bru
new file mode 100644
index 000000000..97a7edbb6
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/bru/runner/1.bru
@@ -0,0 +1,19 @@
+meta {
+ name: 1
+ type: http
+ seq: 1
+}
+
+post {
+ url: https://echo.usebruno.com
+ body: none
+ auth: none
+}
+
+script:pre-request {
+ bru.setVar('bru-runner-req', 1);
+}
+
+script:post-response {
+ bru.setVar('bru.runner.skipRequest', true);
+}
diff --git a/packages/bruno-tests/collection/scripting/api/bru/runner/2.bru b/packages/bruno-tests/collection/scripting/api/bru/runner/2.bru
new file mode 100644
index 000000000..b1be74b22
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/bru/runner/2.bru
@@ -0,0 +1,19 @@
+meta {
+ name: 2
+ type: http
+ seq: 2
+}
+
+post {
+ url: https://echo.usebruno.com
+ body: none
+ auth: none
+}
+
+script:pre-request {
+ bru.runner.skipRequest();
+}
+
+script:post-response {
+ bru.setVar('bru.runner.skipRequest', false);
+}
diff --git a/packages/bruno-tests/collection/scripting/api/bru/runner/3.bru b/packages/bruno-tests/collection/scripting/api/bru/runner/3.bru
new file mode 100644
index 000000000..4abe00b4c
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/bru/runner/3.bru
@@ -0,0 +1,11 @@
+meta {
+ name: 3
+ type: http
+ seq: 3
+}
+
+post {
+ url: https://echo.usebruno.com
+ body: none
+ auth: none
+}
diff --git a/packages/bruno-tests/collection/scripting/api/res/getStatusText.bru b/packages/bruno-tests/collection/scripting/api/res/getStatusText.bru
new file mode 100644
index 000000000..f023d217c
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/res/getStatusText.bru
@@ -0,0 +1,23 @@
+meta {
+ name: getStatusText
+ type: http
+ seq: 6
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+assert {
+ res.statusText: eq OK
+ res.body: eq pong
+}
+
+tests {
+ test("res.getStatusText()", function() {
+ const statusText = res.getStatusText()
+ expect(statusText).to.equal('OK');
+ });
+}
diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/cheerio/cheerio.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/cheerio/cheerio.bru
new file mode 100644
index 000000000..07aad76b2
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/inbuilt modules/cheerio/cheerio.bru
@@ -0,0 +1,42 @@
+meta {
+ name: cheerio
+ type: http
+ seq: 1
+}
+
+post {
+ url: https://echo.usebruno.com
+ body: text
+ auth: none
+}
+
+body:text {
+ Hello Bruno!
+}
+
+script:pre-request {
+ const cheerio = require('cheerio');
+
+ const $ = cheerio.load('Hello world ');
+
+ $('h2.title').text('Hello there!');
+ $('h2').addClass('welcome');
+
+ bru.setVar("cheerio-test-html", $.html());
+}
+
+tests {
+ const cheerio = require('cheerio');
+
+ test("cheerio html - from scripts", function() {
+ const expected = 'Hello there! ';
+ const html = bru.getVar('cheerio-test-html');
+ expect(html).to.eql(expected);
+ });
+
+ test("cheerio html - from tests", function() {
+ const expected = 'Hello Bruno! ';
+ const $ = cheerio.load(res.body);
+ expect($.html()).to.eql(expected);
+ });
+}
diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/xml2js/xml2js.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/xml2js/xml2js.bru
new file mode 100644
index 000000000..db8748ec3
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/inbuilt modules/xml2js/xml2js.bru
@@ -0,0 +1,41 @@
+meta {
+ name: xml2js
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{host}}/ping
+ body: none
+ auth: none
+}
+
+script:pre-request {
+ var parseString = require('xml2js').parseString;
+ var xml = "Hello xml2js! "
+ parseString(xml, function (err, result) {
+ bru.setVar("xml2js-test-result", result);
+ });
+}
+
+tests {
+ var parseString = require('xml2js').parseString;
+
+ test("xml2js parseString in scripts", function() {
+ const expected = {
+ root: 'Hello xml2js!'
+ };
+ const result = bru.getVar('xml2js-test-result');
+ expect(result).to.eql(expected);
+ });
+
+ test("xml2js parseString in tests", async function() {
+ var xml = "Hello inside test! "
+ const expected = {
+ root: 'Hello inside test!'
+ };
+ parseString(xml, function (err, result) {
+ expect(result).to.eql(expected);
+ });
+ });
+}
diff --git a/packages/bruno-tests/collection_oauth2/bruno.json b/packages/bruno-tests/collection_oauth2/bruno.json
index 66949e685..82816b2b5 100644
--- a/packages/bruno-tests/collection_oauth2/bruno.json
+++ b/packages/bruno-tests/collection_oauth2/bruno.json
@@ -1,9 +1,11 @@
{
"version": "1",
- "name": "collection_oauth2",
+ "name": "OAuth2 Demo",
"type": "collection",
"scripts": {
- "moduleWhitelist": ["crypto"],
+ "moduleWhitelist": [
+ "crypto"
+ ],
"filesystemAccess": {
"allow": true
}
@@ -15,4 +17,4 @@
"presets": {
"requestType": "http"
}
-}
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/package.json b/packages/bruno-tests/package.json
index ad819bf1d..75cfde1ae 100644
--- a/packages/bruno-tests/package.json
+++ b/packages/bruno-tests/package.json
@@ -18,13 +18,13 @@
},
"homepage": "https://github.com/usebruno/bruno-testbench#readme",
"dependencies": {
- "axios": "1.7.5",
+ "axios": "^1.8.3",
"body-parser": "1.20.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
- "express": "4.21.1",
+ "express": "^4.21.2",
"express-basic-auth": "^1.2.1",
- "fast-xml-parser": "^4.5.0",
+ "fast-xml-parser": "^5.0.8",
"http-proxy": "^1.18.1",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
diff --git a/packages/bruno-tests/src/echo/index.js b/packages/bruno-tests/src/echo/index.js
index ba9b403ae..00b50bd36 100644
--- a/packages/bruno-tests/src/echo/index.js
+++ b/packages/bruno-tests/src/echo/index.js
@@ -19,6 +19,17 @@ router.post('/xml-raw', (req, res) => {
return res.send(req.rawBody);
});
+router.post('/bin', (req, res) => {
+ const rawBody = req.body;
+
+ if (!rawBody || rawBody.length === 0) {
+ return res.status(400).send('No data received');
+ }
+
+ res.set('Content-Type', req.headers['content-type'] || 'application/octet-stream');
+ res.send(rawBody);
+});
+
router.get('/bom-json-test', (req, res) => {
const jsonData = {
message: 'Hello!',
@@ -37,4 +48,30 @@ router.get('/iso-enc', (req, res) => {
return res.send(Buffer.from(responseText, 'latin1'));
});
+router.post("/custom", (req, res) => {
+ const { headers, content, contentBase64, contentJSON, type } = req.body || {};
+
+ res._headers = {};
+
+ if (type) {
+ res.setHeader('Content-Type', type);
+ }
+
+ if (headers && typeof headers === 'object') {
+ Object.entries(headers).forEach(([key, value]) => {
+ res.setHeader(key, value);
+ });
+ }
+
+ if (contentBase64) {
+ res.write(Buffer.from(contentBase64, 'base64'));
+ } else if (contentJSON !== undefined) {
+ res.write(JSON.stringify(contentJSON));
+ } else if (content !== undefined) {
+ res.write(content);
+ }
+
+ return res.end();
+});
+
module.exports = router;
diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js
index 22f01ed76..735bd929b 100644
--- a/packages/bruno-tests/src/index.js
+++ b/packages/bruno-tests/src/index.js
@@ -11,10 +11,18 @@ const app = new express();
const port = process.env.PORT || 8081;
app.use(cors());
+
+const saveRawBody = (req, res, buf) => {
+ req.rawBuffer = Buffer.from(buf);
+ req.rawBody = buf.toString();
+};
+
+app.use(bodyParser.json({ verify: saveRawBody }));
+app.use(bodyParser.urlencoded({ extended: true, verify: saveRawBody }));
+app.use(bodyParser.text({ verify: saveRawBody }));
app.use(xmlParser());
-app.use(bodyParser.text());
-app.use(bodyParser.json());
-app.use(bodyParser.urlencoded({ extended: true }));
+app.use(express.raw({ type: '*/*', limit: '100mb', verify: saveRawBody }));
+
formDataParser.init(app, express);
app.use('/api/auth', authRouter);
diff --git a/readme.md b/readme.md
index b0c23ecb3..68b2d49d5 100644
--- a/readme.md
+++ b/readme.md
@@ -138,7 +138,7 @@ Or any version control system of your choice
## Important Links 📌
- [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269)
-- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
+- [Roadmap](https://www.usebruno.com/roadmap)
- [Documentation](https://docs.usebruno.com)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
- [Website](https://www.usebruno.com)
diff --git a/scripts/build-electron.js b/scripts/build-electron.js
index 9825c3a09..ab44dcbdf 100644
--- a/scripts/build-electron.js
+++ b/scripts/build-electron.js
@@ -78,14 +78,14 @@ async function main() {
console.log('The directory has been created successfully!');
// Copy build
- await copyFolderIfExists('packages/bruno-app/out', 'packages/bruno-electron/web');
+ await copyFolderIfExists('packages/bruno-app/dist', 'packages/bruno-electron/web');
// Change paths in next
const files = await fs.readdir('packages/bruno-electron/web');
for (const file of files) {
if (file.endsWith('.html')) {
let content = await fs.readFile(`packages/bruno-electron/web/${file}`, 'utf8');
- content = content.replace(/\/_next\//g, '_next/');
+ content = content.replace(/\/static/g, './static');
await fs.writeFile(`packages/bruno-electron/web/${file}`, content);
}
}