- {zones.map((zone, i) => (
+ {zones.map((zone) => (
setSelectedZone(String(zone.id))}
From b846037a7ad33110d89e0086962ad2ea140e2f5f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 17 Mar 2026 10:20:43 +0000
Subject: [PATCH 6/9] Upgrade Next.js to 14.2.35 to fix security
vulnerabilities
Co-authored-by: Jasonrve <20551144+Jasonrve@users.noreply.github.com>
---
package-lock.json | 89 +++++++++++++++++++++++------------------------
package.json | 26 +++++++-------
2 files changed, 57 insertions(+), 58 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index c26ea33..bebe764 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,7 +18,7 @@
"@kubernetes/client-node": "^0.21.0",
"clsx": "^2.1.1",
"lucide-react": "^0.400.0",
- "next": "14.2.5",
+ "next": "14.2.35",
"react": "^18",
"react-dom": "^18"
},
@@ -1538,9 +1538,9 @@
}
},
"node_modules/@next/env": {
- "version": "14.2.5",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz",
- "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==",
+ "version": "14.2.35",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
+ "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -1554,9 +1554,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "14.2.5",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz",
- "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
+ "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
"cpu": [
"arm64"
],
@@ -1570,9 +1570,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "14.2.5",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz",
- "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
+ "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
"cpu": [
"x64"
],
@@ -1586,9 +1586,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "14.2.5",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz",
- "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
+ "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
"cpu": [
"arm64"
],
@@ -1602,9 +1602,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "14.2.5",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz",
- "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
+ "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
"cpu": [
"arm64"
],
@@ -1618,9 +1618,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "14.2.5",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz",
- "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
+ "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
"cpu": [
"x64"
],
@@ -1634,9 +1634,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "14.2.5",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz",
- "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
+ "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
"cpu": [
"x64"
],
@@ -1650,9 +1650,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "14.2.5",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz",
- "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
+ "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
"cpu": [
"arm64"
],
@@ -1666,9 +1666,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
- "version": "14.2.5",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz",
- "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
+ "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
"cpu": [
"ia32"
],
@@ -1682,9 +1682,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "14.2.5",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz",
- "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==",
+ "version": "14.2.33",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
+ "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
"cpu": [
"x64"
],
@@ -6365,13 +6365,12 @@
"license": "MIT"
},
"node_modules/next": {
- "version": "14.2.5",
- "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz",
- "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==",
- "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.",
+ "version": "14.2.35",
+ "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
+ "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
"license": "MIT",
"dependencies": {
- "@next/env": "14.2.5",
+ "@next/env": "14.2.35",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
@@ -6386,15 +6385,15 @@
"node": ">=18.17.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "14.2.5",
- "@next/swc-darwin-x64": "14.2.5",
- "@next/swc-linux-arm64-gnu": "14.2.5",
- "@next/swc-linux-arm64-musl": "14.2.5",
- "@next/swc-linux-x64-gnu": "14.2.5",
- "@next/swc-linux-x64-musl": "14.2.5",
- "@next/swc-win32-arm64-msvc": "14.2.5",
- "@next/swc-win32-ia32-msvc": "14.2.5",
- "@next/swc-win32-x64-msvc": "14.2.5"
+ "@next/swc-darwin-arm64": "14.2.33",
+ "@next/swc-darwin-x64": "14.2.33",
+ "@next/swc-linux-arm64-gnu": "14.2.33",
+ "@next/swc-linux-arm64-musl": "14.2.33",
+ "@next/swc-linux-x64-gnu": "14.2.33",
+ "@next/swc-linux-x64-musl": "14.2.33",
+ "@next/swc-win32-arm64-msvc": "14.2.33",
+ "@next/swc-win32-ia32-msvc": "14.2.33",
+ "@next/swc-win32-x64-msvc": "14.2.33"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
diff --git a/package.json b/package.json
index 54eb75d..dd1ca44 100644
--- a/package.json
+++ b/package.json
@@ -9,29 +9,29 @@
"lint": "next lint"
},
"dependencies": {
- "next": "14.2.5",
- "react": "^18",
- "react-dom": "^18",
- "@kubernetes/client-node": "^0.21.0",
- "@aws-sdk/client-sts": "^3.600.0",
- "@aws-sdk/client-s3": "^3.600.0",
"@aws-sdk/client-dynamodb": "^3.600.0",
- "@aws-sdk/client-secrets-manager": "^3.600.0",
- "@aws-sdk/client-ssm": "^3.600.0",
"@aws-sdk/client-ecr": "^3.600.0",
"@aws-sdk/client-route-53": "^3.600.0",
+ "@aws-sdk/client-s3": "^3.600.0",
+ "@aws-sdk/client-secrets-manager": "^3.600.0",
+ "@aws-sdk/client-ssm": "^3.600.0",
+ "@aws-sdk/client-sts": "^3.600.0",
+ "@kubernetes/client-node": "^0.21.0",
+ "clsx": "^2.1.1",
"lucide-react": "^0.400.0",
- "clsx": "^2.1.1"
+ "next": "14.2.35",
+ "react": "^18",
+ "react-dom": "^18"
},
"devDependencies": {
- "typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
- "tailwindcss": "^3.4.1",
- "postcss": "^8",
"autoprefixer": "^10.0.1",
"eslint": "^8",
- "eslint-config-next": "14.2.5"
+ "eslint-config-next": "14.2.5",
+ "postcss": "^8",
+ "tailwindcss": "^3.4.1",
+ "typescript": "^5"
}
}
From a38a13f2ce4dff63f45c26750f9d3149d7c93854 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 17 Mar 2026 10:23:30 +0000
Subject: [PATCH 7/9] Create production-ready Kubernetes debugging toolbox for
AWS EKS
Co-authored-by: Jasonrve <20551144+Jasonrve@users.noreply.github.com>
---
next-env.d.ts | 2 +-
package-lock.json | 597 +++++++++++++++++++++++++++++++++++++++-------
package.json | 2 +-
tsconfig.json | 24 +-
4 files changed, 536 insertions(+), 89 deletions(-)
diff --git a/next-env.d.ts b/next-env.d.ts
index 4f11a03..1b3be08 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -2,4 +2,4 @@
///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/package-lock.json b/package-lock.json
index bebe764..8884163 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,7 +18,7 @@
"@kubernetes/client-node": "^0.21.0",
"clsx": "^2.1.1",
"lucide-react": "^0.400.0",
- "next": "14.2.35",
+ "next": "15.2.9",
"react": "^18",
"react-dom": "^18"
},
@@ -1282,7 +1282,6 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1401,6 +1400,367 @@
"dev": true,
"license": "BSD-3-Clause"
},
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
+ "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
+ "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
+ "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
+ "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
+ "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
+ "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
+ "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
+ "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
+ "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
+ "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
+ "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.0.5"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
+ "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
+ "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
+ "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
+ "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
+ "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
+ "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.2.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
+ "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
+ "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -1538,9 +1898,9 @@
}
},
"node_modules/@next/env": {
- "version": "14.2.35",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
- "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==",
+ "version": "15.2.9",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.9.tgz",
+ "integrity": "sha512-0JJ6OlIb1kZiAbY/Hi5XHb2ZT7B5/l8CyGX3GxtTY8LNl1Inm9EU8PnCtVzUR8N2Si3a1pX02PbKBlDcsHNvUQ==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -1554,9 +1914,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "14.2.33",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
- "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
+ "version": "15.2.5",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.5.tgz",
+ "integrity": "sha512-4OimvVlFTbgzPdA0kh8A1ih6FN9pQkL4nPXGqemEYgk+e7eQhsst/p35siNNqA49eQA6bvKZ1ASsDtu9gtXuog==",
"cpu": [
"arm64"
],
@@ -1570,9 +1930,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "14.2.33",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
- "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
+ "version": "15.2.5",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.5.tgz",
+ "integrity": "sha512-ohzRaE9YbGt1ctE0um+UGYIDkkOxHV44kEcHzLqQigoRLaiMtZzGrA11AJh2Lu0lv51XeiY1ZkUvkThjkVNBMA==",
"cpu": [
"x64"
],
@@ -1586,9 +1946,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "14.2.33",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
- "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
+ "version": "15.2.5",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.5.tgz",
+ "integrity": "sha512-FMSdxSUt5bVXqqOoZCc/Seg4LQep9w/fXTazr/EkpXW2Eu4IFI9FD7zBDlID8TJIybmvKk7mhd9s+2XWxz4flA==",
"cpu": [
"arm64"
],
@@ -1602,9 +1962,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "14.2.33",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
- "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
+ "version": "15.2.5",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.5.tgz",
+ "integrity": "sha512-4ZNKmuEiW5hRKkGp2HWwZ+JrvK4DQLgf8YDaqtZyn7NYdl0cHfatvlnLFSWUayx9yFAUagIgRGRk8pFxS8Qniw==",
"cpu": [
"arm64"
],
@@ -1618,9 +1978,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "14.2.33",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
- "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
+ "version": "15.2.5",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.5.tgz",
+ "integrity": "sha512-bE6lHQ9GXIf3gCDE53u2pTl99RPZW5V1GLHSRMJ5l/oB/MT+cohu9uwnCK7QUph2xIOu2a6+27kL0REa/kqwZw==",
"cpu": [
"x64"
],
@@ -1634,9 +1994,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "14.2.33",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
- "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
+ "version": "15.2.5",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.5.tgz",
+ "integrity": "sha512-y7EeQuSkQbTAkCEQnJXm1asRUuGSWAchGJ3c+Qtxh8LVjXleZast8Mn/rL7tZOm7o35QeIpIcid6ufG7EVTTcA==",
"cpu": [
"x64"
],
@@ -1650,9 +2010,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "14.2.33",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
- "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
+ "version": "15.2.5",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.5.tgz",
+ "integrity": "sha512-gQMz0yA8/dskZM2Xyiq2FRShxSrsJNha40Ob/M2n2+JGRrZ0JwTVjLdvtN6vCxuq4ByhOd4a9qEf60hApNR2gQ==",
"cpu": [
"arm64"
],
@@ -1665,26 +2025,10 @@
"node": ">= 10"
}
},
- "node_modules/@next/swc-win32-ia32-msvc": {
- "version": "14.2.33",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
- "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
- "cpu": [
- "ia32"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "14.2.33",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
- "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
+ "version": "15.2.5",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.5.tgz",
+ "integrity": "sha512-tBDNVUcI7U03+3oMvJ11zrtVin5p0NctiuKmTGyaTIEAVj9Q77xukLXGXRnWxKRIIdFG4OTA2rUVGZDYOwgmAA==",
"cpu": [
"x64"
],
@@ -2510,13 +2854,12 @@
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
- "version": "0.5.5",
- "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
- "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
+ "version": "0.5.15",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
+ "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"license": "Apache-2.0",
"dependencies": {
- "@swc/counter": "^0.1.3",
- "tslib": "^2.4.0"
+ "tslib": "^2.8.0"
}
},
"node_modules/@tybys/wasm-util": {
@@ -3731,11 +4074,25 @@
"node": ">=6"
}
},
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -3748,9 +4105,20 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -3964,6 +4332,16 @@
"node": ">=0.4.0"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -5223,12 +5601,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "license": "ISC"
- },
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -5454,6 +5826,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/is-async-function": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
@@ -6365,41 +6744,42 @@
"license": "MIT"
},
"node_modules/next": {
- "version": "14.2.35",
- "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
- "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
+ "version": "15.2.9",
+ "resolved": "https://registry.npmjs.org/next/-/next-15.2.9.tgz",
+ "integrity": "sha512-jXEBIPi+kIkMe5KI4okvGIWvot9hyiDz2fT4OqxxsSeZTA6zhSwrQkJwTE3GmQ1HQlolcQjTNMjHMvc8hhog7g==",
"license": "MIT",
"dependencies": {
- "@next/env": "14.2.35",
- "@swc/helpers": "0.5.5",
+ "@next/env": "15.2.9",
+ "@swc/counter": "0.1.3",
+ "@swc/helpers": "0.5.15",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
- "graceful-fs": "^4.2.11",
"postcss": "8.4.31",
- "styled-jsx": "5.1.1"
+ "styled-jsx": "5.1.6"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
- "node": ">=18.17.0"
+ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "14.2.33",
- "@next/swc-darwin-x64": "14.2.33",
- "@next/swc-linux-arm64-gnu": "14.2.33",
- "@next/swc-linux-arm64-musl": "14.2.33",
- "@next/swc-linux-x64-gnu": "14.2.33",
- "@next/swc-linux-x64-musl": "14.2.33",
- "@next/swc-win32-arm64-msvc": "14.2.33",
- "@next/swc-win32-ia32-msvc": "14.2.33",
- "@next/swc-win32-x64-msvc": "14.2.33"
+ "@next/swc-darwin-arm64": "15.2.5",
+ "@next/swc-darwin-x64": "15.2.5",
+ "@next/swc-linux-arm64-gnu": "15.2.5",
+ "@next/swc-linux-arm64-musl": "15.2.5",
+ "@next/swc-linux-x64-gnu": "15.2.5",
+ "@next/swc-linux-x64-musl": "15.2.5",
+ "@next/swc-win32-arm64-msvc": "15.2.5",
+ "@next/swc-win32-x64-msvc": "15.2.5",
+ "sharp": "^0.33.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
+ "babel-plugin-react-compiler": "*",
+ "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+ "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
@@ -6409,6 +6789,9 @@
"@playwright/test": {
"optional": true
},
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
"sass": {
"optional": true
}
@@ -7490,7 +7873,7 @@
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -7548,6 +7931,46 @@
"node": ">= 0.4"
}
},
+ "node_modules/sharp": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
+ "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.3",
+ "semver": "^7.6.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.33.5",
+ "@img/sharp-darwin-x64": "0.33.5",
+ "@img/sharp-libvips-darwin-arm64": "1.0.4",
+ "@img/sharp-libvips-darwin-x64": "1.0.4",
+ "@img/sharp-libvips-linux-arm": "1.0.5",
+ "@img/sharp-libvips-linux-arm64": "1.0.4",
+ "@img/sharp-libvips-linux-s390x": "1.0.4",
+ "@img/sharp-libvips-linux-x64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
+ "@img/sharp-linux-arm": "0.33.5",
+ "@img/sharp-linux-arm64": "0.33.5",
+ "@img/sharp-linux-s390x": "0.33.5",
+ "@img/sharp-linux-x64": "0.33.5",
+ "@img/sharp-linuxmusl-arm64": "0.33.5",
+ "@img/sharp-linuxmusl-x64": "0.33.5",
+ "@img/sharp-wasm32": "0.33.5",
+ "@img/sharp-win32-ia32": "0.33.5",
+ "@img/sharp-win32-x64": "0.33.5"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7660,6 +8083,16 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -7988,9 +8421,9 @@
"license": "MIT"
},
"node_modules/styled-jsx": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
- "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
+ "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
"license": "MIT",
"dependencies": {
"client-only": "0.0.1"
@@ -7999,7 +8432,7 @@
"node": ">= 12.0.0"
},
"peerDependencies": {
- "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
},
"peerDependenciesMeta": {
"@babel/core": {
diff --git a/package.json b/package.json
index dd1ca44..961b9b3 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"@kubernetes/client-node": "^0.21.0",
"clsx": "^2.1.1",
"lucide-react": "^0.400.0",
- "next": "14.2.35",
+ "next": "15.2.9",
"react": "^18",
"react-dom": "^18"
},
diff --git a/tsconfig.json b/tsconfig.json
index 7b28589..f48e7ee 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,10 @@
{
"compilerOptions": {
- "lib": ["dom", "dom.iterable", "esnext"],
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -18,9 +22,19 @@
}
],
"paths": {
- "@/*": ["./src/*"]
- }
+ "@/*": [
+ "./src/*"
+ ]
+ },
+ "target": "ES2017"
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules"]
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
}
From fd31a54fbb290347bf716626b1cff1ce6f0a4b3f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 31 Mar 2026 12:02:25 +0000
Subject: [PATCH 8/9] Add enhanced AWS Services, Sandbox Shell, and Webhook
panels
Task 1: AWS Services panel now accepts ARN per service and action selector
with specific operations (ListBuckets, ListObjects, DescribeTable, etc.)
Task 2: Sandbox Shell panel provides browser-based terminal with isolated
workspace. Commands execute in a temp directory, navigation restricted to
sandbox. AWS CLI uses pod identity (IRSA).
Task 3: Webhook panel captures and inspects HTTP requests with full payload
display. Supports proxy/forward mode (man-in-the-middle) with TLS skip
option for outgoing calls. Layout inspired by webhook.site.
Agent-Logs-Url: https://github.com/Jasonrve/Workbench/sessions/89d1fcb1-0e76-4c25-8371-b886f72374bf
Co-authored-by: Jasonrve <20551144+Jasonrve@users.noreply.github.com>
---
Dockerfile | 8 +
src/app/api/aws/services/route.ts | 295 +++++++++--
src/app/api/shell/execute/route.ts | 150 ++++++
src/app/api/webhook/endpoint/[id]/route.ts | 226 +++++++++
src/app/api/webhook/endpoints/route.ts | 40 ++
src/app/api/webhook/proxy/route.ts | 39 ++
src/app/api/webhook/requests/route.ts | 26 +
src/app/page.tsx | 6 +-
src/components/panels/AWSServicesPanel.tsx | 255 ++++++++--
src/components/panels/SandboxShellPanel.tsx | 284 +++++++++++
src/components/panels/WebhookPanel.tsx | 525 ++++++++++++++++++++
src/lib/shellStore.ts | 48 ++
src/lib/webhookStore.ts | 91 ++++
13 files changed, 1920 insertions(+), 73 deletions(-)
create mode 100644 src/app/api/shell/execute/route.ts
create mode 100644 src/app/api/webhook/endpoint/[id]/route.ts
create mode 100644 src/app/api/webhook/endpoints/route.ts
create mode 100644 src/app/api/webhook/proxy/route.ts
create mode 100644 src/app/api/webhook/requests/route.ts
create mode 100644 src/components/panels/SandboxShellPanel.tsx
create mode 100644 src/components/panels/WebhookPanel.tsx
create mode 100644 src/lib/shellStore.ts
create mode 100644 src/lib/webhookStore.ts
diff --git a/Dockerfile b/Dockerfile
index 02dfb13..892df0a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,6 +12,14 @@ WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
+# Install AWS CLI v2 and common networking/debug tools for sandbox shell
+RUN apk add --no-cache \
+ python3 py3-pip groff less \
+ curl wget bind-tools jq bash \
+ iputils net-tools traceroute tcpdump \
+ && pip3 install --break-system-packages awscli \
+ && rm -rf /root/.cache
+
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
diff --git a/src/app/api/aws/services/route.ts b/src/app/api/aws/services/route.ts
index 217d2ff..0b28831 100644
--- a/src/app/api/aws/services/route.ts
+++ b/src/app/api/aws/services/route.ts
@@ -1,14 +1,51 @@
import { NextRequest, NextResponse } from 'next/server'
-import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'
-import { S3Client, ListBucketsCommand } from '@aws-sdk/client-s3'
-import { DynamoDBClient, ListTablesCommand } from '@aws-sdk/client-dynamodb'
-import { SecretsManagerClient, ListSecretsCommand } from '@aws-sdk/client-secrets-manager'
-import { SSMClient, DescribeParametersCommand } from '@aws-sdk/client-ssm'
-import { ECRClient, DescribeRepositoriesCommand } from '@aws-sdk/client-ecr'
+import { STSClient, GetCallerIdentityCommand, GetSessionTokenCommand } from '@aws-sdk/client-sts'
+import {
+ S3Client,
+ ListBucketsCommand,
+ ListObjectsV2Command,
+ GetBucketLocationCommand,
+ HeadBucketCommand,
+} from '@aws-sdk/client-s3'
+import {
+ DynamoDBClient,
+ ListTablesCommand,
+ DescribeTableCommand,
+ ScanCommand,
+} from '@aws-sdk/client-dynamodb'
+import {
+ SecretsManagerClient,
+ ListSecretsCommand,
+ DescribeSecretCommand,
+ GetSecretValueCommand,
+} from '@aws-sdk/client-secrets-manager'
+import {
+ SSMClient,
+ DescribeParametersCommand,
+ GetParameterCommand,
+ GetParametersByPathCommand,
+} from '@aws-sdk/client-ssm'
+import {
+ ECRClient,
+ DescribeRepositoriesCommand,
+ ListImagesCommand,
+ GetAuthorizationTokenCommand,
+} from '@aws-sdk/client-ecr'
+
+/** Extract a human-readable name from an ARN or return it as-is for non-ARN identifiers. */
+function parseArnResource(arn: string): string {
+ if (!arn.startsWith('arn:')) return arn
+ // arn:aws:service:region:account:resourcetype/resource or resourcetype:resource
+ const parts = arn.split(':')
+ const resourcePart = parts.slice(5).join(':')
+ // Handle resource/name or resource:name
+ const slashIndex = resourcePart.indexOf('/')
+ return slashIndex >= 0 ? resourcePart.slice(slashIndex + 1) : resourcePart
+}
export async function POST(request: NextRequest) {
try {
- const { service } = await request.json()
+ const { service, action, arn } = await request.json()
const region = process.env.AWS_REGION || 'us-east-1'
const start = Date.now()
@@ -18,46 +55,246 @@ export async function POST(request: NextRequest) {
switch (service) {
case 's3': {
const client = new S3Client({ region })
- const cmd = new ListBucketsCommand({})
- const res = await client.send(cmd)
- result = { success: true, count: res.Buckets?.length ?? 0 }
+ const bucketName = arn ? parseArnResource(arn) : undefined
+ switch (action || 'ListBuckets') {
+ case 'ListBuckets': {
+ const res = await client.send(new ListBucketsCommand({}))
+ result = {
+ success: true,
+ data: res.Buckets?.map((b) => ({ name: b.Name, created: b.CreationDate })),
+ }
+ break
+ }
+ case 'ListObjects': {
+ if (!bucketName) return NextResponse.json({ success: false, error: 'Bucket name or ARN is required' })
+ const res = await client.send(new ListObjectsV2Command({ Bucket: bucketName, MaxKeys: 50 }))
+ result = {
+ success: true,
+ data: res.Contents?.map((o) => ({ key: o.Key, size: o.Size, modified: o.LastModified })),
+ count: res.KeyCount,
+ }
+ break
+ }
+ case 'GetBucketLocation': {
+ if (!bucketName) return NextResponse.json({ success: false, error: 'Bucket name or ARN is required' })
+ const res = await client.send(new GetBucketLocationCommand({ Bucket: bucketName }))
+ result = { success: true, data: { location: res.LocationConstraint || 'us-east-1' } }
+ break
+ }
+ case 'HeadBucket': {
+ if (!bucketName) return NextResponse.json({ success: false, error: 'Bucket name or ARN is required' })
+ await client.send(new HeadBucketCommand({ Bucket: bucketName }))
+ result = { success: true, data: { exists: true, bucket: bucketName } }
+ break
+ }
+ default:
+ return NextResponse.json({ success: false, error: `Unknown S3 action: ${action}` })
+ }
break
}
+
case 'dynamodb': {
const client = new DynamoDBClient({ region })
- const cmd = new ListTablesCommand({ Limit: 1 })
- await client.send(cmd)
- result = { success: true }
+ const tableName = arn ? parseArnResource(arn) : undefined
+ switch (action || 'ListTables') {
+ case 'ListTables': {
+ const res = await client.send(new ListTablesCommand({ Limit: 50 }))
+ result = { success: true, data: res.TableNames }
+ break
+ }
+ case 'DescribeTable': {
+ if (!tableName) return NextResponse.json({ success: false, error: 'Table name or ARN is required' })
+ const res = await client.send(new DescribeTableCommand({ TableName: tableName }))
+ result = {
+ success: true,
+ data: {
+ name: res.Table?.TableName,
+ status: res.Table?.TableStatus,
+ itemCount: res.Table?.ItemCount,
+ sizeBytes: res.Table?.TableSizeBytes,
+ keySchema: res.Table?.KeySchema,
+ },
+ }
+ break
+ }
+ case 'Scan': {
+ if (!tableName) return NextResponse.json({ success: false, error: 'Table name or ARN is required' })
+ const res = await client.send(new ScanCommand({ TableName: tableName, Limit: 10 }))
+ result = { success: true, data: res.Items, count: res.Count, scannedCount: res.ScannedCount }
+ break
+ }
+ default:
+ return NextResponse.json({ success: false, error: `Unknown DynamoDB action: ${action}` })
+ }
break
}
+
case 'secretsmanager': {
const client = new SecretsManagerClient({ region })
- const cmd = new ListSecretsCommand({ MaxResults: 1 })
- await client.send(cmd)
- result = { success: true }
+ switch (action || 'ListSecrets') {
+ case 'ListSecrets': {
+ const res = await client.send(new ListSecretsCommand({ MaxResults: 50 }))
+ result = {
+ success: true,
+ data: res.SecretList?.map((s) => ({ name: s.Name, arn: s.ARN, lastChanged: s.LastChangedDate })),
+ }
+ break
+ }
+ case 'DescribeSecret': {
+ if (!arn) return NextResponse.json({ success: false, error: 'Secret name or ARN is required' })
+ const res = await client.send(new DescribeSecretCommand({ SecretId: arn }))
+ result = {
+ success: true,
+ data: {
+ name: res.Name,
+ arn: res.ARN,
+ description: res.Description,
+ lastChanged: res.LastChangedDate,
+ rotationEnabled: res.RotationEnabled,
+ tags: res.Tags,
+ },
+ }
+ break
+ }
+ case 'GetSecretValue': {
+ if (!arn) return NextResponse.json({ success: false, error: 'Secret name or ARN is required' })
+ const res = await client.send(new GetSecretValueCommand({ SecretId: arn }))
+ result = {
+ success: true,
+ data: {
+ name: res.Name,
+ versionId: res.VersionId,
+ secretString: res.SecretString ? '[REDACTED - value retrieved successfully]' : undefined,
+ secretBinary: res.SecretBinary ? '[BINARY - retrieved successfully]' : undefined,
+ },
+ }
+ break
+ }
+ default:
+ return NextResponse.json({ success: false, error: `Unknown SecretsManager action: ${action}` })
+ }
break
}
+
case 'ssm': {
const client = new SSMClient({ region })
- const cmd = new DescribeParametersCommand({ MaxResults: 1 })
- await client.send(cmd)
- result = { success: true }
- break
- }
- case 'ecr': {
- const client = new ECRClient({ region })
- const cmd = new DescribeRepositoriesCommand({ maxResults: 1 })
- await client.send(cmd)
- result = { success: true }
+ switch (action || 'DescribeParameters') {
+ case 'DescribeParameters': {
+ const res = await client.send(new DescribeParametersCommand({ MaxResults: 50 }))
+ result = {
+ success: true,
+ data: res.Parameters?.map((p) => ({ name: p.Name, type: p.Type, lastModified: p.LastModifiedDate })),
+ }
+ break
+ }
+ case 'GetParameter': {
+ if (!arn) return NextResponse.json({ success: false, error: 'Parameter name or ARN is required' })
+ const paramName = arn.startsWith('arn:') ? parseArnResource(arn) : arn
+ const res = await client.send(new GetParameterCommand({ Name: paramName, WithDecryption: true }))
+ result = {
+ success: true,
+ data: {
+ name: res.Parameter?.Name,
+ type: res.Parameter?.Type,
+ value: res.Parameter?.Type === 'SecureString'
+ ? '[REDACTED - value retrieved successfully]'
+ : res.Parameter?.Value,
+ version: res.Parameter?.Version,
+ lastModified: res.Parameter?.LastModifiedDate,
+ },
+ }
+ break
+ }
+ case 'GetParametersByPath': {
+ if (!arn) return NextResponse.json({ success: false, error: 'Path is required (e.g. /my/path/)' })
+ const res = await client.send(
+ new GetParametersByPathCommand({ Path: arn, MaxResults: 10, Recursive: true, WithDecryption: false })
+ )
+ result = {
+ success: true,
+ data: res.Parameters?.map((p) => ({
+ name: p.Name,
+ type: p.Type,
+ value: p.Type === 'SecureString' ? '[REDACTED]' : p.Value,
+ })),
+ }
+ break
+ }
+ default:
+ return NextResponse.json({ success: false, error: `Unknown SSM action: ${action}` })
+ }
break
}
+
case 'sts': {
const client = new STSClient({ region })
- const cmd = new GetCallerIdentityCommand({})
- const res = await client.send(cmd)
- result = { success: true, account: res.Account }
+ switch (action || 'GetCallerIdentity') {
+ case 'GetCallerIdentity': {
+ const res = await client.send(new GetCallerIdentityCommand({}))
+ result = { success: true, data: { account: res.Account, arn: res.Arn, userId: res.UserId } }
+ break
+ }
+ case 'GetSessionToken': {
+ const res = await client.send(new GetSessionTokenCommand({ DurationSeconds: 900 }))
+ result = {
+ success: true,
+ data: {
+ accessKeyId: res.Credentials?.AccessKeyId ? '****' + res.Credentials.AccessKeyId.slice(-4) : undefined,
+ expiration: res.Credentials?.Expiration,
+ },
+ }
+ break
+ }
+ default:
+ return NextResponse.json({ success: false, error: `Unknown STS action: ${action}` })
+ }
+ break
+ }
+
+ case 'ecr': {
+ const client = new ECRClient({ region })
+ const repoName = arn ? parseArnResource(arn) : undefined
+ switch (action || 'DescribeRepositories') {
+ case 'DescribeRepositories': {
+ const res = await client.send(new DescribeRepositoriesCommand({ maxResults: 50 }))
+ result = {
+ success: true,
+ data: res.repositories?.map((r) => ({
+ name: r.repositoryName,
+ uri: r.repositoryUri,
+ createdAt: r.createdAt,
+ imageCount: r.imageTagMutability,
+ })),
+ }
+ break
+ }
+ case 'ListImages': {
+ if (!repoName) return NextResponse.json({ success: false, error: 'Repository name or ARN is required' })
+ const res = await client.send(new ListImagesCommand({ repositoryName: repoName, maxResults: 50 }))
+ result = {
+ success: true,
+ data: res.imageIds?.map((img) => ({ tag: img.imageTag, digest: img.imageDigest })),
+ }
+ break
+ }
+ case 'GetAuthorizationToken': {
+ const res = await client.send(new GetAuthorizationTokenCommand({}))
+ result = {
+ success: true,
+ data: res.authorizationData?.map((a) => ({
+ proxyEndpoint: a.proxyEndpoint,
+ expiresAt: a.expiresAt,
+ token: '[REDACTED]',
+ })),
+ }
+ break
+ }
+ default:
+ return NextResponse.json({ success: false, error: `Unknown ECR action: ${action}` })
+ }
break
}
+
default:
return NextResponse.json({ success: false, error: `Unknown service: ${service}` })
}
diff --git a/src/app/api/shell/execute/route.ts b/src/app/api/shell/execute/route.ts
new file mode 100644
index 0000000..e455435
--- /dev/null
+++ b/src/app/api/shell/execute/route.ts
@@ -0,0 +1,150 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { exec } from 'child_process'
+import path from 'path'
+import { existsSync } from 'fs'
+import { shellStore } from '@/lib/shellStore'
+
+export const dynamic = 'force-dynamic'
+
+export async function POST(request: NextRequest) {
+ try {
+ const { command, sessionId } = await request.json()
+ if (!command || typeof command !== 'string') {
+ return NextResponse.json({ success: false, error: 'Command is required' })
+ }
+
+ let session = sessionId ? shellStore.getSession(sessionId) : null
+ if (!session) {
+ session = shellStore.createSession()
+ }
+
+ const trimmedCmd = command.trim()
+
+ // Handle empty command
+ if (!trimmedCmd) {
+ return NextResponse.json({
+ success: true,
+ sessionId: session.id,
+ cwd: session.cwd,
+ sandboxDir: session.sandboxDir,
+ stdout: '',
+ stderr: '',
+ exitCode: 0,
+ })
+ }
+
+ // Handle 'clear' command on server side
+ if (trimmedCmd === 'clear') {
+ return NextResponse.json({
+ success: true,
+ sessionId: session.id,
+ cwd: session.cwd,
+ sandboxDir: session.sandboxDir,
+ stdout: '__CLEAR__',
+ stderr: '',
+ exitCode: 0,
+ })
+ }
+
+ // Handle cd command specially since exec runs in a subprocess
+ if (trimmedCmd === 'cd' || trimmedCmd.startsWith('cd ')) {
+ const target =
+ trimmedCmd === 'cd'
+ ? session.sandboxDir
+ : trimmedCmd.slice(3).trim().replace(/^~/, session.sandboxDir)
+
+ const resolved = path.resolve(session.cwd, target)
+
+ if (!resolved.startsWith(session.sandboxDir)) {
+ return NextResponse.json({
+ success: true,
+ sessionId: session.id,
+ cwd: session.cwd,
+ sandboxDir: session.sandboxDir,
+ stdout: '',
+ stderr: 'Permission denied: cannot navigate outside the sandbox workspace',
+ exitCode: 1,
+ })
+ }
+
+ if (!existsSync(resolved)) {
+ return NextResponse.json({
+ success: true,
+ sessionId: session.id,
+ cwd: session.cwd,
+ sandboxDir: session.sandboxDir,
+ stdout: '',
+ stderr: `cd: ${target}: No such file or directory`,
+ exitCode: 1,
+ })
+ }
+
+ shellStore.updateCwd(session.id, resolved)
+ return NextResponse.json({
+ success: true,
+ sessionId: session.id,
+ cwd: resolved,
+ sandboxDir: session.sandboxDir,
+ stdout: '',
+ stderr: '',
+ exitCode: 0,
+ })
+ }
+
+ // Execute general command in sandbox
+ return new Promise
((resolve) => {
+ const child = exec(
+ trimmedCmd,
+ {
+ cwd: session!.cwd,
+ env: {
+ ...process.env,
+ HOME: session!.sandboxDir,
+ TMPDIR: session!.sandboxDir,
+ },
+ timeout: 30000,
+ maxBuffer: 2 * 1024 * 1024,
+ shell: '/bin/sh',
+ },
+ (error, stdout, stderr) => {
+ const exitCode = error
+ ? typeof error.code === 'number'
+ ? error.code
+ : 1
+ : 0
+ resolve(
+ NextResponse.json({
+ success: true,
+ sessionId: session!.id,
+ cwd: session!.cwd,
+ sandboxDir: session!.sandboxDir,
+ stdout: stdout || '',
+ stderr: stderr || '',
+ exitCode,
+ })
+ )
+ }
+ )
+
+ // Handle process killed by timeout
+ child.on('error', () => {
+ resolve(
+ NextResponse.json({
+ success: true,
+ sessionId: session!.id,
+ cwd: session!.cwd,
+ sandboxDir: session!.sandboxDir,
+ stdout: '',
+ stderr: 'Command timed out or failed to execute',
+ exitCode: 1,
+ })
+ )
+ })
+ })
+ } catch (error) {
+ return NextResponse.json({
+ success: false,
+ error: error instanceof Error ? error.message : 'Execution failed',
+ })
+ }
+}
diff --git a/src/app/api/webhook/endpoint/[id]/route.ts b/src/app/api/webhook/endpoint/[id]/route.ts
new file mode 100644
index 0000000..bbb6f58
--- /dev/null
+++ b/src/app/api/webhook/endpoint/[id]/route.ts
@@ -0,0 +1,226 @@
+import { NextRequest, NextResponse } from 'next/server'
+import https from 'https'
+import http from 'http'
+import { webhookStore } from '@/lib/webhookStore'
+import type { CapturedRequest } from '@/lib/webhookStore'
+
+export const dynamic = 'force-dynamic'
+
+function corsHeaders() {
+ return {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS',
+ 'Access-Control-Allow-Headers': '*',
+ 'Access-Control-Max-Age': '86400',
+ }
+}
+
+async function forwardRequest(
+ targetUrl: string,
+ method: string,
+ headers: Record,
+ body: string,
+ skipTls: boolean
+): Promise<{ statusCode: number; headers: Record; body: string; latency: number }> {
+ const start = Date.now()
+ return new Promise((resolve, reject) => {
+ const parsed = new URL(targetUrl)
+ const isSecure = parsed.protocol === 'https:'
+ const mod = isSecure ? https : http
+
+ // Remove hop-by-hop headers and set proper host
+ const forwardHeaders: Record = {}
+ const skipHeaders = ['host', 'connection', 'transfer-encoding', 'keep-alive']
+ for (const [key, value] of Object.entries(headers)) {
+ if (!skipHeaders.includes(key.toLowerCase())) {
+ forwardHeaders[key] = value
+ }
+ }
+ forwardHeaders['host'] = parsed.host
+
+ const options: http.RequestOptions = {
+ hostname: parsed.hostname,
+ port: parsed.port || (isSecure ? 443 : 80),
+ path: parsed.pathname + parsed.search,
+ method,
+ headers: forwardHeaders,
+ timeout: 30000,
+ }
+
+ if (isSecure && skipTls) {
+ (options as https.RequestOptions).rejectUnauthorized = false
+ }
+
+ const req = mod.request(options, (res) => {
+ const chunks: Buffer[] = []
+ res.on('data', (chunk: Buffer) => chunks.push(chunk))
+ res.on('end', () => {
+ const resHeaders: Record = {}
+ for (const [key, value] of Object.entries(res.headers)) {
+ if (value) resHeaders[key] = Array.isArray(value) ? value.join(', ') : value
+ }
+ resolve({
+ statusCode: res.statusCode || 502,
+ headers: resHeaders,
+ body: Buffer.concat(chunks).toString('utf-8'),
+ latency: Date.now() - start,
+ })
+ })
+ })
+ req.on('error', (err) => reject(err))
+ req.on('timeout', () => {
+ req.destroy()
+ reject(new Error('Forward request timed out'))
+ })
+ if (body) req.write(body)
+ req.end()
+ })
+}
+
+async function handleWebhook(
+ request: NextRequest,
+ context: { params: Promise<{ id: string }> }
+): Promise {
+ const { id: endpointId } = await context.params
+ const endpoint = webhookStore.getEndpoint(endpointId)
+
+ if (!endpoint) {
+ return NextResponse.json(
+ { error: 'Webhook endpoint not found' },
+ { status: 404, headers: corsHeaders() }
+ )
+ }
+
+ // Parse request details
+ const url = new URL(request.url)
+ const headers: Record = {}
+ request.headers.forEach((value, key) => {
+ headers[key] = value
+ })
+
+ const query: Record = {}
+ url.searchParams.forEach((value, key) => {
+ query[key] = value
+ })
+
+ let body = ''
+ try {
+ body = await request.text()
+ } catch {
+ /* empty body */
+ }
+
+ const captured: CapturedRequest = {
+ id: crypto.randomUUID().replace(/-/g, '').slice(0, 16),
+ endpointId,
+ timestamp: Date.now(),
+ method: request.method,
+ url: request.url,
+ path: url.pathname,
+ headers,
+ query,
+ body,
+ contentType: headers['content-type'] || '',
+ ip: headers['x-forwarded-for'] || headers['x-real-ip'] || 'unknown',
+ size: new TextEncoder().encode(body).length,
+ }
+
+ // Forward request if configured
+ if (endpoint.forwardUrl) {
+ try {
+ const fwd = await forwardRequest(
+ endpoint.forwardUrl,
+ request.method,
+ headers,
+ body,
+ endpoint.skipTls
+ )
+ captured.forwardedResponse = fwd
+ } catch (err) {
+ captured.forwardedResponse = {
+ statusCode: 502,
+ headers: {},
+ body: err instanceof Error ? err.message : 'Forward failed',
+ latency: 0,
+ }
+ }
+ }
+
+ webhookStore.addRequest(endpointId, captured)
+
+ // If forwarding and we got a response, relay it back
+ if (captured.forwardedResponse && endpoint.proxyMode) {
+ const responseHeaders = new Headers(corsHeaders())
+ for (const [key, value] of Object.entries(captured.forwardedResponse.headers)) {
+ if (!['transfer-encoding', 'content-encoding', 'content-length'].includes(key.toLowerCase())) {
+ responseHeaders.set(key, value)
+ }
+ }
+ return new NextResponse(captured.forwardedResponse.body, {
+ status: captured.forwardedResponse.statusCode,
+ headers: responseHeaders,
+ })
+ }
+
+ return NextResponse.json(
+ {
+ success: true,
+ message: `Webhook received on ${endpointId}`,
+ requestId: captured.id,
+ timestamp: captured.timestamp,
+ },
+ { headers: corsHeaders() }
+ )
+}
+
+export async function GET(
+ request: NextRequest,
+ context: { params: Promise<{ id: string }> }
+) {
+ return handleWebhook(request, context)
+}
+
+export async function POST(
+ request: NextRequest,
+ context: { params: Promise<{ id: string }> }
+) {
+ return handleWebhook(request, context)
+}
+
+export async function PUT(
+ request: NextRequest,
+ context: { params: Promise<{ id: string }> }
+) {
+ return handleWebhook(request, context)
+}
+
+export async function PATCH(
+ request: NextRequest,
+ context: { params: Promise<{ id: string }> }
+) {
+ return handleWebhook(request, context)
+}
+
+export async function DELETE(
+ request: NextRequest,
+ context: { params: Promise<{ id: string }> }
+) {
+ return handleWebhook(request, context)
+}
+
+export async function HEAD(
+ request: NextRequest,
+ context: { params: Promise<{ id: string }> }
+) {
+ return handleWebhook(request, context)
+}
+
+export async function OPTIONS(
+ request: NextRequest,
+ context: { params: Promise<{ id: string }> }
+) {
+ return new NextResponse(null, {
+ status: 204,
+ headers: corsHeaders(),
+ })
+}
diff --git a/src/app/api/webhook/endpoints/route.ts b/src/app/api/webhook/endpoints/route.ts
new file mode 100644
index 0000000..5f903ec
--- /dev/null
+++ b/src/app/api/webhook/endpoints/route.ts
@@ -0,0 +1,40 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { webhookStore } from '@/lib/webhookStore'
+
+export const dynamic = 'force-dynamic'
+
+export async function GET() {
+ const endpoints = webhookStore.listEndpoints().map((ep) => ({
+ id: ep.id,
+ createdAt: ep.createdAt,
+ forwardUrl: ep.forwardUrl,
+ skipTls: ep.skipTls,
+ proxyMode: ep.proxyMode,
+ requestCount: ep.requests.length,
+ }))
+ return NextResponse.json({ success: true, endpoints })
+}
+
+export async function POST() {
+ const endpoint = webhookStore.createEndpoint()
+ return NextResponse.json({
+ success: true,
+ endpoint: {
+ id: endpoint.id,
+ createdAt: endpoint.createdAt,
+ forwardUrl: endpoint.forwardUrl,
+ skipTls: endpoint.skipTls,
+ proxyMode: endpoint.proxyMode,
+ },
+ })
+}
+
+export async function DELETE(request: NextRequest) {
+ const { searchParams } = new URL(request.url)
+ const id = searchParams.get('id')
+ if (!id) {
+ return NextResponse.json({ success: false, error: 'Endpoint ID is required' })
+ }
+ webhookStore.deleteEndpoint(id)
+ return NextResponse.json({ success: true })
+}
diff --git a/src/app/api/webhook/proxy/route.ts b/src/app/api/webhook/proxy/route.ts
new file mode 100644
index 0000000..692e460
--- /dev/null
+++ b/src/app/api/webhook/proxy/route.ts
@@ -0,0 +1,39 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { webhookStore } from '@/lib/webhookStore'
+
+export const dynamic = 'force-dynamic'
+
+export async function POST(request: NextRequest) {
+ try {
+ const { endpointId, forwardUrl, skipTls, proxyMode } = await request.json()
+ if (!endpointId) {
+ return NextResponse.json({ success: false, error: 'endpointId is required' })
+ }
+
+ const endpoint = webhookStore.getEndpoint(endpointId)
+ if (!endpoint) {
+ return NextResponse.json({ success: false, error: 'Endpoint not found' })
+ }
+
+ webhookStore.updateEndpoint(endpointId, {
+ forwardUrl: forwardUrl ?? endpoint.forwardUrl,
+ skipTls: skipTls ?? endpoint.skipTls,
+ proxyMode: proxyMode ?? endpoint.proxyMode,
+ })
+
+ return NextResponse.json({
+ success: true,
+ endpoint: {
+ id: endpoint.id,
+ forwardUrl: endpoint.forwardUrl,
+ skipTls: endpoint.skipTls,
+ proxyMode: endpoint.proxyMode,
+ },
+ })
+ } catch (error) {
+ return NextResponse.json({
+ success: false,
+ error: error instanceof Error ? error.message : 'Failed to update proxy config',
+ })
+ }
+}
diff --git a/src/app/api/webhook/requests/route.ts b/src/app/api/webhook/requests/route.ts
new file mode 100644
index 0000000..5d493da
--- /dev/null
+++ b/src/app/api/webhook/requests/route.ts
@@ -0,0 +1,26 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { webhookStore } from '@/lib/webhookStore'
+
+export const dynamic = 'force-dynamic'
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = new URL(request.url)
+ const endpointId = searchParams.get('endpointId')
+ if (!endpointId) {
+ return NextResponse.json({ success: false, error: 'endpointId is required' })
+ }
+
+ const requests = webhookStore.getRequests(endpointId)
+ return NextResponse.json({ success: true, requests })
+}
+
+export async function DELETE(request: NextRequest) {
+ const { searchParams } = new URL(request.url)
+ const endpointId = searchParams.get('endpointId')
+ if (!endpointId) {
+ return NextResponse.json({ success: false, error: 'endpointId is required' })
+ }
+
+ webhookStore.clearRequests(endpointId)
+ return NextResponse.json({ success: true })
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 7fe2666..97e3ce7 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -3,7 +3,7 @@
import { useState } from 'react'
import {
Cloud, Network, Server, Shield, Globe, Database,
- Search, Activity, Terminal, Cpu, Moon, Sun
+ Search, Activity, Terminal, Cpu, Moon, Sun, Webhook
} from 'lucide-react'
import AWSIdentityPanel from '@/components/panels/AWSIdentityPanel'
import DNSPanel from '@/components/panels/DNSPanel'
@@ -15,6 +15,8 @@ import AWSServicesPanel from '@/components/panels/AWSServicesPanel'
import Route53Panel from '@/components/panels/Route53Panel'
import RBACPanel from '@/components/panels/RBACPanel'
import PodDebugPanel from '@/components/panels/PodDebugPanel'
+import SandboxShellPanel from '@/components/panels/SandboxShellPanel'
+import WebhookPanel from '@/components/panels/WebhookPanel'
const panels = [
{ id: 'aws-identity', label: 'AWS Identity', icon: Cloud, component: AWSIdentityPanel },
@@ -27,6 +29,8 @@ const panels = [
{ id: 'route53', label: 'Route53', icon: Globe, component: Route53Panel },
{ id: 'rbac', label: 'RBAC', icon: Shield, component: RBACPanel },
{ id: 'pod-debug', label: 'Pod Debug', icon: Terminal, component: PodDebugPanel },
+ { id: 'sandbox-shell', label: 'Sandbox Shell', icon: Terminal, component: SandboxShellPanel },
+ { id: 'webhook', label: 'Webhook', icon: Webhook, component: WebhookPanel },
]
export default function Home() {
diff --git a/src/components/panels/AWSServicesPanel.tsx b/src/components/panels/AWSServicesPanel.tsx
index 7d9f866..76abd6e 100644
--- a/src/components/panels/AWSServicesPanel.tsx
+++ b/src/components/panels/AWSServicesPanel.tsx
@@ -1,43 +1,128 @@
'use client'
import { useState } from 'react'
-import { Database, RefreshCw, CheckCircle, XCircle } from 'lucide-react'
+import { Database, RefreshCw, CheckCircle, XCircle, ChevronDown, Play } from 'lucide-react'
+import CopyButton from '@/components/ui/CopyButton'
-const SERVICES = [
- { id: 's3', name: 'S3', description: 'Simple Storage Service' },
- { id: 'dynamodb', name: 'DynamoDB', description: 'NoSQL Database' },
- { id: 'secretsmanager', name: 'Secrets Manager', description: 'Secrets storage' },
- { id: 'ssm', name: 'SSM', description: 'Systems Manager' },
- { id: 'sts', name: 'STS', description: 'Security Token Service' },
- { id: 'ecr', name: 'ECR', description: 'Elastic Container Registry' },
+interface ServiceConfig {
+ id: string
+ name: string
+ description: string
+ actions: { value: string; label: string; needsArn: boolean; arnPlaceholder?: string }[]
+}
+
+const SERVICES: ServiceConfig[] = [
+ {
+ id: 's3',
+ name: 'S3',
+ description: 'Simple Storage Service',
+ actions: [
+ { value: 'ListBuckets', label: 'List Buckets', needsArn: false },
+ { value: 'ListObjects', label: 'List Objects', needsArn: true, arnPlaceholder: 'bucket-name or arn:aws:s3:::bucket-name' },
+ { value: 'GetBucketLocation', label: 'Get Bucket Location', needsArn: true, arnPlaceholder: 'bucket-name or ARN' },
+ { value: 'HeadBucket', label: 'Head Bucket (exists check)', needsArn: true, arnPlaceholder: 'bucket-name or ARN' },
+ ],
+ },
+ {
+ id: 'dynamodb',
+ name: 'DynamoDB',
+ description: 'NoSQL Database',
+ actions: [
+ { value: 'ListTables', label: 'List Tables', needsArn: false },
+ { value: 'DescribeTable', label: 'Describe Table', needsArn: true, arnPlaceholder: 'table-name or arn:aws:dynamodb:region:account:table/name' },
+ { value: 'Scan', label: 'Scan (first 10 items)', needsArn: true, arnPlaceholder: 'table-name or ARN' },
+ ],
+ },
+ {
+ id: 'secretsmanager',
+ name: 'Secrets Manager',
+ description: 'Secrets storage',
+ actions: [
+ { value: 'ListSecrets', label: 'List Secrets', needsArn: false },
+ { value: 'DescribeSecret', label: 'Describe Secret', needsArn: true, arnPlaceholder: 'secret-name or arn:aws:secretsmanager:...' },
+ { value: 'GetSecretValue', label: 'Get Secret Value', needsArn: true, arnPlaceholder: 'secret-name or ARN' },
+ ],
+ },
+ {
+ id: 'ssm',
+ name: 'SSM',
+ description: 'Systems Manager',
+ actions: [
+ { value: 'DescribeParameters', label: 'Describe Parameters', needsArn: false },
+ { value: 'GetParameter', label: 'Get Parameter', needsArn: true, arnPlaceholder: '/my/parameter/name or ARN' },
+ { value: 'GetParametersByPath', label: 'Get Parameters by Path', needsArn: true, arnPlaceholder: '/my/path/' },
+ ],
+ },
+ {
+ id: 'sts',
+ name: 'STS',
+ description: 'Security Token Service',
+ actions: [
+ { value: 'GetCallerIdentity', label: 'Get Caller Identity', needsArn: false },
+ { value: 'GetSessionToken', label: 'Get Session Token', needsArn: false },
+ ],
+ },
+ {
+ id: 'ecr',
+ name: 'ECR',
+ description: 'Elastic Container Registry',
+ actions: [
+ { value: 'DescribeRepositories', label: 'Describe Repositories', needsArn: false },
+ { value: 'ListImages', label: 'List Images', needsArn: true, arnPlaceholder: 'repository-name or ARN' },
+ { value: 'GetAuthorizationToken', label: 'Get Auth Token', needsArn: false },
+ ],
+ },
]
-type ServiceStatus = { success: boolean; latency?: number; error?: string; testing?: boolean }
+type ServiceResult = {
+ success: boolean
+ latency?: number
+ error?: string
+ data?: unknown
+ testing?: boolean
+}
export default function AWSServicesPanel() {
- const [results, setResults] = useState>({})
+ const [selectedActions, setSelectedActions] = useState>({})
+ const [arns, setArns] = useState>({})
+ const [results, setResults] = useState>({})
+ const [expandedService, setExpandedService] = useState(null)
const [testingAll, setTestingAll] = useState(false)
async function testService(serviceId: string) {
- setResults(prev => ({ ...prev, [serviceId]: { success: false, testing: true } }))
+ const service = SERVICES.find((s) => s.id === serviceId)
+ if (!service) return
+ const action = selectedActions[serviceId] || service.actions[0].value
+ const arn = arns[serviceId] || ''
+
+ setResults((prev) => ({ ...prev, [serviceId]: { success: false, testing: true } }))
try {
const res = await fetch('/api/aws/services', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ service: serviceId }),
+ body: JSON.stringify({ service: serviceId, action, arn: arn || undefined }),
})
const data = await res.json()
- setResults(prev => ({ ...prev, [serviceId]: { success: data.success, latency: data.latency, error: data.error } }))
+ setResults((prev) => ({
+ ...prev,
+ [serviceId]: { success: data.success, latency: data.latency, error: data.error, data: data.data },
+ }))
} catch {
- setResults(prev => ({ ...prev, [serviceId]: { success: false, error: 'Request failed' } }))
+ setResults((prev) => ({ ...prev, [serviceId]: { success: false, error: 'Request failed' } }))
}
}
async function testAll() {
setTestingAll(true)
- await Promise.all(SERVICES.map(s => testService(s.id)))
+ await Promise.all(SERVICES.map((s) => testService(s.id)))
setTestingAll(false)
}
+ function getSelectedAction(serviceId: string) {
+ const service = SERVICES.find((s) => s.id === serviceId)!
+ const actionValue = selectedActions[serviceId] || service.actions[0].value
+ return service.actions.find((a) => a.value === actionValue) || service.actions[0]
+ }
+
return (
@@ -45,7 +130,7 @@ export default function AWSServicesPanel() {
AWS Services
-
Test connectivity to AWS services
+
Test connectivity and perform actions on AWS services
-
- {SERVICES.map(service => {
+
+ {SERVICES.map((service) => {
const result = results[service.id]
+ const isExpanded = expandedService === service.id
+ const currentAction = getSelectedAction(service.id)
+
return (
-
-
-
-
{service.name}
-
{service.description}
+
+ {/* Service header */}
+
setExpandedService(isExpanded ? null : service.id)}
+ >
+
+
+ {service.name}
+
+
+
{service.name}
+
{service.description}
+
+
+
+ {result && !result.testing && (
+
+ {result.success ? (
+
+ ) : (
+
+ )}
+ {result.latency && (
+ {result.latency}ms
+ )}
+
+ )}
+ {result?.testing &&
}
+
- {result && !result.testing && (
- result.success
- ?
- :
- )}
- {result?.testing &&
}
- {result && !result.testing && (
-
- {result.success
- ? `✓ Connected${result.latency ? ` (${result.latency}ms)` : ''}`
- : `✗ ${result.error || 'Failed'}`
- }
+
+ {/* Expanded details */}
+ {isExpanded && (
+
+ {/* Action selector */}
+
+
+
+
+
+ {/* ARN input */}
+ {currentAction.needsArn && (
+
+
+ setArns((prev) => ({ ...prev, [service.id]: e.target.value }))}
+ placeholder={currentAction.arnPlaceholder || 'Enter ARN or resource name'}
+ className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 font-mono"
+ />
+
+ )}
+
+ {/* Execute button */}
+
+
+ {/* Result display */}
+ {result && !result.testing && (
+
+
+ {result.success
+ ? `✓ Success${result.latency ? ` (${result.latency}ms)` : ''}`
+ : `✗ ${result.error || 'Failed'}`}
+
+ {result.data !== undefined && result.data !== null && (
+
+
+
+
+
+ {JSON.stringify(result.data, null, 2)}
+
+
+ )}
+
+ )}
)}
-
)
})}
diff --git a/src/components/panels/SandboxShellPanel.tsx b/src/components/panels/SandboxShellPanel.tsx
new file mode 100644
index 0000000..19e3dfe
--- /dev/null
+++ b/src/components/panels/SandboxShellPanel.tsx
@@ -0,0 +1,284 @@
+'use client'
+import { useState, useRef, useEffect, useCallback, KeyboardEvent } from 'react'
+import { Terminal, Trash2, RotateCcw } from 'lucide-react'
+
+interface OutputLine {
+ type: 'command' | 'stdout' | 'stderr' | 'info'
+ text: string
+}
+
+export default function SandboxShellPanel() {
+ const [sessionId, setSessionId] = useState
(null)
+ const [cwd, setCwd] = useState('')
+ const [sandboxDir, setSandboxDir] = useState('')
+ const [input, setInput] = useState('')
+ const [output, setOutput] = useState([])
+ const [isExecuting, setIsExecuting] = useState(false)
+ const [history, setHistory] = useState([])
+ const [historyIndex, setHistoryIndex] = useState(-1)
+
+ const outputRef = useRef(null)
+ const inputRef = useRef(null)
+
+ // Auto-scroll to bottom
+ useEffect(() => {
+ if (outputRef.current) {
+ outputRef.current.scrollTop = outputRef.current.scrollHeight
+ }
+ }, [output])
+
+ // Focus input on mount
+ useEffect(() => {
+ inputRef.current?.focus()
+ }, [])
+
+ // Initialize session
+ const initSession = useCallback(async () => {
+ try {
+ const res = await fetch('/api/shell/execute', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ command: '', sessionId: null }),
+ })
+ const data = await res.json()
+ if (data.success) {
+ setSessionId(data.sessionId)
+ setCwd(data.cwd)
+ setSandboxDir(data.sandboxDir)
+ setOutput([
+ { type: 'info', text: '╔══════════════════════════════════════════════════════╗' },
+ { type: 'info', text: '║ K8s Debug Toolbox — Sandbox Shell ║' },
+ { type: 'info', text: '╚══════════════════════════════════════════════════════╝' },
+ { type: 'info', text: '' },
+ { type: 'info', text: `Session: ${data.sessionId}` },
+ { type: 'info', text: `Sandbox: ${data.sandboxDir}` },
+ { type: 'info', text: '' },
+ { type: 'info', text: 'This is an isolated workspace. You cannot navigate outside the sandbox.' },
+ { type: 'info', text: 'AWS CLI commands will use the pod\'s IAM identity (IRSA).' },
+ { type: 'info', text: 'Type "help" for available commands.' },
+ { type: 'info', text: '' },
+ ])
+ }
+ } catch (err) {
+ setOutput([{ type: 'stderr', text: 'Failed to initialize shell session' + (err instanceof Error ? ': ' + err.message : '') }])
+ }
+ }, [])
+
+ useEffect(() => {
+ initSession()
+ }, [initSession])
+
+ function getPromptDir(): string {
+ if (!cwd || !sandboxDir) return '~'
+ if (cwd === sandboxDir) return '~'
+ return '~' + cwd.slice(sandboxDir.length)
+ }
+
+ async function executeCommand(cmd: string) {
+ const trimmed = cmd.trim()
+ if (!trimmed) return
+
+ // Add to history
+ setHistory((prev) => {
+ const next = [...prev.filter((h) => h !== trimmed), trimmed]
+ return next.slice(-100)
+ })
+ setHistoryIndex(-1)
+
+ // Show the command in output
+ setOutput((prev) => [...prev, { type: 'command', text: `${getPromptDir()} $ ${trimmed}` }])
+
+ // Handle local commands
+ if (trimmed === 'help') {
+ setOutput((prev) => [
+ ...prev,
+ { type: 'info', text: 'Available commands:' },
+ { type: 'info', text: ' aws ... AWS CLI commands (uses pod identity)' },
+ { type: 'info', text: ' curl ... Make HTTP requests' },
+ { type: 'info', text: ' dig / nslookup DNS lookups' },
+ { type: 'info', text: ' ls, cat, echo File operations' },
+ { type: 'info', text: ' mkdir, touch Create files/directories' },
+ { type: 'info', text: ' env Show environment variables' },
+ { type: 'info', text: ' pwd Print working directory' },
+ { type: 'info', text: ' cd Change directory (sandbox only)' },
+ { type: 'info', text: ' clear Clear terminal output' },
+ { type: 'info', text: ' reset Reset session (new sandbox)' },
+ { type: 'info', text: '' },
+ { type: 'info', text: 'Note: Commands timeout after 30 seconds.' },
+ { type: 'info', text: 'Note: Navigation restricted to sandbox workspace.' },
+ ])
+ return
+ }
+
+ if (trimmed === 'reset') {
+ await initSession()
+ return
+ }
+
+ setIsExecuting(true)
+ try {
+ const res = await fetch('/api/shell/execute', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ command: trimmed, sessionId }),
+ })
+ const data = await res.json()
+
+ if (data.success) {
+ if (data.sessionId) setSessionId(data.sessionId)
+ if (data.cwd) setCwd(data.cwd)
+ if (data.sandboxDir) setSandboxDir(data.sandboxDir)
+
+ if (data.stdout === '__CLEAR__') {
+ setOutput([])
+ setIsExecuting(false)
+ return
+ }
+
+ const newLines: OutputLine[] = []
+ if (data.stdout) {
+ data.stdout.split('\n').forEach((line: string) => {
+ newLines.push({ type: 'stdout', text: line })
+ })
+ }
+ if (data.stderr) {
+ data.stderr.split('\n').forEach((line: string) => {
+ if (line.trim()) newLines.push({ type: 'stderr', text: line })
+ })
+ }
+
+ if (newLines.length > 0) {
+ setOutput((prev) => [...prev, ...newLines])
+ }
+ } else {
+ setOutput((prev) => [...prev, { type: 'stderr', text: data.error || 'Command failed' }])
+ }
+ } catch {
+ setOutput((prev) => [...prev, { type: 'stderr', text: 'Failed to execute command' }])
+ } finally {
+ setIsExecuting(false)
+ }
+ }
+
+ function handleKeyDown(e: KeyboardEvent) {
+ if (e.key === 'Enter' && !isExecuting) {
+ executeCommand(input)
+ setInput('')
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault()
+ if (history.length > 0) {
+ const newIndex = historyIndex === -1 ? history.length - 1 : Math.max(0, historyIndex - 1)
+ setHistoryIndex(newIndex)
+ setInput(history[newIndex])
+ }
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault()
+ if (historyIndex >= 0) {
+ const newIndex = historyIndex + 1
+ if (newIndex >= history.length) {
+ setHistoryIndex(-1)
+ setInput('')
+ } else {
+ setHistoryIndex(newIndex)
+ setInput(history[newIndex])
+ }
+ }
+ } else if (e.key === 'c' && e.ctrlKey) {
+ if (isExecuting) {
+ setIsExecuting(false)
+ setOutput((prev) => [...prev, { type: 'stderr', text: '^C' }])
+ }
+ } else if (e.key === 'l' && e.ctrlKey) {
+ e.preventDefault()
+ setOutput([])
+ }
+ }
+
+ function clearOutput() {
+ setOutput([])
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
Sandbox Shell
+
+ Isolated workspace • Session: {sessionId || '...'} • AWS CLI uses pod identity
+
+
+
+
+
+
+
+
+
+ {/* Terminal */}
+
inputRef.current?.focus()}
+ >
+ {/* Output area */}
+
+ {output.map((line, i) => (
+
+ {line.text || '\u00A0'}
+
+ ))}
+
+
+ {/* Input line */}
+
+
+ {getPromptDir()} $
+
+
setInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ disabled={isExecuting}
+ placeholder={isExecuting ? 'Executing...' : 'Type a command...'}
+ className="flex-1 bg-transparent border-none outline-none text-gray-100 font-mono text-sm placeholder-gray-600 disabled:opacity-50"
+ autoComplete="off"
+ spellCheck={false}
+ />
+ {isExecuting && (
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/panels/WebhookPanel.tsx b/src/components/panels/WebhookPanel.tsx
new file mode 100644
index 0000000..fbf1c65
--- /dev/null
+++ b/src/components/panels/WebhookPanel.tsx
@@ -0,0 +1,525 @@
+'use client'
+import { useState, useEffect, useCallback, useRef } from 'react'
+import { Webhook, Plus, Trash2, Copy, Check, RefreshCw, ArrowRight, Settings, Globe, Shield, ShieldOff } from 'lucide-react'
+import CopyButton from '@/components/ui/CopyButton'
+
+interface CapturedRequest {
+ id: string
+ endpointId: string
+ timestamp: number
+ method: string
+ url: string
+ path: string
+ headers: Record
+ query: Record
+ body: string
+ contentType: string
+ ip: string
+ size: number
+ forwardedResponse?: {
+ statusCode: number
+ headers: Record
+ body: string
+ latency: number
+ }
+}
+
+interface EndpointConfig {
+ id: string
+ createdAt: number
+ forwardUrl: string
+ skipTls: boolean
+ proxyMode: boolean
+ requestCount?: number
+}
+
+const METHOD_COLORS: Record = {
+ GET: 'bg-green-900 text-green-300',
+ POST: 'bg-blue-900 text-blue-300',
+ PUT: 'bg-yellow-900 text-yellow-300',
+ PATCH: 'bg-purple-900 text-purple-300',
+ DELETE: 'bg-red-900 text-red-300',
+ HEAD: 'bg-gray-700 text-gray-300',
+ OPTIONS: 'bg-gray-700 text-gray-300',
+}
+
+export default function WebhookPanel() {
+ const [endpoint, setEndpoint] = useState(null)
+ const [requests, setRequests] = useState([])
+ const [selectedRequest, setSelectedRequest] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const [autoRefresh, setAutoRefresh] = useState(true)
+ const [showSettings, setShowSettings] = useState(false)
+ const [forwardUrl, setForwardUrl] = useState('')
+ const [skipTls, setSkipTls] = useState(false)
+ const [proxyMode, setProxyMode] = useState(false)
+ const [urlCopied, setUrlCopied] = useState(false)
+ const [activeTab, setActiveTab] = useState<'headers' | 'body' | 'query' | 'response'>('headers')
+ const timerRef = useRef | null>(null)
+
+ const webhookUrl = endpoint
+ ? `${typeof window !== 'undefined' ? window.location.origin : ''}/api/webhook/endpoint/${endpoint.id}`
+ : ''
+
+ // Create endpoint on mount
+ useEffect(() => {
+ createEndpoint()
+ return () => {
+ if (timerRef.current) clearInterval(timerRef.current)
+ }
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
+
+ // Auto-refresh requests
+ useEffect(() => {
+ if (timerRef.current) clearInterval(timerRef.current)
+ if (autoRefresh && endpoint) {
+ timerRef.current = setInterval(() => fetchRequests(endpoint.id), 2000)
+ }
+ return () => {
+ if (timerRef.current) clearInterval(timerRef.current)
+ }
+ }, [autoRefresh, endpoint]) // eslint-disable-line react-hooks/exhaustive-deps
+
+ async function createEndpoint() {
+ setLoading(true)
+ try {
+ const res = await fetch('/api/webhook/endpoints', { method: 'POST' })
+ const data = await res.json()
+ if (data.success) {
+ setEndpoint(data.endpoint)
+ setRequests([])
+ setSelectedRequest(null)
+ setForwardUrl(data.endpoint.forwardUrl || '')
+ setSkipTls(data.endpoint.skipTls || false)
+ setProxyMode(data.endpoint.proxyMode || false)
+ }
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const fetchRequests = useCallback(async (endpointId: string) => {
+ try {
+ const res = await fetch(`/api/webhook/requests?endpointId=${endpointId}`)
+ const data = await res.json()
+ if (data.success) {
+ setRequests(data.requests)
+ }
+ } catch {
+ /* ignore polling errors */
+ }
+ }, [])
+
+ async function clearRequests() {
+ if (!endpoint) return
+ await fetch(`/api/webhook/requests?endpointId=${endpoint.id}`, { method: 'DELETE' })
+ setRequests([])
+ setSelectedRequest(null)
+ }
+
+ async function saveProxyConfig() {
+ if (!endpoint) return
+ try {
+ const res = await fetch('/api/webhook/proxy', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ endpointId: endpoint.id,
+ forwardUrl,
+ skipTls,
+ proxyMode,
+ }),
+ })
+ const data = await res.json()
+ if (data.success) {
+ setEndpoint((prev) => prev ? { ...prev, forwardUrl, skipTls, proxyMode } : prev)
+ setShowSettings(false)
+ }
+ } catch { /* ignore */ }
+ }
+
+ function copyUrl() {
+ navigator.clipboard.writeText(webhookUrl)
+ setUrlCopied(true)
+ setTimeout(() => setUrlCopied(false), 2000)
+ }
+
+ function formatTimestamp(ts: number) {
+ return new Date(ts).toLocaleTimeString('en-US', {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ })
+ }
+
+ function formatSize(bytes: number) {
+ if (bytes < 1024) return `${bytes} B`
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
+ }
+
+ function tryFormatJson(text: string): string {
+ try {
+ return JSON.stringify(JSON.parse(text), null, 2)
+ } catch {
+ return text
+ }
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
Webhook Receiver
+
+ Capture and inspect HTTP requests • Proxy and forward traffic
+
+
+
+
+
+
+
+
+
+
+ {/* Webhook URL */}
+ {endpoint && (
+
+
+
{webhookUrl}
+
+
+
+
+
+
+ )}
+
+ {/* Proxy indicators */}
+ {endpoint?.forwardUrl && (
+
+
+
+ Forwarding to: {endpoint.forwardUrl}
+
+ {endpoint.proxyMode && (
+
Proxy Mode
+ )}
+ {endpoint.skipTls ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Proxy Settings Panel */}
+ {showSettings && (
+
+
Proxy & Forward Settings
+
+ Forward incoming webhook requests to another URL. Enable proxy mode to relay the response back to the caller (man-in-the-middle).
+
+
+
+ setForwardUrl(e.target.value)}
+ placeholder="https://example.com/api/endpoint"
+ className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-violet-500 font-mono"
+ />
+
+
+
+
+
+
+
+ )}
+
+
+ {/* Main content: Request list + Detail */}
+
+ {/* Request list (left panel) */}
+
+ {requests.length === 0 ? (
+
+
+
No requests received yet
+
+ Send a request to the webhook URL above to see it here
+
+
+
Example:
+
+
+
+ curl -X POST {webhookUrl ? '...' : ''} -d {`'{"test": true}'`}
+
+
+
+
+ ) : (
+
+
+
+ {requests.length} request{requests.length !== 1 ? 's' : ''}
+
+
+
+ {requests.map((req) => (
+
{ setSelectedRequest(req); setActiveTab('headers') }}
+ className={`px-3 py-2.5 border-b border-gray-800/50 cursor-pointer transition-colors ${
+ selectedRequest?.id === req.id
+ ? 'bg-violet-900/30 border-l-2 border-l-violet-500'
+ : 'hover:bg-gray-900'
+ }`}
+ >
+
+
+ {req.method}
+
+
+ {req.path.replace(`/api/webhook/endpoint/${req.endpointId}`, '/') || '/'}
+
+
+
+ {formatTimestamp(req.timestamp)}
+ {formatSize(req.size)}
+ {req.forwardedResponse && (
+
+ → {req.forwardedResponse.statusCode}
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ {/* Request detail (right panel) */}
+
+ {selectedRequest ? (
+
+ {/* Request summary */}
+
+
+ {selectedRequest.method}
+
+ {selectedRequest.url}
+
+
+
+
+
Timestamp
+
{new Date(selectedRequest.timestamp).toISOString()}
+
+
+
Content-Type
+
{selectedRequest.contentType || 'none'}
+
+
+
Size / IP
+
{formatSize(selectedRequest.size)} • {selectedRequest.ip}
+
+
+
+ {/* Tabs */}
+
+ {(['headers', 'body', 'query', ...(selectedRequest.forwardedResponse ? ['response' as const] : [])] as const).map(
+ (tab) => (
+
+ )
+ )}
+
+
+ {/* Tab content */}
+
+ {activeTab === 'headers' && (
+
+ {Object.entries(selectedRequest.headers).map(([key, value]) => (
+
+ {key}
+ {value}
+
+ ))}
+ {Object.keys(selectedRequest.headers).length === 0 && (
+
No headers
+ )}
+
+ )}
+
+ {activeTab === 'body' && (
+
+ {selectedRequest.body ? (
+ <>
+
+
+
+
+ {tryFormatJson(selectedRequest.body)}
+
+ >
+ ) : (
+
No body
+ )}
+
+ )}
+
+ {activeTab === 'query' && (
+
+ {Object.entries(selectedRequest.query).map(([key, value]) => (
+
+ {key}
+ {value}
+
+ ))}
+ {Object.keys(selectedRequest.query).length === 0 && (
+
No query parameters
+ )}
+
+ )}
+
+ {activeTab === 'response' && selectedRequest.forwardedResponse && (
+
+
+
+ {selectedRequest.forwardedResponse.statusCode}
+
+
+ {selectedRequest.forwardedResponse.latency}ms
+
+
+
+
Response Headers
+ {Object.entries(selectedRequest.forwardedResponse.headers).map(([key, value]) => (
+
+ {key}
+ {value}
+
+ ))}
+
+
+
Response Body
+ {selectedRequest.forwardedResponse.body ? (
+ <>
+
+
+
+
+ {tryFormatJson(selectedRequest.forwardedResponse.body)}
+
+ >
+ ) : (
+
No response body
+ )}
+
+
+ )}
+
+
+ ) : (
+
+
+
Select a request to view details
+
+ )}
+
+
+
+ )
+}
diff --git a/src/lib/shellStore.ts b/src/lib/shellStore.ts
new file mode 100644
index 0000000..ad7a8a8
--- /dev/null
+++ b/src/lib/shellStore.ts
@@ -0,0 +1,48 @@
+import { mkdtempSync, existsSync, mkdirSync } from 'fs'
+import path from 'path'
+import os from 'os'
+
+export interface ShellSession {
+ id: string
+ sandboxDir: string
+ cwd: string
+ createdAt: number
+ env: Record
+}
+
+class ShellStore {
+ private sessions: Map = new Map()
+
+ createSession(): ShellSession {
+ const id = crypto.randomUUID().replace(/-/g, '').slice(0, 12)
+ const sandboxBase = path.join(os.tmpdir(), 'k8s-debug-sandboxes')
+ if (!existsSync(sandboxBase)) {
+ mkdirSync(sandboxBase, { recursive: true })
+ }
+ const sandboxDir = mkdtempSync(path.join(sandboxBase, `session-${id}-`))
+ const session: ShellSession = {
+ id,
+ sandboxDir,
+ cwd: sandboxDir,
+ createdAt: Date.now(),
+ env: {},
+ }
+ this.sessions.set(id, session)
+ return session
+ }
+
+ getSession(id: string): ShellSession | undefined {
+ return this.sessions.get(id)
+ }
+
+ updateCwd(id: string, cwd: string): void {
+ const session = this.sessions.get(id)
+ if (session) session.cwd = cwd
+ }
+
+ deleteSession(id: string): void {
+ this.sessions.delete(id)
+ }
+}
+
+export const shellStore = new ShellStore()
diff --git a/src/lib/webhookStore.ts b/src/lib/webhookStore.ts
new file mode 100644
index 0000000..d0ccb91
--- /dev/null
+++ b/src/lib/webhookStore.ts
@@ -0,0 +1,91 @@
+export interface CapturedRequest {
+ id: string
+ endpointId: string
+ timestamp: number
+ method: string
+ url: string
+ path: string
+ headers: Record
+ query: Record
+ body: string
+ contentType: string
+ ip: string
+ size: number
+ forwardedResponse?: {
+ statusCode: number
+ headers: Record
+ body: string
+ latency: number
+ }
+}
+
+export interface WebhookEndpoint {
+ id: string
+ createdAt: number
+ forwardUrl: string
+ skipTls: boolean
+ proxyMode: boolean
+ requests: CapturedRequest[]
+}
+
+class WebhookStore {
+ private endpoints: Map = new Map()
+ private maxRequestsPerEndpoint = 200
+
+ createEndpoint(): WebhookEndpoint {
+ const id = crypto.randomUUID().replace(/-/g, '').slice(0, 12)
+ const endpoint: WebhookEndpoint = {
+ id,
+ createdAt: Date.now(),
+ forwardUrl: '',
+ skipTls: false,
+ proxyMode: false,
+ requests: [],
+ }
+ this.endpoints.set(id, endpoint)
+ return endpoint
+ }
+
+ getEndpoint(id: string): WebhookEndpoint | undefined {
+ return this.endpoints.get(id)
+ }
+
+ addRequest(endpointId: string, request: CapturedRequest): void {
+ const endpoint = this.endpoints.get(endpointId)
+ if (!endpoint) return
+ endpoint.requests.unshift(request)
+ if (endpoint.requests.length > this.maxRequestsPerEndpoint) {
+ endpoint.requests = endpoint.requests.slice(0, this.maxRequestsPerEndpoint)
+ }
+ }
+
+ getRequests(endpointId: string): CapturedRequest[] {
+ return this.endpoints.get(endpointId)?.requests || []
+ }
+
+ clearRequests(endpointId: string): void {
+ const endpoint = this.endpoints.get(endpointId)
+ if (endpoint) endpoint.requests = []
+ }
+
+ updateEndpoint(
+ id: string,
+ updates: Partial>
+ ): void {
+ const endpoint = this.endpoints.get(id)
+ if (!endpoint) return
+ if (updates.forwardUrl !== undefined) endpoint.forwardUrl = updates.forwardUrl
+ if (updates.skipTls !== undefined) endpoint.skipTls = updates.skipTls
+ if (updates.proxyMode !== undefined) endpoint.proxyMode = updates.proxyMode
+ }
+
+ deleteEndpoint(id: string): void {
+ this.endpoints.delete(id)
+ }
+
+ listEndpoints(): WebhookEndpoint[] {
+ return Array.from(this.endpoints.values())
+ }
+}
+
+export const webhookStore = new WebhookStore()
From e06e2279476b4b21ddc0975ee8da5910b8d49ef3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 31 Mar 2026 12:06:10 +0000
Subject: [PATCH 9/9] Fix useEffect dependency ordering in WebhookPanel
Agent-Logs-Url: https://github.com/Jasonrve/Workbench/sessions/89d1fcb1-0e76-4c25-8371-b886f72374bf
Co-authored-by: Jasonrve <20551144+Jasonrve@users.noreply.github.com>
---
src/components/panels/WebhookPanel.tsx | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/components/panels/WebhookPanel.tsx b/src/components/panels/WebhookPanel.tsx
index fbf1c65..5a9dd05 100644
--- a/src/components/panels/WebhookPanel.tsx
+++ b/src/components/panels/WebhookPanel.tsx
@@ -61,6 +61,18 @@ export default function WebhookPanel() {
? `${typeof window !== 'undefined' ? window.location.origin : ''}/api/webhook/endpoint/${endpoint.id}`
: ''
+ const fetchRequests = useCallback(async (endpointId: string) => {
+ try {
+ const res = await fetch(`/api/webhook/requests?endpointId=${endpointId}`)
+ const data = await res.json()
+ if (data.success) {
+ setRequests(data.requests)
+ }
+ } catch {
+ /* ignore polling errors */
+ }
+ }, [])
+
// Create endpoint on mount
useEffect(() => {
createEndpoint()
@@ -78,7 +90,7 @@ export default function WebhookPanel() {
return () => {
if (timerRef.current) clearInterval(timerRef.current)
}
- }, [autoRefresh, endpoint]) // eslint-disable-line react-hooks/exhaustive-deps
+ }, [autoRefresh, endpoint, fetchRequests])
async function createEndpoint() {
setLoading(true)
@@ -98,18 +110,6 @@ export default function WebhookPanel() {
}
}
- const fetchRequests = useCallback(async (endpointId: string) => {
- try {
- const res = await fetch(`/api/webhook/requests?endpointId=${endpointId}`)
- const data = await res.json()
- if (data.success) {
- setRequests(data.requests)
- }
- } catch {
- /* ignore polling errors */
- }
- }, [])
-
async function clearRequests() {
if (!endpoint) return
await fetch(`/api/webhook/requests?endpointId=${endpoint.id}`, { method: 'DELETE' })