diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml
index 91a386a27ce..0265125c6e5 100644
--- a/.github/workflows/build-container.yml
+++ b/.github/workflows/build-container.yml
@@ -51,20 +51,21 @@ jobs:
- name: Try the cluster!
run: kubectl get pods -A
- name: Restore image-cache Folder
- id: cache-image-restore
+ id: cache-image-restore2
uses: actions/cache/restore@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
with:
path: ~/image-cache
# cache the container image. All the paths this PR depends on except the e2e-tests folder for the key.
key: ${{ runner.os }}-image-${{ hashFiles('backend/pkg/**', 'backend/cmd/**', 'backend/go.*', 'frontend/src/**', 'frontend/package.json', 'frontend/package-lock.json', 'Makefile', '.github/workflows/build-container.yml', 'Dockerfile', 'Dockerfile.plugins') }}
- name: Restore Cached Docker Images
- if: steps.cache-image-restore.outputs.cache-hit == 'true'
+ if: steps.cache-image-restore2.outputs.cache-hit == 'true'
run: |
export SHELL=/bin/bash
- docker load -i ~/image-cache/headlamp-plugins-test.tar
- docker load -i ~/image-cache/headlamp.tar
+ mkdir -p ~/image-cache
+ gzip -dc ~/image-cache/headlamp-plugins-test.tar.gz | docker load
+ gzip -dc ~/image-cache/headlamp.tar.gz | docker load
- name: Make a .plugins folder for testing later
- if: steps.cache-image-restore.outputs.cache-hit != 'true'
+ if: steps.cache-image-restore2.outputs.cache-hit != 'true'
run: |
echo "Extract pod-counter plugin into .plugins folder, which will be copied into image later by 'make image'."
cd plugins/examples/pod-counter
@@ -77,12 +78,12 @@ jobs:
cd ../../
ls -laR .plugins
- name: Remove unnecessary files
- if: steps.cache-image-restore.outputs.cache-hit != 'true'
+ if: steps.cache-image-restore2.outputs.cache-hit != 'true'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
- name: Build image
- if: steps.cache-image-restore.outputs.cache-hit != 'true'
+ if: steps.cache-image-restore2.outputs.cache-hit != 'true'
run: |
export SHELL=/bin/bash
DOCKER_IMAGE_VERSION=latest make image
@@ -95,7 +96,7 @@ jobs:
kind load docker-image ghcr.io/headlamp-k8s/headlamp-plugins-test:latest --name test
kind load docker-image ghcr.io/headlamp-k8s/headlamp:latest --name test
- name: Test .plugins folder
- if: steps.cache-image-restore.outputs.cache-hit != 'true'
+ if: steps.cache-image-restore2.outputs.cache-hit != 'true'
run: |
export SHELL=/bin/bash
echo "----------------------------"
@@ -186,20 +187,30 @@ jobs:
else
echo "Playwright tests passed successfully"
fi
+ # Clear disk space by removing unnecessary files, apt files and uninstall some playwright dependencies
+ - name: Clear Disk Space
+ if: steps.cache-image-restore2.outputs.cache-hit != 'true'
+ run: |
+ export SHELL=/bin/bash
+ sudo rm -rf /var/lib/apt/lists/*
+ sudo apt-get remove -y libgbm-dev libxcb-dri3-0 libxcb-dri2-0 libxcb-xfixes0 libxcb-shape0 libxcb-shm0 libxcb-render0 libxcb-glx0 || true
+ sudo rm -rf e2e-tests/node_modules/
+ kind delete cluster --name test
+ kind delete cluster --name test2
- name: Save Docker Images to Tar files in image-cache Folder
- if: steps.cache-image-restore.outputs.cache-hit != 'true'
+ if: steps.cache-image-restore2.outputs.cache-hit != 'true'
run: |
export SHELL=/bin/bash
mkdir -p ~/image-cache
- docker save -o ~/image-cache/headlamp-plugins-test.tar ghcr.io/headlamp-k8s/headlamp-plugins-test
- docker save -o ~/image-cache/headlamp.tar ghcr.io/headlamp-k8s/headlamp
+ docker save ghcr.io/headlamp-k8s/headlamp-plugins-test | gzip -9 > ~/image-cache/headlamp-plugins-test.tar.gz
+ docker save ghcr.io/headlamp-k8s/headlamp | gzip -9 > ~/image-cache/headlamp.tar.gz
- name: Cache image-cache Folder
- if: steps.cache-image-restore.outputs.cache-hit != 'true'
+ if: steps.cache-image-restore2.outputs.cache-hit != 'true'
id: cache-image-save
uses: actions/cache/save@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
with:
path: ~/image-cache
- key: ${{ steps.cache-image-restore.outputs.cache-primary-key }}
+ key: ${{ steps.cache-image-restore2.outputs.cache-primary-key }}
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
if: always()
with:
diff --git a/.github/workflows/pr-to-update-homebrew.yml b/.github/workflows/pr-to-update-homebrew.yml
deleted file mode 100644
index 7c0c5816f6d..00000000000
--- a/.github/workflows/pr-to-update-homebrew.yml
+++ /dev/null
@@ -1,103 +0,0 @@
-name: PR to update homebrew
-
-# This action will run after a tag starting with "v" is published
-on:
- push:
- tags:
- - 'v*'
- workflow_dispatch:
-
-env:
- LATEST_HEADLAMP_TAG: latest
-permissions:
- contents: read
-
-jobs:
- create_pr_to_upgrade_homebrew:
- name: Create PR to upgrade homebrew
- runs-on: ubuntu-latest
- permissions:
- contents: write # needed to push a branch
- pull-requests: write # needed to open a pull request
-
- steps:
- - name: Checkout headlamp repo
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- with:
- path: headlamp
- fetch-depth: 0
- - name: Configure Git
- run: |
- user=${{github.actor}}
- if [ -z $user ]; then
- user=yolossn
- fi
- git config --global user.name "$user"
- git config --global user.email "$user@users.noreply.github.com"
- - name: Get headlamp latest tag
- run: |
- cd headlamp
- latestTag=$(git tag --list --sort=version:refname 'v*' | tail -1)
- echo "LATEST_HEADLAMP_TAG=$latestTag" >> $GITHUB_ENV
- echo $latestTag
- - name: Sync homebrew-cask fork from upstream
- run: |
- gh repo sync headlamp-k8s/homebrew-cask
- env:
- GITHUB_TOKEN: ${{ secrets.KINVOLK_REPOS_TOKEN }}
- - name: Check out homebrew-cask repo
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- with:
- repository: headlamp-k8s/homebrew-cask
- path: homebrew-cask
- token: ${{ secrets.KINVOLK_REPOS_TOKEN }}
- fetch-depth: 0
- - name: Update headlamp version in homebrew-cask
- run: |
- user=${{github.actor}}
- HEADLAMP_VERSION=${LATEST_HEADLAMP_TAG:1}
- BRANCH_NAME="hl-ci-update_headlamp_$HEADLAMP_VERSION"
- if [ -z $user ]; then
- user=yolossn
- fi
- cd homebrew-cask
- if git branch -l | grep -q "$BRANCH_NAME"; then
- echo "deleting old branch from local to avoid conflict"
- git branch -D "$BRANCH_NAME"
- fi
- if git branch -a | grep -q "origin/$BRANCH_NAME"; then
- echo "deleting old branch from remote to avoid conflict"
- git push origin --delete "$BRANCH_NAME"
- fi
- wget "https://github.com/kubernetes-sigs/headlamp/releases/download/$LATEST_HEADLAMP_TAG/checksums.txt"
- ARM_SHA=$(cat checksums.txt | grep .arm64.dmg | awk -F" " '{print $1}')
- INTEL_SHA=$(cat checksums.txt | grep .x64.dmg | awk -F" " '{print $1}')
- git checkout -b "$BRANCH_NAME"
- sed -i "s/version\ .*/version \"$HEADLAMP_VERSION\"/g" ./Casks/h/headlamp.rb
- if [ $ARM_SHA ]; then
- echo "replacing ARM SHA"
- sed -i "s/sha256 arm:\ \".*/sha256 arm:\ \"$ARM_SHA\",/g" ./Casks/h/headlamp.rb
- fi
- if [ $INTEL_SHA ]; then
- echo "replacing Intel SHA"
- sed -i "s/ intel:\ \".*/ intel:\ \"$INTEL_SHA\"/g" ./Casks/h/headlamp.rb
- fi
- git diff
- rm ./checksums.txt
- git add ./Casks/h/headlamp.rb
- git status
- git commit --signoff -m "Update Headlamp version to $HEADLAMP_VERSION"
- git status
- git log -1
- git push origin "$BRANCH_NAME" -f
- gh pr create \
- --title "Upgrade Headlamp version to $HEADLAMP_VERSION" \
- --repo "Homebrew/homebrew-cask" \
- --head "headlamp-k8s:$BRANCH_NAME" \
- --base "master" \
- --assignee "$user" \
- --body "Upgrade Headlamp version to $HEADLAMP_VERSION
- cc: @$user" \
- env:
- LATEST_HEADLAMP_TAG: ${{ env.LATEST_HEADLAMP_TAG }}
- GITHUB_TOKEN: ${{ secrets.KINVOLK_REPOS_TOKEN }}
diff --git a/.github/workflows/pr-to-update-winget.yml b/.github/workflows/pr-to-update-winget.yml
deleted file mode 100644
index 3eac5891e49..00000000000
--- a/.github/workflows/pr-to-update-winget.yml
+++ /dev/null
@@ -1,112 +0,0 @@
-name: PR for updating winget
-
-# This action will run after a tag starting with "v" is published
-on:
- push:
- tags:
- - "v*"
- workflow_dispatch:
-
-permissions:
- contents: read
-
-jobs:
- winget-update:
- permissions:
- contents: write # for Git to git push
- pull-requests: write # for creating PRs
- runs-on: ubuntu-latest
- steps:
- - name: Checkout Headlamp
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- with:
- token: ${{ secrets.KINVOLK_REPOS_TOKEN }}
- # we need the full history for the git tag command, so fetch all the branches
- fetch-depth: 0
-
- - name: Configure Git
- run: |
- user=${{github.actor}}
- if [ -z $user ]; then
- user=vyncent-t
- fi
- git config --global user.name "$user"
- git config --global user.email "$user@users.noreply.github.com"
-
- # Set up Node.js environment, pay attention to the version
- # Some features might not be available in older versions
- - name: Create node.js environment
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
- with:
- node-version: "21"
-
- # Install the dependencies for the winget script
- - name: Install winget dependencies
- run: |
- cd $GITHUB_WORKSPACE/app
- npm ci
-
- # We set the latest tag as an environment variable before we use it in the next steps
- # note that we have to echo the variable to the environment file to make it available in the next steps
- - name: Set latest tag
- run: |
- echo "Setting latest tag"
- latestTag=$(git tag --list --sort=version:refname 'v*' | tail -1)
- # Remove the 'v' from the tag
- latestTag=${latestTag#v}
- echo "LATEST_HEADLAMP_TAG=$latestTag" >> $GITHUB_ENV
- echo $latestTag
-
- # checkout the winget-pkgs repository
- - name: Checkout winget-pkgs
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- with:
- repository: headlamp-k8s/winget-pkgs
- path: winget-pkgs
- token: ${{ secrets.KINVOLK_REPOS_TOKEN }}
- # we need the full history for the git tag command, so fetch all the branches
- fetch-depth: 0
-
- # Run the winget script
- - name: Create winget package
- run: |
- echo "Running winget script"
- echo "Repository: ${{ github.repository }}"
- echo "Workspace: ${GITHUB_WORKSPACE}"
- echo $GITHUB_WORKSPACE
- pwd
- echo "creating winget pkgs for ${LATEST_HEADLAMP_TAG}"
- cd $GITHUB_WORKSPACE/app/windows/winget
- node winget-create.js $LATEST_HEADLAMP_TAG $GITHUB_WORKSPACE/winget-pkgs/manifests/h/Headlamp/Headlamp
- echo "Script finished"
-
- - name: Create PR branch
- run: |
- user=${{github.actor}}
- if [ -z $user ]; then
- user=vyncent-t
- fi
- echo "Creating PR branch"
- echo "Repository: ${{ github.repository }}"
- echo "Workspace: ${GITHUB_WORKSPACE}"
- pwd
- echo "moving to winget-pkgs directory"
- cd $GITHUB_WORKSPACE/winget-pkgs
- pwd
- ls
- echo "moving to Headlamp directory"
- cd $GITHUB_WORKSPACE/winget-pkgs/manifests/h/Headlamp/Headlamp
- pwd
- ls
- git checkout -b "hl-ci-winget-update-$LATEST_HEADLAMP_TAG"
- git add .
- git commit -s -m "Update winget package $LATEST_HEADLAMP_TAG"
- git push origin "hl-ci-winget-update-$LATEST_HEADLAMP_TAG"
- env:
- GITHUB_TOKEN: ${{ secrets.KINVOLK_REPOS_TOKEN }}
-
- - name: Create Pull Request
- run: |
- echo "Create pull request"
- echo "continue with the following link"
- echo "https://github.com/headlamp-k8s/winget-pkgs/pull/new/hl-ci-winget-update-$LATEST_HEADLAMP_TAG"
diff --git a/.github/workflows/push-chocolatey-pkg.yml b/.github/workflows/push-chocolatey-pkg.yml
new file mode 100644
index 00000000000..306c06b96ce
--- /dev/null
+++ b/.github/workflows/push-chocolatey-pkg.yml
@@ -0,0 +1,50 @@
+name: Push Chocolatey Package
+
+# This action will run when a PR that updates the Chocolatey package is merged to main
+# It can also be triggered manually
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'app/windows/chocolatey/headlamp.nuspec'
+ - 'app/windows/chocolatey/tools/chocolateyinstall.ps1'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ push-chocolatey:
+ runs-on: windows-latest
+ steps:
+ - name: Checkout Headlamp
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ with:
+ ref: main
+
+ - name: Install Chocolatey
+ run: |
+ Set-ExecutionPolicy Bypass -Scope Process -Force
+ [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
+ iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
+ shell: powershell
+
+ - name: Pack Chocolatey Package
+ run: |
+ cd app/windows/chocolatey
+ choco pack
+ shell: powershell
+
+ - name: Push to Chocolatey
+ run: |
+ cd app/windows/chocolatey
+ $nupkg = Get-ChildItem -Filter *.nupkg | Select-Object -First 1
+ if ($nupkg) {
+ Write-Host "Pushing package: $($nupkg.Name)"
+ choco push $nupkg.FullName --source https://push.chocolatey.org/ --key ${{ secrets.CHOCOLATEY_API_KEY }}
+ } else {
+ Write-Error "No .nupkg file found"
+ exit 1
+ }
+ shell: powershell
diff --git a/app/app-build-manifest.json b/app/app-build-manifest.json
index bbb87ce0e42..5d80194b27e 100644
--- a/app/app-build-manifest.json
+++ b/app/app-build-manifest.json
@@ -3,11 +3,11 @@
"plugins": [
{
"name": "app-catalog",
- "archive": "https://github.com/headlamp-k8s/plugins/releases/download/app-catalog-0.6.3/app-catalog-0.6.3.tar.gz"
+ "archive": "https://github.com/headlamp-k8s/plugins/releases/download/app-catalog-0.7.0/app-catalog-0.7.0.tar.gz"
},
{
"name": "plugin-catalog",
- "archive": "https://github.com/headlamp-k8s/plugins/releases/download/plugin-catalog-0.4.1/headlamp-k8s-plugin-catalog-0.4.1.tar.gz"
+ "archive": "https://github.com/headlamp-k8s/plugins/releases/download/plugin-catalog-0.4.2/headlamp-k8s-plugin-catalog-0.4.2.tar.gz"
},
{
"name": "prometheus",
diff --git a/app/e2e-tests/README.md b/app/e2e-tests/README.md
index 1fc1da0132c..7cab527a3eb 100644
--- a/app/e2e-tests/README.md
+++ b/app/e2e-tests/README.md
@@ -41,17 +41,17 @@ To run the tests for the web mode, you will need to have the backend running. Fo
`cd headlamp`
- run the following command
- `make backend` followed by `make run-backend`
+ `npm run backend:build` followed by `npm run backend:start`
### Frontend
To run the tests for the web mode, you will need to have the frontend running. Follow the steps below to run the frontend:
- cd into the headlamp directory in a separate terminal
- `cd headlamp/frontend`
+ `cd headlamp`
- run the following command
- `make frontend` followed by `make run-frontend`
+ `npm run frontend:build` followed by `npm run frontend:start`
### Running the tests
diff --git a/app/e2e-tests/package-lock.json b/app/e2e-tests/package-lock.json
index 8ee2f7b8a9c..2733c2ad3bf 100644
--- a/app/e2e-tests/package-lock.json
+++ b/app/e2e-tests/package-lock.json
@@ -14,13 +14,13 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.48.1",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz",
- "integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==",
+ "version": "1.56.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
+ "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright": "1.48.1"
+ "playwright": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -55,13 +55,13 @@
}
},
"node_modules/playwright": {
- "version": "1.48.1",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz",
- "integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==",
+ "version": "1.56.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
+ "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.48.1"
+ "playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -74,9 +74,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.48.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz",
- "integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==",
+ "version": "1.56.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
+ "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
diff --git a/app/e2e-tests/tests/clusterRename.spec.ts b/app/e2e-tests/tests/clusterRename.spec.ts
new file mode 100644
index 00000000000..f4f442d9206
--- /dev/null
+++ b/app/e2e-tests/tests/clusterRename.spec.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2025 The Kubernetes Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import path from 'path';
+import { _electron, Page } from 'playwright';
+import { HeadlampPage } from './headlampPage';
+
+// Electron setup
+const electronExecutable = process.platform === 'win32' ? 'electron.cmd' : 'electron';
+const electronPath = path.resolve(__dirname, `../../node_modules/.bin/${electronExecutable}`);
+const appPath = path.resolve(__dirname, '../../');
+let electronApp;
+let electronPage: Page;
+
+// Test configuration
+const TEST_CONFIG = {
+ originalName: 'minikube',
+ newName: 'test-cluster',
+ cancelledName: 'cancelled-cluster',
+ invalidName: 'Invalid Cluster!',
+};
+
+// Helper functions
+async function navigateToSettings(page: Page) {
+ await page.waitForLoadState('load');
+ await page.getByRole('button', { name: 'Settings' }).click();
+ await page.waitForLoadState('load');
+}
+
+async function verifyClusterName(page: Page, expectedName: string) {
+ await page.getByRole('button', { name: 'Settings' }).click();
+ await page.locator('a[href="#/settings/cluster"]').click();
+ // Check the cluster name in the cluster selector combobox
+ await expect(page.locator(`input[placeholder="${expectedName}"]`)).toBeVisible();
+}
+
+async function renameCluster(
+ page: Page,
+ fromName: string,
+ toName: string,
+ confirm: boolean = true
+) {
+ await page.fill(`input[placeholder="${fromName}"]`, toName);
+ await page.getByRole('button', { name: 'Apply' }).click();
+ await page.getByRole('button', { name: confirm ? 'Yes' : 'No' }).click();
+ await page.waitForLoadState('load');
+ await page.locator(`a[href="#/c/${toName}/"]`).click();
+}
+
+// Setup
+test.beforeAll(async () => {
+ electronApp = await _electron.launch({
+ cwd: appPath,
+ executablePath: electronPath,
+ args: ['.'],
+ env: {
+ ...process.env,
+ NODE_ENV: 'development',
+ ELECTRON_DEV: 'true',
+ },
+ });
+
+ electronPage = await electronApp.firstWindow();
+});
+
+test.beforeEach(async ({ page }) => {
+ page.close();
+});
+
+// Tests
+test.describe('Cluster rename functionality', () => {
+ test.beforeEach(() => {
+ test.skip(process.env.PLAYWRIGHT_TEST_MODE !== 'app', 'These tests only run in app mode');
+ });
+
+ test('should rename cluster and verify changes', async ({ page: browserPage }) => {
+ const page = process.env.PLAYWRIGHT_TEST_MODE === 'app' ? electronPage : browserPage;
+ const headlampPage = new HeadlampPage(page);
+ await headlampPage.authenticate();
+
+ await navigateToSettings(page);
+ await expect(page.locator('h2')).toContainText('Cluster Settings');
+
+ // Test invalid inputs
+ await page.fill('input[placeholder="minikube"]', TEST_CONFIG.invalidName);
+ await expect(page.getByRole('button', { name: 'Apply' })).toBeDisabled();
+
+ await page.fill('input[placeholder="minikube"]', '');
+ await expect(page.getByRole('button', { name: 'Apply' })).toBeDisabled();
+
+ // Test successful rename
+ await renameCluster(page, TEST_CONFIG.originalName, TEST_CONFIG.newName);
+ await verifyClusterName(page, TEST_CONFIG.newName);
+ });
+});
diff --git a/app/electron/main.ts b/app/electron/main.ts
index 68beaba0e01..97675f555fa 100644
--- a/app/electron/main.ts
+++ b/app/electron/main.ts
@@ -40,6 +40,7 @@ import url from 'url';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import i18n from './i18next.config';
+import ElectronMCPClient from './mcp-client';
import {
addToPath,
ArtifactHubHeadlampPkg,
@@ -131,6 +132,7 @@ const shouldCheckForUpdates = process.env.HEADLAMP_CHECK_FOR_UPDATES !== 'false'
// make it global so that it doesn't get garbage collected
let mainWindow: BrowserWindow | null;
+let mcpClient: ElectronMCPClient;
/**
* `Action` is an interface for an action to be performed by the plugin manager.
@@ -957,8 +959,7 @@ function getDefaultAppMenu(): AppMenu[] {
},
{
label: i18n.t('About'),
- id: 'original-about',
- url: 'https://github.com/kubernetes-sigs/headlamp',
+ id: 'original-about-help',
},
],
},
@@ -1052,7 +1053,12 @@ function menusToTemplate(mainWindow: BrowserWindow | null, menusFromPlugins: App
return;
}
- if (!!url) {
+ // Handle the "About" menu item from the Help menu specially
+ if (appMenu.id === 'original-about-help') {
+ menu.click = () => {
+ mainWindow?.webContents.send('open-about-dialog');
+ };
+ } else if (!!url) {
menu.click = async () => {
// Open external links in the external browser.
if (!!mainWindow && !url.startsWith('http')) {
@@ -1132,6 +1138,13 @@ function adjustZoom(delta: number) {
function startElecron() {
console.info('App starting...');
+ mcpClient = new ElectronMCPClient();
+
+ // Initialize MCP client
+ mcpClient.initialize().catch(error => {
+ console.error('Failed to initialize MCP client on startup:', error);
+ });
+
let appVersion: string;
if (isDev && process.env.HEADLAMP_APP_VERSION) {
appVersion = process.env.HEADLAMP_APP_VERSION;
@@ -1247,6 +1260,9 @@ function startElecron() {
},
});
+ // Set the main window reference in the MCP client for dialogs
+ mcpClient.setMainWindow(mainWindow);
+
// Load the frontend
mainWindow.loadURL(startUrl);
@@ -1411,6 +1427,15 @@ function startElecron() {
mainWindow?.webContents.send('backend-token', backendToken);
});
+ // Handle cluster change notifications from frontend
+ ipcMain.on('cluster-changed', (event: IpcMainEvent, cluster: string | null) => {
+ if (mcpClient) {
+ mcpClient.handleClusterChange(cluster).catch(error => {
+ console.error('Failed to handle cluster change in MCP client:', error);
+ });
+ }
+ });
+
setupRunCmdHandlers(mainWindow, ipcMain);
new PluginManagerEventListeners().setupEventHandlers();
@@ -1463,6 +1488,11 @@ function startElecron() {
if (mainWindow) {
mainWindow.removeAllListeners('close');
}
+
+ // Cleanup MCP client
+ if (mcpClient) {
+ mcpClient.cleanup().catch(console.error);
+ }
});
}
diff --git a/app/electron/mcp-client.ts b/app/electron/mcp-client.ts
new file mode 100644
index 00000000000..1b16c3374ba
--- /dev/null
+++ b/app/electron/mcp-client.ts
@@ -0,0 +1,1110 @@
+/*
+ * Copyright 2025 The Kubernetes Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { MultiServerMCPClient } from '@langchain/mcp-adapters';
+import { app, BrowserWindow, dialog, ipcMain } from 'electron';
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import { MCPConfigManager } from './mcp-config';
+
+/**
+ * Enable debug logging for MCP client operations.
+ * Controlled by the HEADLAMP_MCP_DEBUG environment variable.
+ * Set HEADLAMP_MCP_DEBUG=true to enable debug logging, or HEADLAMP_MCP_DEBUG=false to disable it.
+ * Defaults to false if not set.
+ */
+const DEBUG = process.env.HEADLAMP_MCP_DEBUG === 'true';
+
+/**
+ * Helper function for debug logging
+ * @param args - Arguments to log
+ */
+function debugLog(...args: any[]): void {
+ if (DEBUG) {
+ console.log('[MCP]', ...args);
+ }
+}
+
+/**
+ * Configuration for an MCP (Model Context Protocol) server
+ */
+interface MCPServer {
+ /** Unique name identifier for the MCP server */
+ name: string;
+ /** Command to execute to start the MCP server */
+ command: string;
+ /** Arguments to pass to the server command */
+ args: string[];
+ /** Whether this server is enabled */
+ enabled: boolean;
+ /** Optional environment variables to set for the server process */
+ env?: Record;
+}
+
+/**
+ * Main MCP configuration containing server definitions
+ */
+interface MCPConfig {
+ /** Whether MCP integration is enabled globally */
+ enabled: boolean;
+ /** List of configured MCP servers */
+ servers: MCPServer[];
+}
+
+/**
+ * Manages MCP (Model Context Protocol) client for Electron desktop application.
+ *
+ * This class handles:
+ * - MCP server lifecycle (initialization, restart, cleanup)
+ * - Tool discovery and execution with user confirmation
+ * - Configuration management and validation
+ * - IPC communication with renderer process
+ * - Cluster context changes
+ */
+class ElectronMCPClient {
+ /** The LangChain MCP client instance managing multiple servers */
+ private client: MultiServerMCPClient | null = null;
+ /** Cached list of available tools from all MCP servers */
+ private tools: any[] = [];
+ /** Whether the MCP client has been successfully initialized */
+ private isInitialized = false;
+ /** Promise tracking ongoing initialization to prevent duplicate initializations */
+ private initializationPromise: Promise | null = null;
+ /** Manages tool-level configuration and statistics */
+ private configManager: MCPConfigManager;
+ /** Reference to the main Electron window for displaying dialogs */
+ private mainWindow: BrowserWindow | null = null;
+ /** Currently active Kubernetes cluster context */
+ private currentCluster: string | null = null;
+
+ /**
+ * Creates a new ElectronMCPClient instance
+ * @param mainWindow - Optional reference to the main browser window for dialogs
+ */
+ constructor(mainWindow: BrowserWindow | null = null) {
+ this.mainWindow = mainWindow;
+ this.configManager = new MCPConfigManager();
+ this.setupIpcHandlers();
+ }
+
+ /**
+ * Set the main window reference for dialogs
+ */
+ setMainWindow(mainWindow: BrowserWindow | null): void {
+ this.mainWindow = mainWindow;
+ }
+
+ /**
+ * Initialize tools configuration for all available tools
+ * This completely replaces the existing config with current tools
+ */
+ private initializeToolsConfiguration(): void {
+ if (!this.tools || this.tools.length === 0) {
+ debugLog('No tools available for configuration initialization');
+ // Clear the config if no tools are available
+ this.configManager.replaceConfig({});
+ return;
+ }
+
+ // Group tools by server name with their schemas
+ const toolsByServer: Record<
+ string,
+ Array<{
+ name: string;
+ inputSchema?: any;
+ description?: string;
+ }>
+ > = {};
+
+ for (const tool of this.tools) {
+ // Extract server name from tool name (format: "serverName__toolName")
+ const toolName = tool.name;
+ const parts = toolName.split('__');
+
+ // Extract schema from the tool (LangChain tools use .schema property)
+ const toolSchema = (tool as any).schema || tool.inputSchema || null;
+ debugLog(
+ `Processing tool: ${toolName}, has inputSchema: ${!!toolSchema}, description: "${
+ tool.description
+ }"`
+ );
+
+ if (parts.length >= 2) {
+ const serverName = parts[0];
+ const actualToolName = parts.slice(1).join('__');
+
+ if (!toolsByServer[serverName]) {
+ toolsByServer[serverName] = [];
+ }
+ toolsByServer[serverName].push({
+ name: actualToolName,
+ inputSchema: toolSchema,
+ description: tool.description || '',
+ });
+ } else {
+ // Fallback for tools without server prefix
+ if (!toolsByServer['default']) {
+ toolsByServer['default'] = [];
+ }
+ toolsByServer['default'].push({
+ name: toolName,
+ inputSchema: toolSchema,
+ description: tool.description || '',
+ });
+ }
+ }
+
+ debugLog('Tools grouped by server:', Object.keys(toolsByServer));
+
+ // Replace the entire configuration with current tools
+ this.configManager.replaceToolsConfig(toolsByServer);
+ }
+
+ /**
+ * Show user confirmation dialog for MCP operations.
+ * Displays a dialog to the user for security confirmation before executing MCP operations.
+ *
+ * @param title - Dialog title
+ * @param message - Main message to display to the user
+ * @param operation - Description of the operation being performed
+ * @returns Promise resolving to true if user allows the operation, false otherwise
+ */
+ private async showConfirmationDialog(
+ title: string,
+ message: string,
+ operation: string
+ ): Promise {
+ if (!this.mainWindow) {
+ console.warn('No main window available for confirmation dialog, allowing operation');
+ return true;
+ }
+
+ const result = await dialog.showMessageBox(this.mainWindow, {
+ type: 'question',
+ buttons: ['Allow', 'Cancel'],
+ defaultId: 1,
+ title,
+ message,
+ detail: `Operation: ${operation}\n\nDo you want to allow this MCP operation?`,
+ });
+
+ return result.response === 0; // 0 is "Allow"
+ }
+
+ /**
+ * Show detailed confirmation dialog for tools configuration changes.
+ * Compares current and new configurations and displays a summary of changes.
+ *
+ * @param newConfig - The new configuration to be applied
+ * @returns Promise resolving to true if user approves changes, false otherwise
+ */
+ private async showToolsConfigConfirmationDialog(newConfig: any): Promise {
+ if (!this.mainWindow) {
+ console.warn('No main window available for confirmation dialog, allowing operation');
+ return true;
+ }
+
+ const currentConfig = this.configManager.getConfig();
+ const summary = this.createToolsConfigSummary(currentConfig, newConfig);
+
+ if (summary.totalChanges === 0) {
+ return true; // No changes, allow operation
+ }
+
+ const result = await dialog.showMessageBox(this.mainWindow, {
+ type: 'question',
+ buttons: ['Apply Changes', 'Cancel'],
+ defaultId: 1,
+ title: 'MCP Tools Configuration Changes',
+ message: `${summary.totalChanges} tool configuration change(s) will be applied:`,
+ detail: summary.summaryText + '\n\nDo you want to apply these changes?',
+ });
+
+ return result.response === 0; // 0 is "Apply Changes"
+ }
+
+ /**
+ * Create a concise summary of tools configuration changes.
+ * Analyzes differences between current and new tool configurations.
+ *
+ * @param currentConfig - Current tool configuration
+ * @param newConfig - New tool configuration to compare against
+ * @returns Object containing total changes count and formatted summary text
+ */
+ private createToolsConfigSummary(
+ currentConfig: any,
+ newConfig: any
+ ): {
+ totalChanges: number;
+ summaryText: string;
+ } {
+ const enabledTools: string[] = [];
+ const disabledTools: string[] = [];
+ const addedTools: string[] = [];
+ const removedTools: string[] = [];
+
+ // Get all server names from both configs
+ const allServers = new Set([
+ ...Object.keys(currentConfig || {}),
+ ...Object.keys(newConfig || {}),
+ ]);
+
+ for (const serverName of allServers) {
+ const currentServerConfig = currentConfig[serverName] || {};
+ const newServerConfig = newConfig[serverName] || {};
+
+ // Get all tool names from both configs
+ const allTools = new Set([
+ ...Object.keys(currentServerConfig),
+ ...Object.keys(newServerConfig),
+ ]);
+
+ for (const toolName of allTools) {
+ const currentTool = currentServerConfig[toolName];
+ const newTool = newServerConfig[toolName];
+ const displayName = `${toolName} (${serverName})`;
+
+ if (!currentTool && newTool) {
+ // New tool added
+ addedTools.push(displayName);
+ if (newTool.enabled) {
+ enabledTools.push(displayName);
+ } else {
+ disabledTools.push(displayName);
+ }
+ } else if (currentTool && !newTool) {
+ // Tool removed
+ removedTools.push(displayName);
+ } else if (currentTool && newTool) {
+ // Tool modified
+ if (currentTool.enabled !== newTool.enabled) {
+ if (newTool.enabled) {
+ enabledTools.push(displayName);
+ } else {
+ disabledTools.push(displayName);
+ }
+ }
+ }
+ }
+ }
+
+ // Build summary text
+ const summaryParts: string[] = [];
+
+ if (enabledTools.length > 0) {
+ summaryParts.push(`✓ ENABLE (${enabledTools.length}): ${enabledTools.join(', ')}`);
+ }
+
+ if (disabledTools.length > 0) {
+ summaryParts.push(`✗ DISABLE (${disabledTools.length}): ${disabledTools.join(', ')}`);
+ }
+
+ const totalChanges =
+ enabledTools.length + disabledTools.length + addedTools.length + removedTools.length;
+
+ return {
+ totalChanges,
+ summaryText: summaryParts.join('\n\n'),
+ };
+ }
+
+ /**
+ * Show detailed configuration change confirmation dialog.
+ * Displays specific changes between current and new MCP server configurations.
+ *
+ * @param currentConfig - Current MCP configuration, or null if none exists
+ * @param newConfig - New MCP configuration to be applied
+ * @returns Promise resolving to true if user approves changes, false otherwise
+ */
+ private async showConfigChangeDialog(
+ currentConfig: MCPConfig | null,
+ newConfig: MCPConfig
+ ): Promise {
+ debugLog('Current MCP Config:', currentConfig);
+ debugLog('New MCP Config:', newConfig);
+ if (!this.mainWindow) {
+ console.warn('No main window available for confirmation dialog, allowing operation');
+ return true;
+ }
+
+ const changes = this.analyzeConfigChanges(currentConfig, newConfig);
+
+ const result = await dialog.showMessageBox(this.mainWindow, {
+ type: 'question',
+ buttons: ['Apply Changes', 'Cancel'],
+ defaultId: 1,
+ title: 'MCP Configuration Changes',
+ message: 'The application wants to update the MCP configuration.',
+ detail:
+ changes.length > 0
+ ? `The following changes will be applied:\n\n${changes.join(
+ '\n'
+ )}\n\nDo you want to apply these changes?`
+ : 'No changes detected in the configuration.\n\nDo you want to proceed anyway?',
+ });
+
+ return result.response === 0; // 0 is "Apply Changes"
+ }
+
+ /**
+ * Analyze differences between current and new configuration.
+ * Creates a human-readable list of changes to MCP server configurations.
+ *
+ * @param currentConfig - Current MCP configuration, or null if none exists
+ * @param newConfig - New MCP configuration to compare against
+ * @returns Array of human-readable change descriptions
+ */
+ private analyzeConfigChanges(currentConfig: MCPConfig | null, newConfig: MCPConfig): string[] {
+ const changes: string[] = [];
+
+ // Check if MCP is being enabled/disabled
+ const currentEnabled = currentConfig?.enabled ?? false;
+ const newEnabled = newConfig.enabled ?? false;
+
+ if (currentEnabled !== newEnabled) {
+ changes.push(`• MCP will be ${newEnabled ? 'ENABLED' : 'DISABLED'}`);
+ }
+
+ // Get current and new server lists
+ const currentServers = currentConfig?.servers ?? [];
+ const newServers = newConfig.servers ?? [];
+
+ // Check for added servers
+ const currentServerNames = new Set(currentServers.map(s => s.name));
+ const newServerNames = new Set(newServers.map(s => s.name));
+
+ for (const server of newServers) {
+ if (!currentServerNames.has(server.name)) {
+ changes.push(`• ADD server: "${server.name}" (${server.command})`);
+ }
+ }
+
+ // Check for removed servers
+ for (const server of currentServers) {
+ if (!newServerNames.has(server.name)) {
+ changes.push(`• REMOVE server: "${server.name}"`);
+ }
+ }
+
+ // Check for modified servers
+ for (const newServer of newServers) {
+ const currentServer = currentServers.find(s => s.name === newServer.name);
+ if (currentServer) {
+ const serverChanges: string[] = [];
+
+ // Check enabled status
+ if (currentServer.enabled !== newServer.enabled) {
+ serverChanges.push(`${newServer.enabled ? 'enable' : 'disable'}`);
+ }
+
+ // Check command
+ if (currentServer.command !== newServer.command) {
+ serverChanges.push(`change command: "${currentServer.command}" → "${newServer.command}"`);
+ }
+
+ // Check arguments
+ const currentArgs = JSON.stringify(currentServer.args || []);
+ const newArgs = JSON.stringify(newServer.args || []);
+ if (currentArgs !== newArgs) {
+ serverChanges.push(`change arguments: ${currentArgs} → ${newArgs}`);
+ }
+
+ // Check environment variables
+ const currentEnv = JSON.stringify(currentServer.env || {});
+ const newEnv = JSON.stringify(newServer.env || {});
+ if (currentEnv !== newEnv) {
+ serverChanges.push(`change environment variables`);
+ }
+
+ if (serverChanges.length > 0) {
+ changes.push(`• MODIFY server "${newServer.name}": ${serverChanges.join(', ')}`);
+ }
+ }
+ }
+
+ return changes;
+ }
+
+ /**
+ * Parse tool name to extract server and tool components.
+ * Tool names follow format "serverName__toolName" where serverName is the MCP server prefix.
+ *
+ * @param fullToolName - Complete tool name potentially including server prefix
+ * @returns Object with serverName and toolName components
+ */
+ private parseToolName(fullToolName: string): { serverName: string; toolName: string } {
+ const parts = fullToolName.split('__');
+ if (parts.length >= 2) {
+ return {
+ serverName: parts[0],
+ toolName: parts.slice(1).join('__'),
+ };
+ }
+ return {
+ serverName: 'default',
+ toolName: fullToolName,
+ };
+ }
+
+ /**
+ * Validate tool parameters against schema from configuration.
+ * Performs basic JSON schema validation on tool parameters before execution.
+ *
+ * @param serverName - Name of the MCP server providing the tool
+ * @param toolName - Name of the tool being validated
+ * @param args - Tool arguments to validate
+ * @returns Object with validation result and optional error message
+ */
+ private validateToolParameters(
+ serverName: string,
+ toolName: string,
+ args: any
+ ): { valid: boolean; error?: string } {
+ const toolState = this.configManager.getToolStats(serverName, toolName);
+ if (!toolState || !toolState.inputSchema) {
+ // No schema available, assume valid
+ return { valid: true };
+ }
+
+ try {
+ const schema = toolState.inputSchema;
+
+ // Basic validation - check required properties
+ if (schema.required && Array.isArray(schema.required)) {
+ for (const requiredProp of schema.required) {
+ if (args[requiredProp] === undefined || args[requiredProp] === null) {
+ return {
+ valid: false,
+ error: `Required parameter '${requiredProp}' is missing`,
+ };
+ }
+ }
+ }
+
+ // Check property types if schema properties are defined
+ if (schema.properties) {
+ for (const [propName, propSchema] of Object.entries(schema.properties as any)) {
+ if (args[propName] !== undefined) {
+ const propType = (propSchema as any).type;
+ const actualType = typeof args[propName];
+
+ if (propType === 'string' && actualType !== 'string') {
+ return {
+ valid: false,
+ error: `Parameter '${propName}' should be a string, got ${actualType}`,
+ };
+ }
+ if (propType === 'number' && actualType !== 'number') {
+ return {
+ valid: false,
+ error: `Parameter '${propName}' should be a number, got ${actualType}`,
+ };
+ }
+ if (propType === 'boolean' && actualType !== 'boolean') {
+ return {
+ valid: false,
+ error: `Parameter '${propName}' should be a boolean, got ${actualType}`,
+ };
+ }
+ if (propType === 'array' && !Array.isArray(args[propName])) {
+ return {
+ valid: false,
+ error: `Parameter '${propName}' should be an array, got ${actualType}`,
+ };
+ }
+ if (
+ propType === 'object' &&
+ (actualType !== 'object' || Array.isArray(args[propName]) || args[propName] === null)
+ ) {
+ return {
+ valid: false,
+ error: `Parameter '${propName}' should be an object, got ${actualType}`,
+ };
+ }
+ }
+ }
+ }
+
+ return { valid: true };
+ } catch (error) {
+ return {
+ valid: false,
+ error: `Schema validation error: ${
+ error instanceof Error ? error.message : 'Unknown error'
+ }`,
+ };
+ }
+ }
+
+ /**
+ * Load MCP server configuration from settings
+ */
+ private loadMCPConfig(): MCPConfig | null {
+ try {
+ const settingsPath = path.join(app.getPath('userData'), 'settings.json');
+ if (!fs.existsSync(settingsPath)) {
+ return null;
+ }
+
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
+ return settings.mcpConfig || null;
+ } catch (error) {
+ console.error('Error loading MCP config:', error);
+ return null;
+ }
+ }
+
+ /**
+ * Expand environment variables and resolve paths in arguments.
+ * Handles:
+ * - Windows environment variables (%USERPROFILE%, %APPDATA%, etc.)
+ * - HEADLAMP_CURRENT_CLUSTER placeholder
+ * - Docker volume mount path conversions for Windows
+ *
+ * @param args - Array of argument strings to expand
+ * @param cluster - Optional cluster context to substitute
+ * @returns Array of expanded argument strings
+ */
+ private expandArgs(args: string[], cluster: string | null = null): string[] {
+ const currentCluster = cluster || this.currentCluster || '';
+
+ return args.map(arg => {
+ // Replace Windows environment variables like %USERPROFILE%
+ let expandedArg = arg;
+
+ // Handle HEADLAMP_CURRENT_CLUSTER placeholder
+ if (expandedArg.includes('HEADLAMP_CURRENT_CLUSTER')) {
+ expandedArg = expandedArg.replace(/HEADLAMP_CURRENT_CLUSTER/g, currentCluster);
+ }
+
+ // Handle %USERPROFILE%
+ if (expandedArg.includes('%USERPROFILE%')) {
+ expandedArg = expandedArg.replace(/%USERPROFILE%/g, os.homedir());
+ }
+
+ // Handle other common Windows environment variables
+ if (expandedArg.includes('%APPDATA%')) {
+ expandedArg = expandedArg.replace(/%APPDATA%/g, process.env.APPDATA || '');
+ }
+
+ if (expandedArg.includes('%LOCALAPPDATA%')) {
+ expandedArg = expandedArg.replace(/%LOCALAPPDATA%/g, process.env.LOCALAPPDATA || '');
+ }
+
+ // Convert Windows backslashes to forward slashes for Docker
+ if (process.platform === 'win32' && expandedArg.includes('\\')) {
+ expandedArg = expandedArg.replace(/\\/g, '/');
+ }
+
+ // Handle Docker volume mount format and ensure proper Windows path format
+ if (expandedArg.includes('type=bind,src=')) {
+ // Parse Docker mount options more carefully to handle paths with commas
+ // Format: type=bind,src=,dst=[,other-options]
+ const typeBindMatch = expandedArg.match(/^type=bind,src=(.+),dst=(.+?)(?:,|$)/);
+ if (typeBindMatch) {
+ // For paths with commas, we need to find the last occurrence of ,dst=
+ const srcStartIdx = expandedArg.indexOf('src=') + 4;
+ const dstStartIdx = expandedArg.lastIndexOf(',dst=');
+
+ if (dstStartIdx > srcStartIdx) {
+ let srcPath = expandedArg.substring(srcStartIdx, dstStartIdx);
+ const remainingPart = expandedArg.substring(dstStartIdx + 5); // Skip ",dst="
+ const dstEndIdx = remainingPart.indexOf(',');
+ const dstPath = dstEndIdx > 0 ? remainingPart.substring(0, dstEndIdx) : remainingPart;
+
+ // Resolve the source path
+ if (process.platform === 'win32') {
+ srcPath = path.resolve(srcPath);
+ // For Docker on Windows, we might need to convert C:\ to /c/ format
+ if (srcPath.match(/^[A-Za-z]:/)) {
+ srcPath =
+ '/' + srcPath.charAt(0).toLowerCase() + srcPath.slice(2).replace(/\\/g, '/');
+ }
+ }
+
+ const otherOptions = dstEndIdx > 0 ? ',' + remainingPart.substring(dstEndIdx + 1) : '';
+ expandedArg = `type=bind,src=${srcPath},dst=${dstPath}${otherOptions}`;
+ }
+ }
+ }
+
+ return expandedArg;
+ });
+ }
+
+ private async initializeClient(): Promise {
+ debugLog('initializeClient called');
+ if (this.isInitialized) {
+ return;
+ }
+
+ if (this.initializationPromise) {
+ return this.initializationPromise;
+ }
+
+ debugLog('Starting MCP client initialization...');
+ this.initializationPromise = this.doInitialize();
+ return this.initializationPromise;
+ }
+
+ private async doInitialize(): Promise {
+ try {
+ debugLog('Initializing MCP client in Electron main process...');
+
+ // Load MCP configuration from settings
+ const mcpConfig = this.loadMCPConfig();
+
+ if (
+ !mcpConfig ||
+ !mcpConfig.enabled ||
+ !mcpConfig.servers ||
+ mcpConfig.servers.length === 0
+ ) {
+ debugLog('MCP is disabled or no servers configured');
+ this.isInitialized = true;
+ return;
+ }
+
+ // Build MCP servers configuration from settings
+ const mcpServers: any = {};
+
+ for (const server of mcpConfig.servers) {
+ if (!server.enabled || !server.name || !server.command) {
+ continue;
+ }
+
+ // Expand environment variables and resolve paths in arguments
+ const expandedArgs = this.expandArgs(server.args || [], this.currentCluster);
+ console.log(`Expanded args for ${server.name}:`, expandedArgs);
+
+ // Prepare environment variables
+ const serverEnv = { ...process.env, ...(server.env || {}) };
+
+ mcpServers[server.name] = {
+ transport: 'stdio',
+ command: server.command,
+ args: expandedArgs,
+ env: serverEnv,
+ restart: {
+ enabled: true,
+ maxAttempts: 3,
+ delayMs: 2000,
+ },
+ };
+ }
+
+ // If no enabled servers, skip initialization
+ if (Object.keys(mcpServers).length === 0) {
+ console.log('No enabled MCP servers found');
+ this.isInitialized = true;
+ return;
+ }
+
+ console.log('Initializing MCP client with servers:', Object.keys(mcpServers));
+
+ this.client = new MultiServerMCPClient({
+ throwOnLoadError: false, // Don't throw on load error to allow partial initialization
+ prefixToolNameWithServerName: true, // Prefix to avoid name conflicts
+ additionalToolNamePrefix: '',
+ useStandardContentBlocks: true,
+ mcpServers,
+ defaultToolTimeout: 2 * 60 * 1000, // 2 minutes
+ });
+
+ // Get and cache the tools
+ this.tools = await this.client.getTools();
+ // Initialize configuration for available tools
+ this.initializeToolsConfiguration();
+
+ this.isInitialized = true;
+ console.log('MCP client initialized successfully with', this.tools.length, 'tools');
+ } catch (error) {
+ console.error('Failed to initialize MCP client:', error);
+ this.client = null;
+ this.isInitialized = false;
+ this.initializationPromise = null;
+ throw error;
+ }
+ }
+
+ private setupIpcHandlers(): void {
+ // Handle MCP tool execution
+ ipcMain.handle('mcp-execute-tool', async (event, { toolName, args, toolCallId }) => {
+ console.log('args in mcp-execute-tool:', args);
+ try {
+ await this.initializeClient();
+
+ if (!this.client || this.tools.length === 0) {
+ throw new Error('MCP client not initialized or no tools available');
+ }
+
+ // Parse tool name
+ const { serverName, toolName: actualToolName } = this.parseToolName(toolName);
+
+ // Check if tool is enabled
+ const isEnabled = this.configManager.isToolEnabled(serverName, actualToolName);
+
+ if (!isEnabled) {
+ throw new Error(`Tool ${actualToolName} from server ${serverName} is disabled`);
+ }
+
+ // Find the tool by name
+ const tool = this.tools.find(t => t.name === toolName);
+ if (!tool) {
+ throw new Error(`Tool ${toolName} not found`);
+ }
+
+ // Validate parameters against schema from configuration
+ const validation = this.validateToolParameters(serverName, actualToolName, args);
+ if (!validation.valid) {
+ throw new Error(`Parameter validation failed: ${validation.error}`);
+ }
+
+ console.log(`Executing MCP tool: ${toolName} with args:`, args);
+
+ // Execute the tool directly using LangChain's invoke method
+ const result = await tool.invoke(args);
+ console.log(`MCP tool ${toolName} executed successfully`);
+
+ // Record tool usage
+ this.configManager.recordToolUsage(serverName, actualToolName);
+
+ return {
+ success: true,
+ result,
+ toolCallId,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ toolCallId,
+ };
+ }
+ });
+
+ // Handle MCP client status check
+ ipcMain.handle('mcp-get-status', async () => {
+ return {
+ isInitialized: this.isInitialized,
+ hasClient: this.client !== null,
+ };
+ });
+
+ // Handle MCP client reset/restart with user confirmation
+ ipcMain.handle('mcp-reset-client', async () => {
+ try {
+ // Show confirmation dialog
+ const userConfirmed = await this.showConfirmationDialog(
+ 'MCP Client Reset',
+ 'The application wants to reset the MCP client. This will restart all MCP server connections.',
+ 'Reset MCP client'
+ );
+
+ if (!userConfirmed) {
+ return {
+ success: false,
+ error: 'User cancelled the operation',
+ };
+ }
+
+ console.log('Resetting MCP client...');
+
+ if (this.client) {
+ // If the client has a close/dispose method, call it
+ if (typeof (this.client as any).close === 'function') {
+ await (this.client as any).close();
+ }
+ }
+
+ this.client = null;
+ this.isInitialized = false;
+ this.initializationPromise = null;
+
+ // Re-initialize
+ await this.initializeClient();
+
+ return { success: true };
+ } catch (error) {
+ console.error('Error resetting MCP client:', error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ };
+ }
+ });
+
+ // Handle MCP configuration updates with detailed user confirmation
+ ipcMain.handle('mcp-update-config', async (event, mcpConfig: MCPConfig) => {
+ try {
+ // Get current configuration for comparison
+ const currentConfig = this.loadMCPConfig();
+ console.log('Requested MCP configuration update:', mcpConfig);
+ // Show detailed confirmation dialog with changes
+ const userConfirmed = await this.showConfigChangeDialog(currentConfig, mcpConfig);
+
+ if (!userConfirmed) {
+ return {
+ success: false,
+ error: 'User cancelled the configuration update',
+ };
+ }
+
+ console.log('Updating MCP configuration with user confirmation...');
+
+ // Save to settings file
+ const settingsPath = path.join(app.getPath('userData'), 'settings.json');
+ let settings: any = {};
+
+ if (fs.existsSync(settingsPath)) {
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
+ }
+
+ settings.mcpConfig = mcpConfig;
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
+
+ // Reset and reinitialize client with new config
+ if (this.client) {
+ if (typeof (this.client as any).close === 'function') {
+ await (this.client as any).close();
+ }
+ }
+ this.client = null;
+ this.isInitialized = false;
+ this.initializationPromise = null;
+
+ // Re-initialize with new config
+ await this.initializeClient();
+
+ console.log('MCP configuration updated successfully');
+ return { success: true };
+ } catch (error) {
+ console.error('Error updating MCP configuration:', error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ };
+ }
+ });
+
+ // Handle getting current MCP configuration
+ ipcMain.handle('mcp-get-config', async () => {
+ try {
+ const mcpConfig = this.loadMCPConfig();
+ return {
+ success: true,
+ config: mcpConfig || { enabled: false, servers: [] },
+ };
+ } catch (error) {
+ console.error('Error getting MCP configuration:', error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ config: { enabled: false, servers: [] },
+ };
+ }
+ });
+
+ // Handle getting MCP tools configuration
+ ipcMain.handle('mcp-get-tools-config', async () => {
+ try {
+ const toolsConfig = this.configManager.getConfig();
+ return {
+ success: true,
+ config: toolsConfig,
+ };
+ } catch (error) {
+ console.error('Error getting MCP tools configuration:', error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ config: {},
+ };
+ }
+ });
+
+ // Handle updating MCP tools configuration with user confirmation
+ ipcMain.handle('mcp-update-tools-config', async (event, toolsConfig: any) => {
+ console.log('Requested MCP tools configuration update:', toolsConfig);
+ try {
+ // Show confirmation dialog with detailed changes
+ const userConfirmed = await this.showToolsConfigConfirmationDialog(toolsConfig);
+
+ if (!userConfirmed) {
+ return {
+ success: false,
+ error: 'User cancelled the operation',
+ };
+ }
+
+ this.configManager.setConfig(toolsConfig);
+ return { success: true };
+ } catch (error) {
+ console.error('Error updating MCP tools configuration:', error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ };
+ }
+ });
+
+ // Handle enabling/disabling specific tools
+ ipcMain.handle('mcp-set-tool-enabled', async (event, { serverName, toolName, enabled }) => {
+ try {
+ this.configManager.setToolEnabled(serverName, toolName, enabled);
+ return { success: true };
+ } catch (error) {
+ console.error('Error setting tool enabled state:', error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ };
+ }
+ });
+
+ // Handle getting tool statistics
+ ipcMain.handle('mcp-get-tool-stats', async (event, { serverName, toolName }) => {
+ try {
+ const stats = this.configManager.getToolStats(serverName, toolName);
+ return {
+ success: true,
+ stats,
+ };
+ } catch (error) {
+ console.error('Error getting tool statistics:', error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ stats: null,
+ };
+ }
+ });
+
+ // Handle cluster context changes
+ ipcMain.handle('mcp-cluster-change', async (event, { cluster }) => {
+ try {
+ console.log('Received cluster change event:', cluster);
+ await this.handleClusterChange(cluster);
+ return {
+ success: true,
+ };
+ } catch (error) {
+ console.error('Error handling cluster change:', error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ };
+ }
+ });
+ }
+
+ /**
+ * Check if any server in the config uses HEADLAMP_CURRENT_CLUSTER placeholder.
+ * This determines whether the MCP client needs to be restarted on cluster changes.
+ *
+ * @param mcpConfig - MCP configuration to check
+ * @returns True if any enabled server has HEADLAMP_CURRENT_CLUSTER in its arguments
+ */
+ private hasClusterDependentServers(mcpConfig: MCPConfig | null): boolean {
+ if (!mcpConfig || !mcpConfig.servers) {
+ return false;
+ }
+
+ return mcpConfig.servers.some(
+ server =>
+ server.enabled &&
+ server.args &&
+ server.args.some(arg => arg.includes('HEADLAMP_CURRENT_CLUSTER'))
+ );
+ }
+
+ /**
+ * Handle cluster context change
+ * This will restart MCP servers if any server uses HEADLAMP_CURRENT_CLUSTER
+ */
+ async handleClusterChange(newCluster: string | null): Promise {
+ // If cluster hasn't actually changed, do nothing
+ if (this.currentCluster === newCluster) {
+ return;
+ }
+
+ const oldCluster = this.currentCluster;
+ this.currentCluster = newCluster;
+
+ // Check if we have any cluster-dependent servers
+ const mcpConfig = this.loadMCPConfig();
+ if (!this.hasClusterDependentServers(mcpConfig)) {
+ console.log('No cluster-dependent MCP servers found, skipping restart');
+ return;
+ }
+
+ try {
+ // Reset the client
+ if (this.client) {
+ if (typeof (this.client as any).close === 'function') {
+ await (this.client as any).close();
+ }
+ }
+
+ this.client = null;
+ this.isInitialized = false;
+ this.initializationPromise = null;
+
+ // Re-initialize with new cluster context
+ await this.initializeClient();
+
+ console.log('MCP client restarted successfully for new cluster:', newCluster);
+ } catch (error) {
+ console.error('Error restarting MCP client for cluster change:', error);
+ // Restore previous cluster on error
+ this.currentCluster = oldCluster;
+ throw error;
+ }
+ }
+
+ /**
+ * Public method to initialize the MCP client
+ * This should be called when the app starts
+ */
+ async initialize(): Promise {
+ try {
+ await this.initializeClient();
+ } catch (error) {
+ console.error('Failed to initialize MCP client on startup:', error);
+ // Don't throw error to prevent app startup failure
+ }
+ }
+
+ /**
+ * Cleanup method to be called when the app is shutting down
+ */
+ async cleanup(): Promise {
+ if (this.client) {
+ try {
+ await this.client.close();
+ } catch (error) {
+ console.error('Error cleaning up MCP client:', error);
+ }
+ }
+ this.client = null;
+ this.tools = [];
+ this.isInitialized = false;
+ this.initializationPromise = null;
+ }
+}
+
+export default ElectronMCPClient;
diff --git a/app/electron/mcp-config.ts b/app/electron/mcp-config.ts
new file mode 100644
index 00000000000..209b8180ba3
--- /dev/null
+++ b/app/electron/mcp-config.ts
@@ -0,0 +1,303 @@
+/*
+ * Copyright 2025 The Kubernetes Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { app } from 'electron';
+import * as fs from 'fs';
+import * as path from 'path';
+
+export interface MCPToolState {
+ enabled: boolean;
+ lastUsed?: Date;
+ usageCount?: number;
+ inputSchema?: any; // JSON schema for tool parameters
+ description?: string; // Tool description from MCP server
+}
+
+export interface MCPServerToolState {
+ [toolName: string]: MCPToolState;
+}
+
+export interface MCPToolsConfig {
+ [serverName: string]: MCPServerToolState;
+}
+
+export class MCPConfigManager {
+ private configPath: string;
+ private config: MCPToolsConfig = {};
+
+ constructor() {
+ this.configPath = path.join(app.getPath('userData'), 'headlamp-mcp-config.json');
+ this.loadConfig();
+ }
+
+ /**
+ * Load MCP tools configuration from file
+ */
+ private loadConfig(): void {
+ try {
+ if (fs.existsSync(this.configPath)) {
+ const configData = fs.readFileSync(this.configPath, 'utf-8');
+ this.config = JSON.parse(configData);
+ } else {
+ this.config = {};
+ }
+ } catch (error) {
+ console.error('Error loading MCP tools configuration:', error);
+ this.config = {};
+ }
+ }
+
+ /**
+ * Save MCP tools configuration to file
+ */
+ private saveConfig(): void {
+ try {
+ fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
+ } catch (error) {
+ console.error('Error saving MCP tools configuration:', error);
+ }
+ }
+
+ /**
+ * Get the enabled state of a specific tool
+ */
+ isToolEnabled(serverName: string, toolName: string): boolean {
+ const serverConfig = this.config[serverName];
+ if (!serverConfig) {
+ // Default to enabled for new tools
+ return true;
+ }
+
+ const toolState = serverConfig[toolName];
+ if (!toolState) {
+ // Default to enabled for new tools
+ return true;
+ }
+
+ return toolState.enabled;
+ }
+
+ /**
+ * Set the enabled state of a specific tool
+ */
+ setToolEnabled(serverName: string, toolName: string, enabled: boolean): void {
+ if (!this.config[serverName]) {
+ this.config[serverName] = {};
+ }
+
+ if (!this.config[serverName][toolName]) {
+ this.config[serverName][toolName] = {
+ enabled: true,
+ usageCount: 0,
+ };
+ }
+
+ this.config[serverName][toolName].enabled = enabled;
+ this.saveConfig();
+ }
+
+ /**
+ * Get all disabled tools for a server
+ */
+ getDisabledTools(serverName: string): string[] {
+ const serverConfig = this.config[serverName];
+ if (!serverConfig) {
+ return [];
+ }
+
+ return Object.entries(serverConfig)
+ .filter(([, toolState]) => !toolState.enabled)
+ .map(([toolName]) => toolName);
+ }
+
+ /**
+ * Get all enabled tools for a server
+ */
+ getEnabledTools(serverName: string): string[] {
+ const serverConfig = this.config[serverName];
+ if (!serverConfig) {
+ return [];
+ }
+
+ return Object.entries(serverConfig)
+ .filter(([, toolState]) => toolState.enabled)
+ .map(([toolName]) => toolName);
+ }
+
+ /**
+ * Update tool usage statistics
+ */
+ recordToolUsage(serverName: string, toolName: string): void {
+ if (!this.config[serverName]) {
+ this.config[serverName] = {};
+ }
+
+ if (!this.config[serverName][toolName]) {
+ this.config[serverName][toolName] = {
+ enabled: true,
+ usageCount: 0,
+ };
+ }
+
+ const toolState = this.config[serverName][toolName];
+ toolState.lastUsed = new Date();
+ toolState.usageCount = (toolState.usageCount || 0) + 1;
+ this.saveConfig();
+ }
+
+ /**
+ * Get the complete configuration
+ */
+ getConfig(): MCPToolsConfig {
+ return { ...this.config };
+ }
+
+ /**
+ * Set the complete configuration
+ */
+ setConfig(newConfig: MCPToolsConfig): void {
+ this.config = { ...newConfig };
+ this.saveConfig();
+ }
+
+ /**
+ * Reset configuration to empty state
+ */
+ resetConfig(): void {
+ this.config = {};
+ this.saveConfig();
+ }
+
+ /**
+ * Initialize default configuration for available tools with schemas
+ */
+ initializeToolsConfig(
+ serverName: string,
+ toolsInfo: Array<{
+ name: string;
+ inputSchema?: any;
+ description?: string;
+ }>
+ ): void {
+ if (!this.config[serverName]) {
+ this.config[serverName] = {};
+ }
+
+ const serverConfig = this.config[serverName];
+ let hasChanges = false;
+
+ for (const toolInfo of toolsInfo) {
+ const toolName = toolInfo.name;
+
+ if (!serverConfig[toolName]) {
+ serverConfig[toolName] = {
+ enabled: true,
+ usageCount: 0,
+ inputSchema: toolInfo.inputSchema || null,
+ description: toolInfo.description || '',
+ };
+ hasChanges = true;
+ } else {
+ // Always update schema and description for existing tools
+ let toolChanged = false;
+
+ // Update schema if it's different or missing
+ const currentSchema = JSON.stringify(serverConfig[toolName].inputSchema || null);
+ const newSchema = JSON.stringify(toolInfo.inputSchema || null);
+ if (currentSchema !== newSchema) {
+ serverConfig[toolName].inputSchema = toolInfo.inputSchema || null;
+ toolChanged = true;
+ }
+
+ // Update description if it's different or missing
+ const currentDescription = serverConfig[toolName].description || '';
+ const newDescription = toolInfo.description || '';
+ if (currentDescription !== newDescription) {
+ serverConfig[toolName].description = newDescription;
+ toolChanged = true;
+ }
+
+ if (toolChanged) {
+ hasChanges = true;
+ }
+ }
+ }
+
+ if (hasChanges) {
+ this.saveConfig();
+ }
+ }
+
+ /**
+ * Get tool statistics
+ */
+ getToolStats(serverName: string, toolName: string): MCPToolState | null {
+ const serverConfig = this.config[serverName];
+ if (!serverConfig || !serverConfig[toolName]) {
+ return null;
+ }
+
+ return { ...serverConfig[toolName] };
+ }
+
+ /**
+ * Replace the entire tools configuration with a new set of tools
+ * This overwrites all existing tools with only the current ones
+ */
+ replaceToolsConfig(
+ toolsByServer: Record<
+ string,
+ Array<{
+ name: string;
+ inputSchema?: any;
+ description?: string;
+ }>
+ >
+ ): void {
+ // Create a new config object
+ const newConfig: MCPToolsConfig = {};
+
+ for (const [serverName, toolsInfo] of Object.entries(toolsByServer)) {
+ newConfig[serverName] = {};
+
+ for (const toolInfo of toolsInfo) {
+ const toolName = toolInfo.name;
+
+ // Check if this tool existed in the old config to preserve enabled state and usage count
+ const oldToolState = this.config[serverName]?.[toolName];
+
+ newConfig[serverName][toolName] = {
+ enabled: oldToolState?.enabled ?? true, // Preserve enabled state or default to true
+ usageCount: oldToolState?.usageCount ?? 0, // Preserve usage count or default to 0
+ inputSchema: toolInfo.inputSchema || null,
+ description: toolInfo.description || '',
+ };
+ }
+ }
+
+ // Replace the entire config
+ this.config = newConfig;
+ this.saveConfig();
+ }
+
+ /**
+ * Replace the entire configuration with a new config object
+ */
+ replaceConfig(newConfig: MCPToolsConfig): void {
+ this.config = newConfig;
+ this.saveConfig();
+ }
+}
diff --git a/app/electron/preload.ts b/app/electron/preload.ts
index e65c2c13404..c6e7b9545fa 100644
--- a/app/electron/preload.ts
+++ b/app/electron/preload.ts
@@ -30,6 +30,7 @@ contextBridge.exposeInMainWorld('desktopApi', {
'plugin-manager',
'request-backend-token',
'request-plugin-permission-secrets',
+ 'cluster-changed',
];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
@@ -47,6 +48,7 @@ contextBridge.exposeInMainWorld('desktopApi', {
'plugin-manager',
'backend-token',
'plugin-permission-secrets',
+ 'open-about-dialog',
];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
@@ -57,4 +59,27 @@ contextBridge.exposeInMainWorld('desktopApi', {
removeListener: (channel: string, func: (...args: unknown[]) => void) => {
ipcRenderer.removeListener(channel, func);
},
+
+ // MCP client APIs
+ mcp: {
+ executeTool: (toolName: string, args: Record, toolCallId?: string) =>
+ ipcRenderer.invoke('mcp-execute-tool', { toolName, args, toolCallId }),
+ getStatus: () => ipcRenderer.invoke('mcp-get-status'),
+ resetClient: () => ipcRenderer.invoke('mcp-reset-client'),
+ getConfig: () => ipcRenderer.invoke('mcp-get-config'),
+ updateConfig: (config: any) => ipcRenderer.invoke('mcp-update-config', config),
+ getToolsConfig: () => ipcRenderer.invoke('mcp-get-tools-config'),
+ updateToolsConfig: (config: any) => ipcRenderer.invoke('mcp-update-tools-config', config),
+ setToolEnabled: (serverName: string, toolName: string, enabled: boolean) =>
+ ipcRenderer.invoke('mcp-set-tool-enabled', { serverName, toolName, enabled }),
+ getToolStats: (serverName: string, toolName: string) =>
+ ipcRenderer.invoke('mcp-get-tool-stats', { serverName, toolName }),
+ clusterChange: (cluster: string | null) =>
+ ipcRenderer.invoke('mcp-cluster-change', { cluster }),
+ },
+
+ // Notify cluster change (for MCP server restart)
+ notifyClusterChange: (cluster: string | null) => {
+ ipcRenderer.send('cluster-changed', cluster);
+ },
});
diff --git a/app/package-lock.json b/app/package-lock.json
index 7f7d034d619..ff1e2eee32e 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -1,13 +1,14 @@
{
"name": "headlamp",
- "version": "0.36.0",
+ "version": "0.37.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "headlamp",
- "version": "0.36.0",
+ "version": "0.37.0",
"dependencies": {
+ "@langchain/mcp-adapters": "^0.6.0",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"dotenv": "^16.4.5",
@@ -1958,6 +1959,13 @@
"hasInstallScript": true,
"optional": true
},
+ "node_modules/@cfworker/json-schema": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
+ "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@@ -3495,6 +3503,76 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@langchain/core": {
+ "version": "0.3.73",
+ "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.73.tgz",
+ "integrity": "sha512-E5dK9/MDH9671yuU3ZoMfSkMC7njtZrOZYrmLGk+2cvGk92yqxv/+MMxwOYoFtPMEWx9T8mTg2omHqcXXaEpGw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@cfworker/json-schema": "^4.0.2",
+ "ansi-styles": "^5.0.0",
+ "camelcase": "6",
+ "decamelize": "1.2.0",
+ "js-tiktoken": "^1.0.12",
+ "langsmith": "^0.3.46",
+ "mustache": "^4.2.0",
+ "p-queue": "^6.6.2",
+ "p-retry": "4",
+ "uuid": "^10.0.0",
+ "zod": "^3.25.32",
+ "zod-to-json-schema": "^3.22.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@langchain/core/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@langchain/core/node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@langchain/mcp-adapters": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@langchain/mcp-adapters/-/mcp-adapters-0.6.0.tgz",
+ "integrity": "sha512-NHQNH9NciLhxlCnL/4HDebiYT3UQvpBfF5KPlIi/uSXn8te/bYjPV64gUyAloNNo+fjj4qDvKP1/nHj0r7fKFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.12.1",
+ "debug": "^4.4.0",
+ "zod": "^3.24.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "extended-eventsource": "^1.x"
+ },
+ "peerDependencies": {
+ "@langchain/core": "^0.3.66"
+ }
+ },
"node_modules/@malept/cross-spawn-promise": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
@@ -3567,6 +3645,29 @@
"node": ">= 10.0.0"
}
},
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.17.5",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz",
+ "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.6",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.0.1",
+ "express-rate-limit": "^7.5.0",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.23.8",
+ "zod-to-json-schema": "^3.24.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@mswjs/interceptors": {
"version": "0.38.7",
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz",
@@ -3857,6 +3958,13 @@
"@types/node": "*"
}
},
+ "node_modules/@types/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/semver": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz",
@@ -3876,6 +3984,13 @@
"integrity": "sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==",
"dev": true
},
+ "node_modules/@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/verror": {
"version": "1.10.9",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz",
@@ -4138,6 +4253,40 @@
"integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
"dev": true
},
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@@ -4175,7 +4324,6 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -5031,7 +5179,6 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -5099,6 +5246,26 @@
"bluebird": "^3.5.5"
}
},
+ "node_modules/body-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.0",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.6.3",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.0",
+ "raw-body": "^3.0.0",
+ "type-is": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -5382,6 +5549,15 @@
"node": ">= 10.0.0"
}
},
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@@ -5406,7 +5582,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -5416,6 +5591,22 @@
"node": ">= 0.4"
}
},
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -5784,12 +5975,81 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/console-table-printer": {
+ "version": "2.14.6",
+ "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.14.6.tgz",
+ "integrity": "sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "simple-wcswidth": "^1.0.1"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-disposition/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
"node_modules/copyfiles": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz",
@@ -5901,6 +6161,19 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/crc": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
@@ -6124,11 +6397,12 @@
}
},
"node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -6139,6 +6413,16 @@
}
}
},
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -6287,6 +6571,15 @@
"node": ">=0.4.0"
}
},
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -6510,7 +6803,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -6526,6 +6818,12 @@
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@@ -6812,6 +7110,15 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/encoding-sniffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
@@ -6939,7 +7246,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6949,7 +7255,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -7012,7 +7317,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -7121,6 +7425,12 @@
"node": ">=6"
}
},
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
"node_modules/escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
@@ -7697,6 +8007,43 @@
"node": ">=0.10.0"
}
},
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/eventsource": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
+ "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -7757,6 +8104,91 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/express": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.0",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
+ "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": ">= 4.11"
+ }
+ },
+ "node_modules/express/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express/node_modules/mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/extended-eventsource": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/extended-eventsource/-/extended-eventsource-1.7.0.tgz",
+ "integrity": "sha512-s8rtvZuYcKBpzytHb5g95cHbZ1J99WeMnV18oKc5wKoxkHzlzpPc/bNAm7Da2Db0BDw0CAu1z3LpH+7UsyzIpw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@@ -7790,8 +8222,7 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-fifo": {
"version": "1.3.2",
@@ -7817,8 +8248,7 @@
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
@@ -7905,6 +8335,23 @@
"node": ">=8"
}
},
+ "node_modules/finalhandler": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/find-process": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz",
@@ -8012,6 +8459,24 @@
"node": ">= 6"
}
},
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -8182,7 +8647,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -8216,7 +8680,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -8420,7 +8883,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -8506,7 +8968,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -8625,6 +9086,31 @@
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-errors/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -8803,7 +9289,6 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "dev": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
@@ -8931,6 +9416,15 @@
"node": ">= 0.10"
}
},
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -9265,6 +9759,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
"node_modules/is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@@ -10224,6 +10724,16 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
+ "node_modules/js-tiktoken": {
+ "version": "1.0.21",
+ "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
+ "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "base64-js": "^1.5.1"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -10270,8 +10780,7 @@
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@@ -10347,6 +10856,42 @@
"node": ">=6"
}
},
+ "node_modules/langsmith": {
+ "version": "0.3.67",
+ "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.67.tgz",
+ "integrity": "sha512-l4y3RmJ9yWF5a29fLg3eWZQxn6Q6dxTOgLGgQHzPGZHF3NUynn+A+airYIe/Yt4rwjGbuVrABAPsXBkVu/Hi7g==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/uuid": "^10.0.0",
+ "chalk": "^4.1.2",
+ "console-table-printer": "^2.12.1",
+ "p-queue": "^6.6.2",
+ "p-retry": "4",
+ "semver": "^7.6.3",
+ "uuid": "^10.0.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "*",
+ "@opentelemetry/exporter-trace-otlp-proto": "*",
+ "@opentelemetry/sdk-trace-base": "*",
+ "openai": "*"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@opentelemetry/exporter-trace-otlp-proto": {
+ "optional": true
+ },
+ "@opentelemetry/sdk-trace-base": {
+ "optional": true
+ },
+ "openai": {
+ "optional": true
+ }
+ }
+ },
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -10604,12 +11149,32 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -10828,9 +11393,20 @@
}
},
"node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mustache": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
+ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "mustache": "bin/mustache"
+ }
},
"node_modules/natural-compare": {
"version": "1.4.0",
@@ -10838,6 +11414,15 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -10955,17 +11540,15 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
- "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
- "dev": true,
- "peer": true,
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -11085,6 +11668,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -11180,6 +11775,60 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/p-queue": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
+ "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "eventemitter3": "^4.0.4",
+ "p-timeout": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-retry": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
+ "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/retry": "0.12.0",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-retry/node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/p-timeout": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
+ "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "p-finally": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
@@ -11264,6 +11913,15 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -11323,6 +11981,16 @@
"node": "14 || >=16.14"
}
},
+ "node_modules/path-to-regexp": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -11364,6 +12032,15 @@
"node": ">= 6"
}
},
+ "node_modules/pkce-challenge": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
+ "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
"node_modules/pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@@ -11534,6 +12211,19 @@
"node": ">= 8"
}
},
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -11547,7 +12237,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
- "dev": true,
"engines": {
"node": ">=6"
}
@@ -11568,6 +12257,21 @@
}
]
},
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -11649,6 +12353,46 @@
"rimraf": "bin.js"
}
},
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
+ "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.7.0",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
+ "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/rcedit": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-4.0.1.tgz",
@@ -12047,6 +12791,22 @@
"node": ">=8.0"
}
},
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/rsvp": {
"version": "4.8.5",
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
@@ -12130,8 +12890,7 @@
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sanitize-filename": {
"version": "1.6.3",
@@ -12149,9 +12908,10 @@
"dev": true
},
"node_modules/semver": {
- "version": "7.6.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
- "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
@@ -12166,6 +12926,49 @@
"dev": true,
"optional": true
},
+ "node_modules/send": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "mime-types": "^3.0.1",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/send/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/send/node_modules/mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/serialize-error": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
@@ -12182,6 +12985,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -12216,6 +13034,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -12376,16 +13200,69 @@
}
},
"node_modules/side-channel": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
- "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
- "dev": true,
- "peer": true,
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.7",
"es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.4",
- "object-inspect": "^1.13.1"
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
@@ -12411,6 +13288,13 @@
"node": ">=10"
}
},
+ "node_modules/simple-wcswidth": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz",
+ "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -12513,6 +13397,15 @@
"node": ">= 6"
}
},
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/stop-iteration-iterator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
@@ -13013,6 +13906,15 @@
"node": ">=10.13.0"
}
},
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@@ -13104,6 +14006,41 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/type-is/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/type-is/node_modules/mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/typed-array-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
@@ -13289,6 +14226,15 @@
"node": ">= 4.0.0"
}
},
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/untildify": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
@@ -13331,7 +14277,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
"dependencies": {
"punycode": "^2.1.0"
}
@@ -13347,6 +14292,20 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
+ "node_modules/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/v8-to-istanbul": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
@@ -13370,6 +14329,15 @@
"node": ">= 10.13.0"
}
},
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/verror": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
@@ -13874,6 +14842,24 @@
"engines": {
"node": ">= 6"
}
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.24.6",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
+ "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.24.1"
+ }
}
},
"dependencies": {
@@ -15209,6 +16195,12 @@
"integrity": "sha512-iTZ8cVGZ5dglNRyFdSj8U60mHIrC8XNIuOHN/NkM5/dQP4nsmpyqeQTAADLLQgoFCNJD+DiwQCv8dR2cCeWP4g==",
"optional": true
},
+ "@cfworker/json-schema": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
+ "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
+ "peer": true
+ },
"@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@@ -16205,6 +17197,51 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "@langchain/core": {
+ "version": "0.3.73",
+ "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.73.tgz",
+ "integrity": "sha512-E5dK9/MDH9671yuU3ZoMfSkMC7njtZrOZYrmLGk+2cvGk92yqxv/+MMxwOYoFtPMEWx9T8mTg2omHqcXXaEpGw==",
+ "peer": true,
+ "requires": {
+ "@cfworker/json-schema": "^4.0.2",
+ "ansi-styles": "^5.0.0",
+ "camelcase": "6",
+ "decamelize": "1.2.0",
+ "js-tiktoken": "^1.0.12",
+ "langsmith": "^0.3.46",
+ "mustache": "^4.2.0",
+ "p-queue": "^6.6.2",
+ "p-retry": "4",
+ "uuid": "^10.0.0",
+ "zod": "^3.25.32",
+ "zod-to-json-schema": "^3.22.3"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "peer": true
+ },
+ "camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "peer": true
+ }
+ }
+ },
+ "@langchain/mcp-adapters": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@langchain/mcp-adapters/-/mcp-adapters-0.6.0.tgz",
+ "integrity": "sha512-NHQNH9NciLhxlCnL/4HDebiYT3UQvpBfF5KPlIi/uSXn8te/bYjPV64gUyAloNNo+fjj4qDvKP1/nHj0r7fKFw==",
+ "requires": {
+ "@modelcontextprotocol/sdk": "^1.12.1",
+ "debug": "^4.4.0",
+ "extended-eventsource": "^1.x",
+ "zod": "^3.24.2"
+ }
+ },
"@malept/cross-spawn-promise": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
@@ -16255,6 +17292,25 @@
}
}
},
+ "@modelcontextprotocol/sdk": {
+ "version": "1.17.5",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz",
+ "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==",
+ "requires": {
+ "ajv": "^6.12.6",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.0.1",
+ "express-rate-limit": "^7.5.0",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.23.8",
+ "zod-to-json-schema": "^3.24.1"
+ }
+ },
"@mswjs/interceptors": {
"version": "0.38.7",
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz",
@@ -16529,6 +17585,12 @@
"@types/node": "*"
}
},
+ "@types/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
+ "peer": true
+ },
"@types/semver": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz",
@@ -16547,6 +17609,12 @@
"integrity": "sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==",
"dev": true
},
+ "@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "peer": true
+ },
"@types/verror": {
"version": "1.10.9",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz",
@@ -16716,6 +17784,30 @@
"integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
"dev": true
},
+ "accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "requires": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "dependencies": {
+ "mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="
+ },
+ "mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "requires": {
+ "mime-db": "^1.54.0"
+ }
+ }
+ }
+ },
"acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@@ -16742,7 +17834,6 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -17415,8 +18506,7 @@
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "dev": true
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"binary-extensions": {
"version": "2.2.0",
@@ -17466,6 +18556,22 @@
"bluebird": "^3.5.5"
}
},
+ "body-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+ "requires": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.0",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.6.3",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.0",
+ "raw-body": "^3.0.0",
+ "type-is": "^2.0.0"
+ }
+ },
"boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -17678,6 +18784,11 @@
"sax": "^1.2.4"
}
},
+ "bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
+ },
"call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@@ -17696,12 +18807,20 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "dev": true,
"requires": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
}
},
+ "call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "requires": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ }
+ },
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -17971,12 +19090,51 @@
}
}
},
+ "console-table-printer": {
+ "version": "2.14.6",
+ "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.14.6.tgz",
+ "integrity": "sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw==",
+ "peer": true,
+ "requires": {
+ "simple-wcswidth": "^1.0.1"
+ }
+ },
+ "content-disposition": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+ "requires": {
+ "safe-buffer": "5.2.1"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+ }
+ }
+ },
+ "content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
+ },
"convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
+ "cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="
+ },
+ "cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="
+ },
"copyfiles": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz",
@@ -18063,6 +19221,15 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
+ "cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "requires": {
+ "object-assign": "^4",
+ "vary": "^1"
+ }
+ },
"crc": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
@@ -18216,13 +19383,19 @@
}
},
"debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"requires": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
}
},
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "peer": true
+ },
"decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -18329,6 +19502,11 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+ },
"detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -18495,7 +19673,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dev": true,
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
@@ -18507,6 +19684,11 @@
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+ },
"ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@@ -18740,6 +19922,11 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
+ "encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
+ },
"encoding-sniffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
@@ -18849,14 +20036,12 @@
"es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "dev": true
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
"es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
},
"es-get-iterator": {
"version": "1.1.3",
@@ -18912,7 +20097,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "dev": true,
"requires": {
"es-errors": "^1.3.0"
}
@@ -18996,6 +20180,11 @@
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA=="
},
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+ },
"escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
@@ -19423,6 +20612,30 @@
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
},
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
+ },
+ "eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "peer": true
+ },
+ "eventsource": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+ "requires": {
+ "eventsource-parser": "^3.0.1"
+ }
+ },
+ "eventsource-parser": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
+ "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="
+ },
"execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -19467,6 +20680,67 @@
"jest-util": "^29.7.0"
}
},
+ "express": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+ "requires": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.0",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "dependencies": {
+ "mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="
+ },
+ "mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "requires": {
+ "mime-db": "^1.54.0"
+ }
+ }
+ }
+ },
+ "express-rate-limit": {
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
+ "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
+ "requires": {}
+ },
+ "extended-eventsource": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/extended-eventsource/-/extended-eventsource-1.7.0.tgz",
+ "integrity": "sha512-s8rtvZuYcKBpzytHb5g95cHbZ1J99WeMnV18oKc5wKoxkHzlzpPc/bNAm7Da2Db0BDw0CAu1z3LpH+7UsyzIpw==",
+ "optional": true
+ },
"extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@@ -19489,8 +20763,7 @@
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"fast-fifo": {
"version": "1.3.2",
@@ -19513,8 +20786,7 @@
"fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"fast-levenshtein": {
"version": "2.0.6",
@@ -19594,6 +20866,19 @@
"to-regex-range": "^5.0.1"
}
},
+ "finalhandler": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+ "requires": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ }
+ },
"find-process": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz",
@@ -19677,6 +20962,16 @@
"mime-types": "^2.1.12"
}
},
+ "forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
+ },
+ "fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="
+ },
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -19809,7 +21104,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "dev": true,
"requires": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
@@ -19833,7 +21127,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "dev": true,
"requires": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
@@ -19973,8 +21266,7 @@
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "dev": true
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"graceful-fs": {
"version": "4.2.11",
@@ -20039,8 +21331,7 @@
"has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "dev": true
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
"has-tostringtag": {
"version": "1.0.2",
@@ -20136,6 +21427,25 @@
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
+ "http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "requires": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "dependencies": {
+ "statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
+ }
+ }
+ },
"https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -20261,7 +21571,6 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
@@ -20343,6 +21652,11 @@
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="
},
+ "ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
+ },
"is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -20557,6 +21871,11 @@
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
"dev": true
},
+ "is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
+ },
"is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@@ -21272,6 +22591,15 @@
}
}
},
+ "js-tiktoken": {
+ "version": "1.0.21",
+ "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
+ "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
+ "peer": true,
+ "requires": {
+ "base64-js": "^1.5.1"
+ }
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -21309,8 +22637,7 @@
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@@ -21371,6 +22698,21 @@
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
"dev": true
},
+ "langsmith": {
+ "version": "0.3.67",
+ "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.67.tgz",
+ "integrity": "sha512-l4y3RmJ9yWF5a29fLg3eWZQxn6Q6dxTOgLGgQHzPGZHF3NUynn+A+airYIe/Yt4rwjGbuVrABAPsXBkVu/Hi7g==",
+ "peer": true,
+ "requires": {
+ "@types/uuid": "^10.0.0",
+ "chalk": "^4.1.2",
+ "console-table-printer": "^2.12.1",
+ "p-queue": "^6.6.2",
+ "p-retry": "4",
+ "semver": "^7.6.3",
+ "uuid": "^10.0.0"
+ }
+ },
"language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -21573,8 +22915,17 @@
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "dev": true
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
+ },
+ "media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="
+ },
+ "merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="
},
"merge-stream": {
"version": "2.0.0",
@@ -21716,9 +23067,15 @@
"dev": true
},
"ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "mustache": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
+ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
+ "peer": true
},
"natural-compare": {
"version": "1.4.0",
@@ -21726,6 +23083,11 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
+ "negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="
+ },
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -21828,15 +23190,12 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
- "dev": true
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-inspect": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
- "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
- "dev": true,
- "peer": true
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
},
"object-is": {
"version": "1.1.6",
@@ -21917,6 +23276,14 @@
"es-object-atoms": "^1.0.0"
}
},
+ "on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -21987,6 +23354,43 @@
}
}
},
+ "p-queue": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
+ "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
+ "peer": true,
+ "requires": {
+ "eventemitter3": "^4.0.4",
+ "p-timeout": "^3.2.0"
+ }
+ },
+ "p-retry": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
+ "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
+ "peer": true,
+ "requires": {
+ "@types/retry": "0.12.0",
+ "retry": "^0.13.1"
+ },
+ "dependencies": {
+ "retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "peer": true
+ }
+ }
+ },
+ "p-timeout": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
+ "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
+ "peer": true,
+ "requires": {
+ "p-finally": "^1.0.0"
+ }
+ },
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
@@ -22047,6 +23451,11 @@
"parse5": "^7.0.0"
}
},
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+ },
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -22090,6 +23499,11 @@
}
}
},
+ "path-to-regexp": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="
+ },
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -22119,6 +23533,11 @@
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
"dev": true
},
+ "pkce-challenge": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
+ "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="
+ },
"pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@@ -22249,6 +23668,15 @@
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag=="
},
+ "proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "requires": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ }
+ },
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -22261,8 +23689,7 @@
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
- "dev": true
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"pure-rand": {
"version": "6.1.0",
@@ -22270,6 +23697,14 @@
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
"dev": true
},
+ "qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "requires": {
+ "side-channel": "^1.1.0"
+ }
+ },
"queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -22323,6 +23758,32 @@
}
}
},
+ "range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
+ },
+ "raw-body": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
+ "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
+ "requires": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.7.0",
+ "unpipe": "1.0.0"
+ },
+ "dependencies": {
+ "iconv-lite": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
+ "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ }
+ }
+ }
+ },
"rcedit": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-4.0.1.tgz",
@@ -22640,6 +24101,18 @@
"sprintf-js": "^1.1.2"
}
},
+ "router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "requires": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ }
+ },
"rsvp": {
"version": "4.8.5",
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
@@ -22696,8 +24169,7 @@
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sanitize-filename": {
"version": "1.6.3",
@@ -22715,9 +24187,9 @@
"dev": true
},
"semver": {
- "version": "7.6.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
- "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w=="
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="
},
"semver-compare": {
"version": "1.0.0",
@@ -22726,6 +24198,39 @@
"dev": true,
"optional": true
},
+ "send": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+ "requires": {
+ "debug": "^4.3.5",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "mime-types": "^3.0.1",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.1"
+ },
+ "dependencies": {
+ "mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="
+ },
+ "mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "requires": {
+ "mime-db": "^1.54.0"
+ }
+ }
+ }
+ },
"serialize-error": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
@@ -22736,6 +24241,17 @@
"type-fest": "^0.13.1"
}
},
+ "serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+ "requires": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ }
+ },
"set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -22764,6 +24280,11 @@
"has-property-descriptors": "^1.0.2"
}
},
+ "setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -22878,16 +24399,47 @@
}
},
"side-channel": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
- "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
- "dev": true,
- "peer": true,
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"requires": {
- "call-bind": "^1.0.7",
"es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.4",
- "object-inspect": "^1.13.1"
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ }
+ },
+ "side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "requires": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ }
+ },
+ "side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "requires": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ }
+ },
+ "side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "requires": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
}
},
"signal-exit": {
@@ -22904,6 +24456,12 @@
"semver": "^7.5.3"
}
},
+ "simple-wcswidth": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz",
+ "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==",
+ "peer": true
+ },
"sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -22981,6 +24539,11 @@
"integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==",
"dev": true
},
+ "statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="
+ },
"stop-iteration-iterator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
@@ -23380,6 +24943,11 @@
"streamx": "^2.12.5"
}
},
+ "toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
+ },
"truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@@ -23450,6 +25018,31 @@
"dev": true,
"optional": true
},
+ "type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "requires": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "dependencies": {
+ "mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="
+ },
+ "mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "requires": {
+ "mime-db": "^1.54.0"
+ }
+ }
+ }
+ },
"typed-array-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
@@ -23581,6 +25174,11 @@
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
},
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
+ },
"untildify": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
@@ -23600,7 +25198,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
"requires": {
"punycode": "^2.1.0"
}
@@ -23616,6 +25213,12 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
+ "uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+ "peer": true
+ },
"v8-to-istanbul": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
@@ -23633,6 +25236,11 @@
"integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==",
"dev": true
},
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
+ },
"verror": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
@@ -24019,6 +25627,17 @@
}
}
}
+ },
+ "zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
+ },
+ "zod-to-json-schema": {
+ "version": "3.24.6",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
+ "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
+ "requires": {}
}
}
}
diff --git a/app/package.json b/app/package.json
index 084aba83d76..9c378222e73 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,6 +1,6 @@
{
"name": "headlamp",
- "version": "0.36.0",
+ "version": "0.37.0",
"description": "Easy-to-use and extensible Kubernetes web UI",
"main": "electron/main.js",
"homepage": "https://github.com/kubernetes-sigs/headlamp/#readme",
@@ -118,6 +118,7 @@
"files": [
"electron/main.js",
"electron/preload.js",
+ "electron/mcp-client.js",
"electron/i18next.config.js",
"electron/i18n-helper.js",
"electron/windowSize.js",
@@ -171,6 +172,7 @@
"typescript": "5.5.4"
},
"dependencies": {
+ "@langchain/mcp-adapters": "^0.6.0",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"dotenv": "^16.4.5",
diff --git a/app/windows/chocolatey/headlamp.nuspec b/app/windows/chocolatey/headlamp.nuspec
index e4ce6d35d89..a695efbb121 100644
--- a/app/windows/chocolatey/headlamp.nuspec
+++ b/app/windows/chocolatey/headlamp.nuspec
@@ -3,7 +3,7 @@
headlamp
- 0.36.0
+ 0.37.0
https://github.com/kubernetes-sigs/headlamp/tree/main/app/windows/chocolatey
Headlamp
Kinvolk
diff --git a/app/windows/chocolatey/tools/chocolateyinstall.ps1 b/app/windows/chocolatey/tools/chocolateyinstall.ps1
index c18103bbf7e..e7e4b2ac6d8 100644
--- a/app/windows/chocolatey/tools/chocolateyinstall.ps1
+++ b/app/windows/chocolatey/tools/chocolateyinstall.ps1
@@ -1,8 +1,8 @@
$ErrorActionPreference = 'Stop'; # stop on all errors
$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
-$headlampVersion = '0.36.0'
+$headlampVersion = '0.37.0'
$url = "https://github.com/kubernetes-sigs/headlamp/releases/download/v${headlampVersion}/Headlamp-${headlampVersion}-win-x64.exe"
-$checksum = '612678fabbc41bac8bae4b14e4cbbb4e888f77d24d97b5f125f44d4154648553'
+$checksum = 'c48779c5f7da1eeb0eeef636db61a029519f35219206c727d809d39e79453612'
$packageArgs = @{
packageName = $env:ChocolateyPackageName
diff --git a/backend/README.md b/backend/README.md
index 10aab9e198c..c537e54a90f 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -1,8 +1,8 @@
# Quickstart
```bash
-make backend
-make run-backend
+npm run backend:build
+npm run backend:start
```
See more detailed [Headlamp backend documentation on the web](
diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go
index 17d47378099..4bda7d1ce67 100644
--- a/backend/cmd/headlamp.go
+++ b/backend/cmd/headlamp.go
@@ -20,6 +20,7 @@ import (
"bytes"
"compress/gzip"
"context"
+ "crypto/rand"
"crypto/tls"
"encoding/base64"
"encoding/json"
@@ -34,6 +35,7 @@ import (
"path/filepath"
"runtime"
"strings"
+ "sync"
"syscall"
"time"
@@ -79,11 +81,20 @@ type HeadlampConfig struct {
oidcUseAccessToken bool
oidcSkipTLSVerify bool
oidcCACert string
+ oidcUsePKCE bool
cache cache.Cache[interface{}]
multiplexer *Multiplexer
telemetryConfig cfg.Config
oidcScopes []string
telemetryHandler *telemetry.RequestHandler
+ // meUsernamePaths lists the JMESPath expressions tried for the username in /clusters/{cluster}/me.
+ meUsernamePaths string
+ // meEmailPaths lists the JMESPath expressions tried for the email in /clusters/{cluster}/me.
+ meEmailPaths string
+ // meGroupsPaths lists the JMESPath expressions tried for the groups in /clusters/{cluster}/me.
+ meGroupsPaths string
+ // meUserInfoURL is the URL to fetch additional user info for the /me endpoint. /oauth2/userinfo
+ meUserInfoURL string
}
const DrainNodeCacheTTL = 20 // seconds
@@ -111,9 +122,11 @@ type clientConfig struct {
}
type OauthConfig struct {
- Config *oauth2.Config
- Verifier *oidc.IDTokenVerifier
- Ctx context.Context
+ Config *oauth2.Config
+ Verifier *oidc.IDTokenVerifier
+ Ctx context.Context
+ CodeVerifier string // PKCE code verifier
+ Cluster string // cluster context name this is associated with
}
// returns True if a file exists.
@@ -379,6 +392,13 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
logger.Log(logger.LevelInfo, nil, nil, "Proxy URLs: "+fmt.Sprint(config.ProxyURLs))
logger.Log(logger.LevelInfo, nil, nil, "TLS certificate path: "+config.TLSCertPath)
logger.Log(logger.LevelInfo, nil, nil, "TLS key path: "+config.TLSKeyPath)
+ logger.Log(logger.LevelInfo, nil, nil, "me Username Paths: "+config.meUsernamePaths)
+ logger.Log(logger.LevelInfo, nil, nil, "me Email Paths: "+config.meEmailPaths)
+ logger.Log(logger.LevelInfo, nil, nil, "me Groups Paths: "+config.meGroupsPaths)
+ logger.Log(logger.LevelInfo, nil, nil, "me User Info URL: "+config.meUserInfoURL)
+ logger.Log(logger.LevelInfo, nil, nil, "Base URL: "+config.BaseURL)
+ logger.Log(logger.LevelInfo, nil, nil, "Use In Cluster: "+fmt.Sprint(config.UseInCluster))
+ logger.Log(logger.LevelInfo, nil, nil, "Watch Plugins Changes: "+fmt.Sprint(config.WatchPluginsChanges))
plugins.PopulatePluginsCache(config.StaticPluginDir, config.PluginDir, config.cache)
@@ -477,6 +497,16 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
portforward.GetPortForwardByID(config.cache, w, r)
}).Methods("GET")
+ // Expose user info so the frontend can show the current user in the top bar using the per-cluster auth cookie.
+ r.HandleFunc("/clusters/{clusterName}/me",
+ auth.HandleMe(auth.MeHandlerOptions{
+ UsernamePaths: config.meUsernamePaths,
+ EmailPaths: config.meEmailPaths,
+ GroupsPaths: config.meGroupsPaths,
+ UserInfoURL: config.meUserInfoURL,
+ }),
+ ).Methods("GET")
+
config.handleClusterRequests(r)
r.HandleFunc("/externalproxy", func(w http.ResponseWriter, r *http.Request) {
@@ -600,7 +630,10 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
config.addClusterSetupRoute(r)
- oauthRequestMap := make(map[string]*OauthConfig)
+ var (
+ oauthRequestMap = make(map[string]*OauthConfig)
+ oauthMu sync.Mutex
+ )
r.HandleFunc("/oidc", func(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
@@ -665,12 +698,38 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
RedirectURL: getOidcCallbackURL(r, config),
Scopes: append([]string{oidc.ScopeOpenID}, oidcAuthConfig.Scopes...),
}
- /* we encode the cluster to base64 and set it as state so that when getting redirected
- by oidc we can use this state value to get cluster name
- */
- state := base64.StdEncoding.EncodeToString([]byte(cluster))
- oauthRequestMap[state] = &OauthConfig{Config: oauthConfig, Verifier: verifier, Ctx: ctx}
- http.Redirect(w, r, oauthConfig.AuthCodeURL(state), http.StatusFound)
+
+ // state should be unique per request, cryptographically secure random, url safe
+ state := func() string {
+ b := make([]byte, 32)
+ if _, err := rand.Read(b); err != nil {
+ panic(err)
+ }
+ return base64.RawURLEncoding.EncodeToString(b)
+ }()
+
+ entry := &OauthConfig{
+ Config: oauthConfig,
+ Verifier: verifier,
+ Ctx: ctx,
+ Cluster: cluster,
+ }
+
+ var authURL string
+
+ if config.oidcUsePKCE {
+ entry.CodeVerifier = oauth2.GenerateVerifier()
+ authURL = oauthConfig.AuthCodeURL(state, oauth2.S256ChallengeOption(entry.CodeVerifier))
+ } else {
+ authURL = oauthConfig.AuthCodeURL(state)
+ }
+
+ // Store the request config keyed by state for callback handling
+ oauthMu.Lock()
+ oauthRequestMap[state] = entry
+ oauthMu.Unlock()
+
+ http.Redirect(w, r, authURL, http.StatusFound)
}).Queries("cluster", "{cluster}")
r.HandleFunc("/drain-node", config.handleNodeDrain).Methods("POST")
@@ -680,93 +739,114 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
r.HandleFunc("/oidc-callback", func(w http.ResponseWriter, r *http.Request) {
state := r.URL.Query().Get("state")
- decodedState, err := base64.StdEncoding.DecodeString(state)
- if err != nil {
- logger.Log(logger.LevelError, nil, err, "failed to decode state")
- http.Error(w, "wrong state set, invalid request "+err.Error(), http.StatusBadRequest)
+ if state == "" {
+ logger.Log(logger.LevelError, nil, err, "invalid request state is empty")
+ http.Error(w, "invalid request state is empty", http.StatusBadRequest)
return
}
- if state == "" {
- logger.Log(logger.LevelError, nil, err, "invalid request state is empty")
- http.Error(w, "invalid request state is empty", http.StatusBadRequest)
+ oauthMu.Lock()
+
+ oauthConfig, ok := oauthRequestMap[state]
+ if ok {
+ // We have a copy of the oauthConfig, we can delete the map entry now
+ delete(oauthRequestMap, state)
+ }
+
+ oauthMu.Unlock()
+ if !ok {
+ http.Error(w, "invalid request", http.StatusBadRequest)
return
}
- //nolint:nestif
- if oauthConfig, ok := oauthRequestMap[state]; ok {
- oauth2Token, err := oauthConfig.Config.Exchange(oauthConfig.Ctx, r.URL.Query().Get("code"))
- if err != nil {
- logger.Log(logger.LevelError, nil, err, "failed to exchange token")
- http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
+ var oauth2Token *oauth2.Token
- return
- }
+ var err error
- tokenType := "id_token"
- if config.oidcUseAccessToken {
- tokenType = "access_token"
- }
+ // Exchange authorization code for token, with or without PKCE
+ if config.oidcUsePKCE && oauthConfig.CodeVerifier != "" {
+ // Use PKCE code verifier for token exchange
+ oauth2Token, err = oauthConfig.Config.Exchange(
+ oauthConfig.Ctx,
+ r.URL.Query().Get("code"),
+ oauth2.SetAuthURLParam("code_verifier", oauthConfig.CodeVerifier),
+ )
+ } else {
+ // Standard token exchange without PKCE
+ oauth2Token, err = oauthConfig.Config.Exchange(
+ oauthConfig.Ctx,
+ r.URL.Query().Get("code"),
+ )
+ }
- rawUserToken, ok := oauth2Token.Extra(tokenType).(string)
- if !ok {
- logger.Log(logger.LevelError, nil, err, fmt.Sprintf("no %s field in oauth2 token", tokenType))
- http.Error(w, fmt.Sprintf("No %s field in oauth2 token.", tokenType), http.StatusInternalServerError)
+ if err != nil {
+ logger.Log(logger.LevelError, nil, err, "failed to exchange token")
+ http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
- return
- }
+ return
+ }
- if err := config.cache.Set(context.Background(),
- fmt.Sprintf("oidc-token-%s", rawUserToken), oauth2Token.RefreshToken); err != nil {
- logger.Log(logger.LevelError, nil, err, "failed to cache refresh token")
- http.Error(w, "Failed to cache refresh token: "+err.Error(), http.StatusInternalServerError)
+ tokenType := "id_token"
+ if config.oidcUseAccessToken {
+ tokenType = "access_token"
+ }
- return
- }
+ rawUserToken, ok := oauth2Token.Extra(tokenType).(string)
+ if !ok {
+ logger.Log(logger.LevelError, nil, err, fmt.Sprintf("no %s field in oauth2 token", tokenType))
+ http.Error(w, fmt.Sprintf("No %s field in oauth2 token.", tokenType), http.StatusInternalServerError)
- idToken, err := oauthConfig.Verifier.Verify(oauthConfig.Ctx, rawUserToken)
- if err != nil {
- logger.Log(logger.LevelError, nil, err, "failed to verify ID Token")
- http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
- return
- }
+ if err := config.cache.Set(context.Background(),
+ fmt.Sprintf("oidc-token-%s", rawUserToken), oauth2Token.RefreshToken); err != nil {
+ logger.Log(logger.LevelError, nil, err, "failed to cache refresh token")
+ http.Error(w, "Failed to cache refresh token: "+err.Error(), http.StatusInternalServerError)
- resp := struct {
- OAuth2Token *oauth2.Token
- IDTokenClaims *json.RawMessage // ID Token payload is just JSON.
- }{oauth2Token, new(json.RawMessage)}
+ return
+ }
- if err := idToken.Claims(&resp.IDTokenClaims); err != nil {
- logger.Log(logger.LevelError, nil, err, "failed to get id token claims")
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ idToken, err := oauthConfig.Verifier.Verify(oauthConfig.Ctx, rawUserToken)
+ if err != nil {
+ logger.Log(logger.LevelError, nil, err, "failed to verify ID Token")
+ http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
- return
- }
+ return
+ }
- var redirectURL string
- if config.DevMode {
- redirectURL = "http://localhost:3000/"
- } else {
- redirectURL = "/"
- }
+ resp := struct {
+ OAuth2Token *oauth2.Token
+ IDTokenClaims *json.RawMessage // ID Token payload is just JSON.
+ }{oauth2Token, new(json.RawMessage)}
- baseURL := strings.Trim(config.BaseURL, "/")
- if baseURL != "" {
- redirectURL += baseURL + "/"
- }
+ if err := idToken.Claims(&resp.IDTokenClaims); err != nil {
+ logger.Log(logger.LevelError, nil, err, "failed to get id token claims")
+ http.Error(w, err.Error(), http.StatusInternalServerError)
- // Set auth cookie
- auth.SetTokenCookie(w, r, string(decodedState), rawUserToken, config.BaseURL)
+ return
+ }
- redirectURL += fmt.Sprintf("auth?cluster=%1s", decodedState)
- http.Redirect(w, r, redirectURL, http.StatusSeeOther)
+ var redirectURL string
+ if config.DevMode {
+ redirectURL = "http://localhost:3000/"
} else {
- http.Error(w, "invalid request", http.StatusBadRequest)
- return
+ redirectURL = "/"
+ }
+
+ baseURL := strings.Trim(config.BaseURL, "/")
+ if baseURL != "" {
+ redirectURL += baseURL + "/"
}
+
+ // Set auth cookie
+ auth.SetTokenCookie(w, r, oauthConfig.Cluster, rawUserToken, config.BaseURL)
+
+ redirectURL += fmt.Sprintf("auth?cluster=%1s", oauthConfig.Cluster)
+
+ http.Redirect(w, r, redirectURL, http.StatusSeeOther)
})
// Serve the frontend if needed
diff --git a/backend/cmd/server.go b/backend/cmd/server.go
index 72739ff7c39..73b434746e6 100644
--- a/backend/cmd/server.go
+++ b/backend/cmd/server.go
@@ -61,32 +61,52 @@ func main() {
StartHeadlampServer(headlampConfig)
}
+// buildHeadlampCFG maps the parsed config into the struct the backend uses.
+func buildHeadlampCFG(conf *config.Config, kubeConfigStore kubeconfig.ContextStore) *headlampconfig.HeadlampCFG {
+ return &headlampconfig.HeadlampCFG{
+ UseInCluster: conf.InCluster,
+ KubeConfigPath: conf.KubeConfigPath,
+ SkippedKubeContexts: conf.SkippedKubeContexts,
+ ListenAddr: conf.ListenAddr,
+ CacheEnabled: conf.CacheEnabled,
+ Port: conf.Port,
+ DevMode: conf.DevMode,
+ StaticDir: conf.StaticDir,
+ Insecure: conf.InsecureSsl,
+ PluginDir: conf.PluginsDir,
+ EnableHelm: conf.EnableHelm,
+ EnableDynamicClusters: conf.EnableDynamicClusters,
+ WatchPluginsChanges: conf.WatchPluginsChanges,
+ KubeConfigStore: kubeConfigStore,
+ BaseURL: conf.BaseURL,
+ ProxyURLs: strings.Split(conf.ProxyURLs, ","),
+ TLSCertPath: conf.TLSCertPath,
+ TLSKeyPath: conf.TLSKeyPath,
+ }
+}
+
+// buildTelemetryConfig collects only the telemetry fields and passes them along.
+func buildTelemetryConfig(conf *config.Config) config.Config {
+ return config.Config{
+ ServiceName: conf.ServiceName,
+ ServiceVersion: conf.ServiceVersion,
+ TracingEnabled: conf.TracingEnabled,
+ MetricsEnabled: conf.MetricsEnabled,
+ JaegerEndpoint: conf.JaegerEndpoint,
+ OTLPEndpoint: conf.OTLPEndpoint,
+ UseOTLPHTTP: conf.UseOTLPHTTP,
+ StdoutTraceEnabled: conf.StdoutTraceEnabled,
+ SamplingRate: conf.SamplingRate,
+ }
+}
+
func createHeadlampConfig(conf *config.Config) *HeadlampConfig {
cache := cache.New[interface{}]()
kubeConfigStore := kubeconfig.NewContextStore()
multiplexer := NewMultiplexer(kubeConfigStore)
headlampConfig := &HeadlampConfig{
- HeadlampCFG: &headlampconfig.HeadlampCFG{
- UseInCluster: conf.InCluster,
- KubeConfigPath: conf.KubeConfigPath,
- SkippedKubeContexts: conf.SkippedKubeContexts,
- ListenAddr: conf.ListenAddr,
- CacheEnabled: conf.CacheEnabled,
- Port: conf.Port,
- DevMode: conf.DevMode,
- StaticDir: conf.StaticDir,
- Insecure: conf.InsecureSsl,
- PluginDir: conf.PluginsDir,
- EnableHelm: conf.EnableHelm,
- EnableDynamicClusters: conf.EnableDynamicClusters,
- WatchPluginsChanges: conf.WatchPluginsChanges,
- KubeConfigStore: kubeConfigStore,
- BaseURL: conf.BaseURL,
- ProxyURLs: strings.Split(conf.ProxyURLs, ","),
- TLSCertPath: conf.TLSCertPath,
- TLSKeyPath: conf.TLSKeyPath,
- },
+ HeadlampCFG: buildHeadlampCFG(conf, kubeConfigStore),
oidcClientID: conf.OidcClientID,
oidcValidatorClientID: conf.OidcValidatorClientID,
oidcClientSecret: conf.OidcClientSecret,
@@ -96,19 +116,14 @@ func createHeadlampConfig(conf *config.Config) *HeadlampConfig {
oidcScopes: strings.Split(conf.OidcScopes, ","),
oidcSkipTLSVerify: conf.OidcSkipTLSVerify,
oidcUseAccessToken: conf.OidcUseAccessToken,
+ oidcUsePKCE: conf.OidcUsePKCE,
+ meUsernamePaths: conf.MeUsernamePath,
+ meEmailPaths: conf.MeEmailPath,
+ meGroupsPaths: conf.MeGroupsPath,
+ meUserInfoURL: conf.MeUserInfoURL,
cache: cache,
multiplexer: multiplexer,
- telemetryConfig: config.Config{
- ServiceName: conf.ServiceName,
- ServiceVersion: conf.ServiceVersion,
- TracingEnabled: conf.TracingEnabled,
- MetricsEnabled: conf.MetricsEnabled,
- JaegerEndpoint: conf.JaegerEndpoint,
- OTLPEndpoint: conf.OTLPEndpoint,
- UseOTLPHTTP: conf.UseOTLPHTTP,
- StdoutTraceEnabled: conf.StdoutTraceEnabled,
- SamplingRate: conf.SamplingRate,
- },
+ telemetryConfig: buildTelemetryConfig(conf),
}
if conf.OidcCAFile != "" {
diff --git a/backend/go.mod b/backend/go.mod
index 6486d0eed2d..22d05b7a4b2 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -30,6 +30,7 @@ require (
require (
github.com/coreos/go-oidc/v3 v3.11.0
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
+ github.com/jmespath/go-jmespath v0.4.0
github.com/prometheus/client_golang v1.22.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0
go.opentelemetry.io/otel v1.35.0
diff --git a/backend/go.sum b/backend/go.sum
index 3e04a1f03fc..854525104bd 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -290,6 +290,7 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
diff --git a/backend/pkg/auth/auth.go b/backend/pkg/auth/auth.go
index d8da4c63da3..2691cf9aa29 100644
--- a/backend/pkg/auth/auth.go
+++ b/backend/pkg/auth/auth.go
@@ -30,7 +30,10 @@ import (
"time"
"github.com/coreos/go-oidc/v3/oidc"
+ "github.com/gorilla/mux"
+ "github.com/jmespath/go-jmespath"
"github.com/kubernetes-sigs/headlamp/backend/pkg/cache"
+ cfg "github.com/kubernetes-sigs/headlamp/backend/pkg/config"
"github.com/kubernetes-sigs/headlamp/backend/pkg/kubeconfig"
"github.com/kubernetes-sigs/headlamp/backend/pkg/logger"
"golang.org/x/oauth2"
@@ -271,3 +274,216 @@ func RefreshAndCacheNewToken(ctx context.Context, oidcAuthConfig *kubeconfig.Oid
return newToken, nil
}
+
+type MeHandlerOptions struct {
+ // UsernamePaths is a list of JMESPath expressions to resolve the username claim.
+ UsernamePaths string
+ // EmailPaths is a list of JMESPath expressions to resolve the email claim.
+ EmailPaths string
+ // GroupsPaths is a list of JMESPath expressions to resolve group memberships.
+ GroupsPaths string
+ // UserInfoURL is the URL to fetch additional user info for the /me endpoint.
+ UserInfoURL string
+}
+
+// HandleMe returns a handler that reads the per-cluster auth cookie and responds with user info.
+func HandleMe(opts MeHandlerOptions) http.HandlerFunc {
+ usernamePaths, emailPaths, groupsPaths, userInfoURL := cfg.ApplyMeDefaults(
+ opts.UsernamePaths,
+ opts.EmailPaths,
+ opts.GroupsPaths,
+ opts.UserInfoURL,
+ )
+ compiledUsernamePaths := compileJMESPaths(usernamePaths)
+ compiledEmailPaths := compileJMESPaths(emailPaths)
+ compiledGroupsPaths := compileJMESPaths(groupsPaths)
+
+ return func(w http.ResponseWriter, r *http.Request) {
+ clusterName := mux.Vars(r)["clusterName"]
+ if clusterName == "" {
+ writeMeJSON(w, http.StatusBadRequest, map[string]interface{}{"message": "cluster not specified"})
+ return
+ }
+
+ requestCluster, token := ParseClusterAndToken(r)
+
+ if requestCluster == "" {
+ requestCluster = clusterName
+ }
+
+ if requestCluster != clusterName {
+ writeMeJSON(w, http.StatusBadRequest, map[string]interface{}{"message": "cluster mismatch"})
+ return
+ }
+
+ if token == "" {
+ writeMeJSON(w, http.StatusUnauthorized, map[string]interface{}{"message": "unauthorized"})
+ return
+ }
+
+ claims, status, errMsg := parseClaimsFromToken(token)
+ if status != 0 {
+ writeMeJSON(w, status, map[string]interface{}{"message": errMsg})
+ return
+ }
+
+ if expiry, err := GetExpiryUnixTimeUTC(claims); err != nil || time.Now().After(expiry) {
+ writeMeJSON(w, http.StatusUnauthorized, map[string]interface{}{"message": "token expired"})
+ return
+ }
+
+ username := stringValueFromJMESPaths(claims, compiledUsernamePaths)
+ email := stringValueFromJMESPaths(claims, compiledEmailPaths)
+ groups := stringSliceFromJMESPaths(claims, compiledGroupsPaths)
+
+ writeMeResponse(w, username, email, groups, userInfoURL)
+ }
+}
+
+// parseClaimsFromToken extracts the JWT claims from a token.
+func parseClaimsFromToken(token string) (map[string]interface{}, int, string) {
+ parts := strings.SplitN(token, ".", 3)
+ if len(parts) != 3 || parts[1] == "" {
+ return nil, http.StatusUnauthorized, "invalid token"
+ }
+
+ claims, err := DecodeBase64JSON(parts[1])
+ if err != nil {
+ return nil, http.StatusUnauthorized, "invalid token claims"
+ }
+
+ return claims, 0, ""
+}
+
+// writeMeResponse serializes the identity payload with the standard cache-busting headers.
+func writeMeResponse(w http.ResponseWriter, username, email string, groups []string, userInfoURL string) {
+ writeMeJSON(w, http.StatusOK, map[string]interface{}{
+ "username": username,
+ "email": email,
+ "groups": groups,
+ "userInfoURL": userInfoURL,
+ })
+}
+
+// writeMeJSON sets the standard cache-control headers used by /me responses and writes the JSON payload.
+func writeMeJSON(w http.ResponseWriter, status int, payload map[string]interface{}) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, private")
+ w.Header().Set("Pragma", "no-cache")
+ w.Header().Set("Expires", "0")
+ w.Header().Set("Vary", "Cookie")
+ w.Header().Del("ETag")
+
+ w.WriteHeader(status)
+
+ if err := json.NewEncoder(w).Encode(payload); err != nil {
+ logger.Log(logger.LevelError, nil, err, "failed to encode me response")
+ }
+}
+
+// stringValueFromJMESPaths iterates pre-compiled JMESPath expressions and returns the first string result.
+func stringValueFromJMESPaths(payload map[string]interface{}, paths []*jmespath.JMESPath) string {
+ for _, expr := range paths {
+ res, err := expr.Search(payload)
+ if err != nil || res == nil {
+ continue
+ }
+
+ switch v := res.(type) {
+ case string:
+ if v != "" {
+ return v
+ }
+ case fmt.Stringer:
+ vs := v.String()
+ if vs != "" {
+ return vs
+ }
+ case float64:
+ return fmt.Sprintf("%v", v)
+ case int64:
+ return fmt.Sprintf("%v", v)
+ case map[string]interface{}:
+ if encoded, ok := marshalToString(v); ok && encoded != "" {
+ return encoded
+ }
+ }
+ }
+
+ return ""
+}
+
+// stringSliceFromJMESPaths iterates pre-compiled JMESPath expressions and returns the first []string result.
+func stringSliceFromJMESPaths(payload map[string]interface{}, paths []*jmespath.JMESPath) []string {
+ for _, expr := range paths {
+ res, err := expr.Search(payload)
+ if err != nil || res == nil {
+ continue
+ }
+
+ switch v := res.(type) {
+ case []interface{}:
+ out := make([]string, 0, len(v))
+
+ for _, it := range v {
+ switch s := it.(type) {
+ case string:
+ out = append(out, s)
+ case float64:
+ out = append(out, fmt.Sprintf("%v", s))
+ case int64:
+ out = append(out, fmt.Sprintf("%v", s))
+ default:
+ if encoded, ok := marshalToString(it); ok && encoded != "" {
+ out = append(out, encoded)
+ }
+ }
+ }
+
+ return out
+ case []string:
+ return v
+ }
+ }
+
+ return []string{}
+}
+
+// compileJMESPaths parses and compiles a list of JMESPath expressions once.
+func compileJMESPaths(pathCSV string) []*jmespath.JMESPath {
+ if strings.TrimSpace(pathCSV) == "" {
+ return []*jmespath.JMESPath{}
+ }
+
+ rawPaths := strings.Split(pathCSV, ",")
+ compiled := make([]*jmespath.JMESPath, 0, len(rawPaths))
+
+ for _, raw := range rawPaths {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ continue
+ }
+
+ expr, err := jmespath.Compile(raw)
+ if err != nil {
+ logger.Log(logger.LevelWarn, map[string]string{"jmespath": raw}, err,
+ "failed to compile JMESPath expression, skipping")
+ continue
+ }
+
+ compiled = append(compiled, expr)
+ }
+
+ return compiled
+}
+
+// marshalToString encodes the provided value as JSON and logs failures.
+func marshalToString(val interface{}) (string, bool) {
+ b, err := json.Marshal(val)
+ if err != nil {
+ logger.Log(logger.LevelWarn, nil, err, "failed to marshal value to JSON string")
+ return "", false
+ }
+
+ return string(b), true
+}
diff --git a/backend/pkg/auth/auth_test.go b/backend/pkg/auth/auth_test.go
index 85e0e856199..f165863e05b 100644
--- a/backend/pkg/auth/auth_test.go
+++ b/backend/pkg/auth/auth_test.go
@@ -23,6 +23,7 @@ import (
"encoding/json"
"encoding/pem"
"errors"
+ "fmt"
"net/http"
"net/http/httptest"
"os"
@@ -31,6 +32,7 @@ import (
"testing"
"time"
+ "github.com/gorilla/mux"
"github.com/kubernetes-sigs/headlamp/backend/pkg/auth"
"github.com/kubernetes-sigs/headlamp/backend/pkg/cache"
"github.com/kubernetes-sigs/headlamp/backend/pkg/kubeconfig"
@@ -852,3 +854,174 @@ func TestConfigureTLSContext_CACert(t *testing.T) {
require.NoError(t, err)
assert.True(t, caCertParsed.IsCA, "Generated certificate should be a CA certificate")
}
+
+func makeTestToken(t *testing.T, claims map[string]interface{}) string {
+ // helper to build unsigned JWT-like string for tests
+ header := map[string]string{"alg": "none", "typ": "JWT"}
+ headerJSON, err := json.Marshal(header)
+ require.NoError(t, err)
+ claimsJSON, err := json.Marshal(claims)
+ require.NoError(t, err)
+
+ return fmt.Sprintf("%s.%s.signature",
+ base64.RawURLEncoding.EncodeToString(headerJSON),
+ base64.RawURLEncoding.EncodeToString(claimsJSON),
+ )
+}
+
+func TestHandleMe_Success(t *testing.T) {
+ t.Parallel()
+
+ expiry := time.Now().Add(time.Hour).Unix()
+ claims := map[string]interface{}{
+ "preferred_username": "alice",
+ "email": "alice@example.com",
+ "groups": []string{"dev", "ops"},
+ "exp": float64(expiry),
+ }
+
+ token := makeTestToken(t, claims)
+
+ req := httptest.NewRequest(http.MethodGet, "/clusters/test/me", nil)
+ req = mux.SetURLVars(req, map[string]string{"clusterName": "test"})
+ req.AddCookie(&http.Cookie{
+ Name: fmt.Sprintf("headlamp-auth-%s.0", auth.SanitizeClusterName("test")),
+ Value: token,
+ })
+
+ rr := httptest.NewRecorder()
+
+ handler := auth.HandleMe(auth.MeHandlerOptions{
+ UsernamePaths: "preferred_username",
+ EmailPaths: "email",
+ GroupsPaths: "groups",
+ })
+
+ handler(rr, req)
+
+ require.Equal(t, http.StatusOK, rr.Code)
+
+ var got struct {
+ Username string `json:"username"`
+ Email string `json:"email"`
+ Groups []string `json:"groups"`
+ }
+
+ require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &got))
+
+ assert.Equal(t, "alice", got.Username)
+ assert.Equal(t, "alice@example.com", got.Email)
+ assert.Equal(t, []string{"dev", "ops"}, got.Groups)
+ assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
+ assert.Equal(t, "no-store, no-cache, must-revalidate, private", rr.Header().Get("Cache-Control"))
+ assert.Equal(t, "Cookie", rr.Header().Get("Vary"))
+}
+
+func TestHandleMe_HeaderToken(t *testing.T) {
+ t.Parallel()
+
+ expiry := time.Now().Add(time.Hour).Unix()
+ claims := map[string]interface{}{
+ "preferred_username": "alice",
+ "email": "alice@example.com",
+ "groups": []string{"dev", "ops"},
+ "exp": float64(expiry),
+ }
+
+ token := makeTestToken(t, claims)
+
+ req := httptest.NewRequest(http.MethodGet, "/clusters/test/me", nil)
+ req = mux.SetURLVars(req, map[string]string{"clusterName": "test"})
+ req.Header.Set("Authorization", "Bearer "+token)
+
+ rr := httptest.NewRecorder()
+
+ handler := auth.HandleMe(auth.MeHandlerOptions{
+ UsernamePaths: "preferred_username",
+ EmailPaths: "email",
+ GroupsPaths: "groups",
+ })
+
+ handler(rr, req)
+
+ require.Equal(t, http.StatusOK, rr.Code)
+
+ var got struct {
+ Username string `json:"username"`
+ Email string `json:"email"`
+ Groups []string `json:"groups"`
+ }
+
+ require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &got))
+ assert.Equal(t, "alice", got.Username)
+ assert.Equal(t, "alice@example.com", got.Email)
+ assert.Equal(t, []string{"dev", "ops"}, got.Groups)
+}
+
+func TestHandleMe_ExpiredToken(t *testing.T) {
+ t.Parallel()
+
+ expiry := time.Now().Add(-time.Hour).Unix()
+ claims := map[string]interface{}{
+ "preferred_username": "alice",
+ "email": "alice@example.com",
+ "groups": []string{"dev", "ops"},
+ "exp": float64(expiry),
+ }
+
+ token := makeTestToken(t, claims)
+
+ req := httptest.NewRequest(http.MethodGet, "/clusters/test/me", nil)
+ req = mux.SetURLVars(req, map[string]string{"clusterName": "test"})
+ req.AddCookie(&http.Cookie{
+ Name: fmt.Sprintf("headlamp-auth-%s.0", auth.SanitizeClusterName("test")),
+ Value: token,
+ })
+
+ rr := httptest.NewRecorder()
+
+ handler := auth.HandleMe(auth.MeHandlerOptions{
+ UsernamePaths: "preferred_username",
+ EmailPaths: "email",
+ GroupsPaths: "groups",
+ })
+
+ handler(rr, req)
+
+ require.Equal(t, http.StatusUnauthorized, rr.Code)
+
+ var got struct {
+ Message string `json:"message"`
+ }
+
+ require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &got))
+ assert.Equal(t, "token expired", got.Message)
+ assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
+ assert.Equal(t, "no-store, no-cache, must-revalidate, private", rr.Header().Get("Cache-Control"))
+ assert.Equal(t, "Cookie", rr.Header().Get("Vary"))
+}
+
+func TestHandleMe_MissingCookie(t *testing.T) {
+ t.Parallel()
+
+ req := httptest.NewRequest(http.MethodGet, "/clusters/test/me", nil)
+ req = mux.SetURLVars(req, map[string]string{"clusterName": "test"})
+
+ rr := httptest.NewRecorder()
+
+ handler := auth.HandleMe(auth.MeHandlerOptions{})
+
+ handler(rr, req)
+
+ require.Equal(t, http.StatusUnauthorized, rr.Code)
+
+ var got struct {
+ Message string `json:"message"`
+ }
+
+ require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &got))
+ assert.Equal(t, "unauthorized", got.Message)
+ assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
+ assert.Equal(t, "no-store, no-cache, must-revalidate, private", rr.Header().Get("Cache-Control"))
+ assert.Equal(t, "Cookie", rr.Header().Get("Vary"))
+}
diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go
index 76c3c8f00d6..a7092101349 100644
--- a/backend/pkg/config/config.go
+++ b/backend/pkg/config/config.go
@@ -20,6 +20,13 @@ import (
const defaultPort = 4466
+const (
+ DefaultMeUsernamePath = "preferred_username,upn,username,name"
+ DefaultMeEmailPath = "email"
+ DefaultMeGroupsPath = "groups,realm_access.roles"
+ DefaultMeUserInfoURL = ""
+)
+
type Config struct {
Version bool `koanf:"version"`
InCluster bool `koanf:"in-cluster"`
@@ -47,6 +54,11 @@ type Config struct {
OidcUseAccessToken bool `koanf:"oidc-use-access-token"`
OidcSkipTLSVerify bool `koanf:"oidc-skip-tls-verify"`
OidcCAFile string `koanf:"oidc-ca-file"`
+ MeUsernamePath string `koanf:"me-username-path"`
+ MeEmailPath string `koanf:"me-email-path"`
+ MeGroupsPath string `koanf:"me-groups-path"`
+ MeUserInfoURL string `koanf:"me-user-info-url"`
+ OidcUsePKCE bool `koanf:"oidc-use-pkce"`
// telemetry configs
ServiceName string `koanf:"service-name"`
ServiceVersion *string `koanf:"service-version"`
@@ -219,6 +231,41 @@ func setKubeConfigPath(config *Config) {
}
}
+// ApplyMeDefaults trims and applies defaults to the JMESPath expressions used for the /me endpoint.
+func ApplyMeDefaults(usernamePath, emailPath, groupsPath, userInfoURL string) (string, string, string, string) {
+ username := strings.TrimSpace(usernamePath)
+ if username == "" {
+ username = DefaultMeUsernamePath
+ }
+
+ email := strings.TrimSpace(emailPath)
+ if email == "" {
+ email = DefaultMeEmailPath
+ }
+
+ groups := strings.TrimSpace(groupsPath)
+ if groups == "" {
+ groups = DefaultMeGroupsPath
+ }
+
+ userInfo := strings.TrimSpace(userInfoURL)
+ if userInfo == "" {
+ userInfo = DefaultMeUserInfoURL
+ }
+
+ return username, email, groups, userInfo
+}
+
+// setMeDefaults ensures the /clusters/{clusterName}/me claim paths fall back to defaults when unset.
+func setMeDefaults(config *Config) {
+ config.MeUsernamePath, config.MeEmailPath, config.MeGroupsPath, config.MeUserInfoURL = ApplyMeDefaults(
+ config.MeUsernamePath,
+ config.MeEmailPath,
+ config.MeGroupsPath,
+ config.MeUserInfoURL,
+ )
+}
+
// Parse Loads the config from flags and env.
// env vars should start with HEADLAMP_CONFIG_ and use _ as separator
// If a value is set both in flags and env then flag takes priority.
@@ -267,6 +314,7 @@ func Parse(args []string) (*Config, error) {
// 7. Post-process: patch plugin flag and kubeconfig path.
patchWatchPluginsChanges(&config, explicitFlags)
setKubeConfigPath(&config)
+ setMeDefaults(&config)
// 8. Validate parsed config.
if err := config.Validate(); err != nil {
@@ -320,6 +368,15 @@ func DefaultHeadlampKubeConfigFile() (string, error) {
func flagset() *flag.FlagSet {
f := flag.NewFlagSet("config", flag.ContinueOnError)
+ addGeneralFlags(f)
+ addOIDCFlags(f)
+ addTelemetryFlags(f)
+ addTLSFlags(f)
+
+ return f
+}
+
+func addGeneralFlags(f *flag.FlagSet) {
f.Bool("version", false, "Print version information and exit")
f.Bool("in-cluster", false, "Set when running from a k8s cluster")
f.Bool("dev", false, "Allow connections from other origins")
@@ -337,18 +394,32 @@ func flagset() *flag.FlagSet {
f.String("listen-addr", "", "Address to listen on; default is empty, which means listening to any address")
f.Uint("port", defaultPort, "Port to listen from")
f.String("proxy-urls", "", "Allow proxy requests to specified URLs")
+ f.Bool("enable-helm", false, "Enable Helm operations")
+}
+func addOIDCFlags(f *flag.FlagSet) {
f.String("oidc-client-id", "", "ClientID for OIDC")
f.String("oidc-client-secret", "", "ClientSecret for OIDC")
f.String("oidc-validator-client-id", "", "Override ClientID for OIDC during validation")
f.String("oidc-idp-issuer-url", "", "Identity provider issuer URL for OIDC")
f.String("oidc-callback-url", "", "Callback URL for OIDC")
f.String("oidc-validator-idp-issuer-url", "", "Override Identity provider issuer URL for OIDC during validation")
- f.String("oidc-scopes", "profile,email",
- "A comma separated list of scopes needed from the OIDC provider")
+ f.String("oidc-scopes", "profile,email", "A comma separated list of scopes needed from the OIDC provider")
f.Bool("oidc-skip-tls-verify", false, "Skip TLS verification for OIDC")
f.String("oidc-ca-file", "", "CA file for OIDC")
f.Bool("oidc-use-access-token", false, "Setup oidc to pass through the access_token instead of the default id_token")
+ f.Bool("oidc-use-pkce", false, "Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow")
+ f.String("me-username-path", DefaultMeUsernamePath,
+ "Comma separated JMESPath expressions used to read username from the JWT payload")
+ f.String("me-email-path", DefaultMeEmailPath,
+ "Comma separated JMESPath expressions used to read email from the JWT payload")
+ f.String("me-groups-path", DefaultMeGroupsPath,
+ "Comma separated JMESPath expressions used to read groups from the JWT payload")
+ f.String("me-user-info-url", DefaultMeUserInfoURL,
+ "URL to fetch additional user info for the /me endpoint. For oauth2proxy /oauth2/userinfo can be used.")
+}
+
+func addTelemetryFlags(f *flag.FlagSet) {
// Telemetry flags.
f.String("service-name", "headlamp", "Service name for telemetry")
f.String("service-version", "0.30.0", "Service version for telemetry")
@@ -358,12 +429,12 @@ func flagset() *flag.FlagSet {
f.Bool("use-otlp-http", false, "Use HTTP instead of gRPC for OTLP export")
f.Bool("stdout-trace-enabled", false, "Enable tracing output to stdout")
f.Float64("sampling-rate", 1.0, "Sampling rate for traces")
+}
+
+func addTLSFlags(f *flag.FlagSet) {
// TLS flags
f.String("tls-cert-path", "", "Certificate for serving TLS")
f.String("tls-key-path", "", "Key for serving TLS")
- f.Bool("enable-helm", false, "Enable Helm operations")
-
- return f
}
// Gets the default plugins-dir depending on platform.
diff --git a/backend/pkg/config/config_test.go b/backend/pkg/config/config_test.go
index 9059482a23a..8ff162bba58 100644
--- a/backend/pkg/config/config_test.go
+++ b/backend/pkg/config/config_test.go
@@ -41,6 +41,9 @@ func TestParseBasic(t *testing.T) {
assert.Equal(t, "", conf.ListenAddr)
assert.Equal(t, uint(4466), conf.Port)
assert.Equal(t, "profile,email", conf.OidcScopes)
+ assert.Equal(t, config.DefaultMeUsernamePath, conf.MeUsernamePath)
+ assert.Equal(t, config.DefaultMeEmailPath, conf.MeEmailPath)
+ assert.Equal(t, config.DefaultMeGroupsPath, conf.MeGroupsPath)
},
},
{
@@ -48,6 +51,7 @@ func TestParseBasic(t *testing.T) {
args: []string{"go run ./cmd", "--port=3456"},
verify: func(t *testing.T, conf *config.Config) {
assert.Equal(t, uint(3456), conf.Port)
+ assert.Equal(t, config.DefaultMeUsernamePath, conf.MeUsernamePath)
},
},
}
@@ -63,47 +67,63 @@ func TestParseBasic(t *testing.T) {
}
}
-func TestParseWithEnv(t *testing.T) {
- tests := []struct {
- name string
- args []string
- env map[string]string
- verify func(*testing.T, *config.Config)
- }{
- {
- name: "from_env",
- args: []string{"go run ./cmd", "-in-cluster"},
- env: map[string]string{
- "HEADLAMP_CONFIG_OIDC_CLIENT_SECRET": "superSecretBotsStayAwayPlease",
- },
- verify: func(t *testing.T, conf *config.Config) {
- assert.Equal(t, "superSecretBotsStayAwayPlease", conf.OidcClientSecret)
- },
+var ParseWithEnvTests = []struct {
+ name string
+ args []string
+ env map[string]string
+ verify func(*testing.T, *config.Config)
+}{
+ {
+ name: "from_env",
+ args: []string{"go run ./cmd", "-in-cluster"},
+ env: map[string]string{
+ "HEADLAMP_CONFIG_OIDC_CLIENT_SECRET": "superSecretBotsStayAwayPlease",
},
- {
- name: "both_args_and_env",
- args: []string{"go run ./cmd", "--port=9876"},
- env: map[string]string{
- "HEADLAMP_CONFIG_PORT": "1234",
- },
- verify: func(t *testing.T, conf *config.Config) {
- assert.NotEqual(t, uint(1234), conf.Port)
- assert.Equal(t, uint(9876), conf.Port)
- },
+ verify: func(t *testing.T, conf *config.Config) {
+ assert.Equal(t, "superSecretBotsStayAwayPlease", conf.OidcClientSecret)
},
- {
- name: "kubeconfig_from_default_env",
- args: []string{"go run ./cmd"},
- env: map[string]string{
- "KUBECONFIG": "~/.kube/test_config.yaml",
- },
- verify: func(t *testing.T, conf *config.Config) {
- assert.Equal(t, "~/.kube/test_config.yaml", conf.KubeConfigPath)
- },
+ },
+ {
+ name: "both_args_and_env",
+ args: []string{"go run ./cmd", "--port=9876"},
+ env: map[string]string{
+ "HEADLAMP_CONFIG_PORT": "1234",
},
- }
+ verify: func(t *testing.T, conf *config.Config) {
+ assert.NotEqual(t, uint(1234), conf.Port)
+ assert.Equal(t, uint(9876), conf.Port)
+ },
+ },
+ {
+ name: "me_paths",
+ args: []string{"go run ./cmd"},
+ env: map[string]string{
+ "HEADLAMP_CONFIG_ME_USERNAME_PATH": "user.name",
+ "HEADLAMP_CONFIG_ME_EMAIL_PATH": "user.email",
+ "HEADLAMP_CONFIG_ME_GROUPS_PATH": "user.groups",
+ "HEADLAMP_CONFIG_ME_USER_INFO_URL": "/oauth2/userinfo",
+ },
+ verify: func(t *testing.T, conf *config.Config) {
+ assert.Equal(t, "user.name", conf.MeUsernamePath)
+ assert.Equal(t, "user.email", conf.MeEmailPath)
+ assert.Equal(t, "user.groups", conf.MeGroupsPath)
+ assert.Equal(t, "/oauth2/userinfo", conf.MeUserInfoURL)
+ },
+ },
+ {
+ name: "kubeconfig_from_default_env",
+ args: []string{"go run ./cmd"},
+ env: map[string]string{
+ "KUBECONFIG": "~/.kube/test_config.yaml",
+ },
+ verify: func(t *testing.T, conf *config.Config) {
+ assert.Equal(t, "~/.kube/test_config.yaml", conf.KubeConfigPath)
+ },
+ },
+}
- for _, tt := range tests {
+func TestParseWithEnv(t *testing.T) {
+ for _, tt := range ParseWithEnvTests {
t.Run(tt.name, func(t *testing.T) {
for key, value := range tt.env {
os.Setenv(key, value)
diff --git a/backend/pkg/kubeconfig/kubeconfig.go b/backend/pkg/kubeconfig/kubeconfig.go
index 1960db1e799..4dff110abf1 100644
--- a/backend/pkg/kubeconfig/kubeconfig.go
+++ b/backend/pkg/kubeconfig/kubeconfig.go
@@ -975,7 +975,8 @@ func GetInClusterContext(oidcIssuerURL string,
var oidcConf *OidcConfig
- if oidcClientID != "" && oidcClientSecret != "" && oidcIssuerURL != "" && oidcScopes != "" {
+ if oidcClientID != "" && oidcIssuerURL != "" && oidcScopes != "" {
+ // client secret is optional for in-cluster OIDC configuration
oidcConf = &OidcConfig{
ClientID: oidcClientID,
ClientSecret: oidcClientSecret,
diff --git a/backend/pkg/serviceproxy/connection.go b/backend/pkg/serviceproxy/connection.go
index 834dbe08c4f..292be596119 100644
--- a/backend/pkg/serviceproxy/connection.go
+++ b/backend/pkg/serviceproxy/connection.go
@@ -35,6 +35,10 @@ func (c *Connection) Get(requestURI string) ([]byte, error) {
return nil, fmt.Errorf("invalid request uri: %w", err)
}
+ if rel.IsAbs() {
+ return nil, fmt.Errorf("request uri must be a relative path")
+ }
+
fullURL := base.ResolveReference(rel)
body, err := HTTPGet(context.Background(), fullURL.String())
diff --git a/backend/pkg/serviceproxy/connection_test.go b/backend/pkg/serviceproxy/connection_test.go
index 3bdb1e6bcb9..71d4490cae6 100644
--- a/backend/pkg/serviceproxy/connection_test.go
+++ b/backend/pkg/serviceproxy/connection_test.go
@@ -41,38 +41,45 @@ func TestNewConnection(t *testing.T) {
}
}
-func TestGet(t *testing.T) {
- tests := []struct {
- name string
- uri string
- requestURI string
- wantBody []byte
- wantErr bool
- }{
- {
- name: "valid request",
- uri: "http://example.com",
- requestURI: "/test",
- wantBody: []byte("Hello, World!"),
- wantErr: false,
- },
- {
- name: "invalid URI",
- uri: " invalid-uri",
- requestURI: "/test",
- wantBody: nil,
- wantErr: true,
- },
- {
- name: "invalid request URI",
- uri: "http://example.com",
- requestURI: " invalid-request-uri",
- wantBody: nil,
- wantErr: true,
- },
- }
+var getTests = []struct {
+ name string
+ uri string
+ requestURI string
+ wantBody []byte
+ wantErr bool
+}{
+ {
+ name: "valid request",
+ uri: "http://example.com",
+ requestURI: "/test",
+ wantBody: []byte("Hello, World!"),
+ wantErr: false,
+ },
+ {
+ name: "invalid URI",
+ uri: " invalid-uri",
+ requestURI: "/test",
+ wantBody: nil,
+ wantErr: true,
+ },
+ {
+ name: "invalid request URI",
+ uri: "http://example.com",
+ requestURI: " invalid-request-uri",
+ wantBody: nil,
+ wantErr: true,
+ },
+ {
+ name: "absolute request URI rejected",
+ uri: "http://example.com",
+ requestURI: "http://malicious.local",
+ wantBody: nil,
+ wantErr: true,
+ },
+}
- for _, tt := range tests {
+func TestGet(t *testing.T) {
+ for _, tt := range getTests {
t.Run(tt.name, func(t *testing.T) {
conn := &Connection{URI: tt.uri}
diff --git a/charts/headlamp/Chart.yaml b/charts/headlamp/Chart.yaml
index 4f67c132b07..fbc369e49c6 100644
--- a/charts/headlamp/Chart.yaml
+++ b/charts/headlamp/Chart.yaml
@@ -20,8 +20,8 @@ sources:
maintainers:
- name: kinvolk
url: https://kinvolk.io/
-version: 0.36.0
-appVersion: 0.36.0
+version: 0.37.0
+appVersion: 0.37.0
annotations:
artifacthub.io/signKey: |
fingerprint: 2956B7F7167769370C93730C7264DA7B85D08A37
diff --git a/charts/headlamp/README.md b/charts/headlamp/README.md
index 26b2fba7efe..f837b817b9f 100644
--- a/charts/headlamp/README.md
+++ b/charts/headlamp/README.md
@@ -85,10 +85,12 @@ $ helm install my-headlamp headlamp/headlamp \
| config.oidc.clientSecret | string | `""` | OIDC client secret |
| config.oidc.issuerURL | string | `""` | OIDC issuer URL |
| config.oidc.scopes | string | `""` | OIDC scopes to be used |
+| config.oidc.usePKCE | bool | `false` | Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow |
| config.oidc.secret.create | bool | `true` | Create OIDC secret using provided values |
| config.oidc.secret.name | string | `"oidc"` | Name of the OIDC secret |
| config.oidc.externalSecret.enabled | bool | `false` | Enable using external secret for OIDC |
| config.oidc.externalSecret.name | string | `""` | Name of external OIDC secret |
+| config.oidc.meUserInfoURL | string | `""` | URL to fetch additional user info for the /me endpoint. For oauth2proxy /oauth2/userinfo can be used. |
There are three ways to configure OIDC:
diff --git a/charts/headlamp/templates/deployment.yaml b/charts/headlamp/templates/deployment.yaml
index 772dce8fb42..81d91fa8f2c 100644
--- a/charts/headlamp/templates/deployment.yaml
+++ b/charts/headlamp/templates/deployment.yaml
@@ -8,7 +8,9 @@
{{- $callbackURL := "" }}
{{- $validatorClientID := "" }}
{{- $validatorIssuerURL := "" }}
+{{- $usePKCE := "" }}
{{- $useAccessToken := "" }}
+{{- $meUserInfoURL := "" }}
# This block of code is used to extract the values from the env.
# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml.
@@ -37,6 +39,12 @@
{{- if eq .name "OIDC_USE_ACCESS_TOKEN" }}
{{- $useAccessToken = .value | toString }}
{{- end }}
+ {{- if eq .name "OIDC_USE_PKCE" }}
+ {{- $usePKCE = .value | toString }}
+ {{- end }}
+ {{- if eq .name "ME_USER_INFO_URL" }}
+ {{- $meUserInfoURL = .value | toString }}
+ {{- end }}
{{- end }}
apiVersion: apps/v1
@@ -159,6 +167,20 @@ spec:
name: {{ $oidc.secret.name }}
key: useAccessToken
{{- end }}
+ {{- if $oidc.usePKCE }}
+ - name: OIDC_USE_PKCE
+ valueFrom:
+ secretKeyRef:
+ name: {{ $oidc.secret.name }}
+ key: usePKCE
+ {{- end }}
+ {{- if $oidc.meUserInfoURL }}
+ - name: ME_USER_INFO_URL
+ valueFrom:
+ secretKeyRef:
+ name: {{ $oidc.secret.name }}
+ key: meUserInfoURL
+ {{- end }}
{{- else }}
{{- if $oidc.clientID }}
- name: OIDC_CLIENT_ID
@@ -192,6 +214,14 @@ spec:
- name: OIDC_USE_ACCESS_TOKEN
value: {{ $oidc.useAccessToken }}
{{- end }}
+ {{- if $oidc.usePKCE }}
+ - name: OIDC_USE_PKCE
+ value: {{ $oidc.usePKCE }}
+ {{- end }}
+ {{- if $oidc.meUserInfoURL }}
+ - name: ME_USER_INFO_URL
+ value: {{ $oidc.meUserInfoURL }}
+ {{- end }}
{{- end }}
{{- if .Values.env }}
{{- toYaml .Values.env | nindent 12 }}
@@ -245,6 +275,12 @@ spec:
# Check if useAccessToken is non false either from env or oidc.config
- "-oidc-use-access-token=$(OIDC_USE_ACCESS_TOKEN)"
{{- end }}
+ {{- if or (eq ($oidc.usePKCE | toString) "true") (eq $usePKCE "true") }}
+ - "-oidc-use-pkce=$(OIDC_USE_PKCE)"
+ {{- end }}
+ {{- if or (ne $oidc.meUserInfoURL "") (ne $meUserInfoURL "") }}
+ - "-me-user-info-url=$(ME_USER_INFO_URL)"
+ {{- end }}
{{- else }}
- "-oidc-client-id=$(OIDC_CLIENT_ID)"
- "-oidc-client-secret=$(OIDC_CLIENT_SECRET)"
@@ -262,6 +298,9 @@ spec:
# Check if validatorIssuerURL is non empty either from env or oidc.config
- "-oidc-validator-idp-issuer-url=$(OIDC_VALIDATOR_ISSUER_URL)"
{{- end }}
+ {{- if or (ne $oidc.meUserInfoURL "") (ne $meUserInfoURL "") }}
+ - "-me-user-info-url=$(ME_USER_INFO_URL)"
+ {{- end }}
{{- end }}
{{- with .Values.config.baseURL }}
- "-base-url={{ . }}"
@@ -309,13 +348,15 @@ spec:
{{- end }}
args:
- |
- echo "Installing headlamp-plugin globally..."
- npm install -g @headlamp-k8s/pluginctl@{{ .Values.pluginsManager.version }}
- echo "Installed headlamp-plugin successfully."
if [ -f "/config/plugin.yml" ]; then
echo "Installing plugins from config..."
cat /config/plugin.yml
- pluginctl install --config /config/plugin.yml --folderName {{ .Values.config.pluginsDir }} --watch
+ # Use a writable cache directory
+ export NPM_CONFIG_CACHE=/tmp/npm-cache
+ # Use a writable config directory
+ export NPM_CONFIG_USERCONFIG=/tmp/npm-userconfig
+ mkdir -p /tmp/npm-cache /tmp/npm-userconfig
+ npx --yes @headlamp-k8s/pluginctl@{{ .Values.pluginsManager.version }} install --config /config/plugin.yml --folderName {{ .Values.config.pluginsDir }} --watch
fi
volumeMounts:
- name: plugins-dir
diff --git a/charts/headlamp/templates/plugin-configmap.yaml b/charts/headlamp/templates/plugin-configmap.yaml
index 5d5619fb4d1..43bf007bde7 100644
--- a/charts/headlamp/templates/plugin-configmap.yaml
+++ b/charts/headlamp/templates/plugin-configmap.yaml
@@ -1,4 +1,4 @@
-{{- if .Values.pluginsManager.enabled }}
+{{- if .Values.pluginsManager.enabled -}}
apiVersion: v1
kind: ConfigMap
metadata:
@@ -7,6 +7,5 @@ metadata:
labels:
{{- include "headlamp.labels" . | nindent 4 }}
data:
- plugin.yml: |
- {{ .Values.pluginsManager.configContent | nindent 4 }}
+ plugin.yml: |{{ .Values.pluginsManager.configContent | nindent 4 }}
{{- end }}
diff --git a/charts/headlamp/templates/secret.yaml b/charts/headlamp/templates/secret.yaml
index 1af93d96fed..afda05afa01 100644
--- a/charts/headlamp/templates/secret.yaml
+++ b/charts/headlamp/templates/secret.yaml
@@ -31,5 +31,8 @@ data:
{{- with .useAccessToken }}
useAccessToken: {{ . | toString | b64enc | quote }}
{{- end }}
+{{- with .usePKCE }}
+ usePKCE: {{ . | toString | b64enc | quote }}
+{{- end }}
{{- end }}
{{- end }}
diff --git a/charts/headlamp/templates/service.yaml b/charts/headlamp/templates/service.yaml
index 1a80c333d1b..16a1dd82ec5 100644
--- a/charts/headlamp/templates/service.yaml
+++ b/charts/headlamp/templates/service.yaml
@@ -18,7 +18,7 @@ spec:
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy | quote }}
{{- end }}
{{ if (and (eq .Values.service.type "LoadBalancer") (not (empty .Values.service.loadBalancerSourceRanges))) }}
- loadBalancerSourceRanges: {{ .Values.service.loadBalancerSourceRanges }}
+ loadBalancerSourceRanges: {{ .Values.service.loadBalancerSourceRanges | toYaml | nindent 2 }}
{{ end }}
{{- if (and (eq .Values.service.type "LoadBalancer") (not (empty .Values.service.loadBalancerIP))) }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
diff --git a/charts/headlamp/tests/expected_templates/azure-oidc-with-validators.yaml b/charts/headlamp/tests/expected_templates/azure-oidc-with-validators.yaml
index 0a5436a766e..ca2e9474ef1 100644
--- a/charts/headlamp/tests/expected_templates/azure-oidc-with-validators.yaml
+++ b/charts/headlamp/tests/expected_templates/azure-oidc-with-validators.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/clusterrolebinding.yaml
@@ -18,10 +18,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -39,10 +39,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -66,10 +66,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -94,7 +94,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/default.yaml b/charts/headlamp/tests/expected_templates/default.yaml
index 181ab0a31e2..c55a00079d9 100644
--- a/charts/headlamp/tests/expected_templates/default.yaml
+++ b/charts/headlamp/tests/expected_templates/default.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/secret.yaml
@@ -27,10 +27,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -48,10 +48,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -75,10 +75,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -103,7 +103,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/extra-args.yaml b/charts/headlamp/tests/expected_templates/extra-args.yaml
index 000cfe891d1..232c40a31c2 100644
--- a/charts/headlamp/tests/expected_templates/extra-args.yaml
+++ b/charts/headlamp/tests/expected_templates/extra-args.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/secret.yaml
@@ -27,10 +27,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -48,10 +48,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -75,10 +75,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -103,7 +103,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/extra-manifests.yaml b/charts/headlamp/tests/expected_templates/extra-manifests.yaml
index 348aae4e04b..54caf8fdb06 100644
--- a/charts/headlamp/tests/expected_templates/extra-manifests.yaml
+++ b/charts/headlamp/tests/expected_templates/extra-manifests.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/secret.yaml
@@ -44,10 +44,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -65,10 +65,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -92,10 +92,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -120,7 +120,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml b/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml
new file mode 100644
index 00000000000..6fbc748a641
--- /dev/null
+++ b/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml
@@ -0,0 +1,130 @@
+---
+# Source: headlamp/templates/serviceaccount.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: headlamp
+ namespace: default
+ labels:
+ helm.sh/chart: headlamp-0.37.0
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ app.kubernetes.io/version: "0.37.0"
+ app.kubernetes.io/managed-by: Helm
+---
+# Source: headlamp/templates/secret.yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ name: oidc
+ namespace: default
+type: Opaque
+data:
+---
+# Source: headlamp/templates/clusterrolebinding.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: headlamp-admin
+ labels:
+ helm.sh/chart: headlamp-0.37.0
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ app.kubernetes.io/version: "0.37.0"
+ app.kubernetes.io/managed-by: Helm
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cluster-admin
+subjects:
+- kind: ServiceAccount
+ name: headlamp
+ namespace: default
+---
+# Source: headlamp/templates/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ name: headlamp
+ namespace: default
+ labels:
+ helm.sh/chart: headlamp-0.37.0
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ app.kubernetes.io/version: "0.37.0"
+ app.kubernetes.io/managed-by: Helm
+spec:
+ type: ClusterIP
+
+ ports:
+ - port: 80
+ targetPort: http
+ protocol: TCP
+ name: http
+ selector:
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+---
+# Source: headlamp/templates/deployment.yaml
+# This block of code is used to extract the values from the env.
+# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: headlamp
+ namespace: default
+ labels:
+ helm.sh/chart: headlamp-0.37.0
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ app.kubernetes.io/version: "0.37.0"
+ app.kubernetes.io/managed-by: Helm
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ spec:
+ serviceAccountName: headlamp
+ automountServiceAccountToken: true
+ securityContext:
+ {}
+ containers:
+ - name: headlamp
+ securityContext:
+ privileged: false
+ runAsGroup: 101
+ runAsNonRoot: true
+ runAsUser: 100
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
+ imagePullPolicy: IfNotPresent
+
+ env:
+ - name: ME_USER_INFO_URL
+ value: /oauth2/userinfocustom1
+ args:
+ - "-in-cluster"
+ - "-plugins-dir=/headlamp/plugins"
+ # Check if externalSecret is disabled
+ - "-me-user-info-url=$(ME_USER_INFO_URL)"
+ ports:
+ - name: http
+ containerPort: 4466
+ protocol: TCP
+ livenessProbe:
+ httpGet:
+ path: "/"
+ port: http
+ readinessProbe:
+ httpGet:
+ path: "/"
+ port: http
+ resources:
+ {}
diff --git a/charts/headlamp/tests/expected_templates/me-user-info-url.yaml b/charts/headlamp/tests/expected_templates/me-user-info-url.yaml
new file mode 100644
index 00000000000..0eb1535488a
--- /dev/null
+++ b/charts/headlamp/tests/expected_templates/me-user-info-url.yaml
@@ -0,0 +1,133 @@
+---
+# Source: headlamp/templates/serviceaccount.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: headlamp
+ namespace: default
+ labels:
+ helm.sh/chart: headlamp-0.37.0
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ app.kubernetes.io/version: "0.37.0"
+ app.kubernetes.io/managed-by: Helm
+---
+# Source: headlamp/templates/secret.yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ name: oidc
+ namespace: default
+type: Opaque
+data:
+---
+# Source: headlamp/templates/clusterrolebinding.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: headlamp-admin
+ labels:
+ helm.sh/chart: headlamp-0.37.0
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ app.kubernetes.io/version: "0.37.0"
+ app.kubernetes.io/managed-by: Helm
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cluster-admin
+subjects:
+- kind: ServiceAccount
+ name: headlamp
+ namespace: default
+---
+# Source: headlamp/templates/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ name: headlamp
+ namespace: default
+ labels:
+ helm.sh/chart: headlamp-0.37.0
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ app.kubernetes.io/version: "0.37.0"
+ app.kubernetes.io/managed-by: Helm
+spec:
+ type: ClusterIP
+
+ ports:
+ - port: 80
+ targetPort: http
+ protocol: TCP
+ name: http
+ selector:
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+---
+# Source: headlamp/templates/deployment.yaml
+# This block of code is used to extract the values from the env.
+# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: headlamp
+ namespace: default
+ labels:
+ helm.sh/chart: headlamp-0.37.0
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ app.kubernetes.io/version: "0.37.0"
+ app.kubernetes.io/managed-by: Helm
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ spec:
+ serviceAccountName: headlamp
+ automountServiceAccountToken: true
+ securityContext:
+ {}
+ containers:
+ - name: headlamp
+ securityContext:
+ privileged: false
+ runAsGroup: 101
+ runAsNonRoot: true
+ runAsUser: 100
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
+ imagePullPolicy: IfNotPresent
+
+ env:
+ - name: ME_USER_INFO_URL
+ valueFrom:
+ secretKeyRef:
+ name: oidc
+ key: meUserInfoURL
+ args:
+ - "-in-cluster"
+ - "-plugins-dir=/headlamp/plugins"
+ # Check if externalSecret is disabled
+ - "-me-user-info-url=$(ME_USER_INFO_URL)"
+ ports:
+ - name: http
+ containerPort: 4466
+ protocol: TCP
+ livenessProbe:
+ httpGet:
+ path: "/"
+ port: http
+ readinessProbe:
+ httpGet:
+ path: "/"
+ port: http
+ resources:
+ {}
diff --git a/charts/headlamp/tests/expected_templates/namespace-override-oidc-create-secret.yaml b/charts/headlamp/tests/expected_templates/namespace-override-oidc-create-secret.yaml
index 6f088f5140b..5707d18dd6a 100644
--- a/charts/headlamp/tests/expected_templates/namespace-override-oidc-create-secret.yaml
+++ b/charts/headlamp/tests/expected_templates/namespace-override-oidc-create-secret.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: mynamespace2
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/secret.yaml
@@ -31,10 +31,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -52,10 +52,10 @@ metadata:
name: headlamp
namespace: mynamespace2
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -79,10 +79,10 @@ metadata:
name: headlamp
namespace: mynamespace2
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -107,7 +107,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/namespace-override.yaml b/charts/headlamp/tests/expected_templates/namespace-override.yaml
index 6033e353291..ea03730cc63 100644
--- a/charts/headlamp/tests/expected_templates/namespace-override.yaml
+++ b/charts/headlamp/tests/expected_templates/namespace-override.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: mynamespace
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/secret.yaml
@@ -27,10 +27,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -48,10 +48,10 @@ metadata:
name: headlamp
namespace: mynamespace
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -75,10 +75,10 @@ metadata:
name: headlamp
namespace: mynamespace
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -103,7 +103,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/non-azure-oidc.yaml b/charts/headlamp/tests/expected_templates/non-azure-oidc.yaml
index 3032a1e9e61..25110e14dc1 100644
--- a/charts/headlamp/tests/expected_templates/non-azure-oidc.yaml
+++ b/charts/headlamp/tests/expected_templates/non-azure-oidc.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/clusterrolebinding.yaml
@@ -18,10 +18,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -39,10 +39,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -66,10 +66,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -94,7 +94,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/oidc-create-secret.yaml b/charts/headlamp/tests/expected_templates/oidc-create-secret.yaml
index bff34aeb6ef..9160482da27 100644
--- a/charts/headlamp/tests/expected_templates/oidc-create-secret.yaml
+++ b/charts/headlamp/tests/expected_templates/oidc-create-secret.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/secret.yaml
@@ -31,10 +31,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -52,10 +52,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -79,10 +79,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -107,7 +107,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/oidc-directly-env.yaml b/charts/headlamp/tests/expected_templates/oidc-directly-env.yaml
index 3585650a06a..b6a74a28e3f 100644
--- a/charts/headlamp/tests/expected_templates/oidc-directly-env.yaml
+++ b/charts/headlamp/tests/expected_templates/oidc-directly-env.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/secret.yaml
@@ -27,10 +27,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -48,10 +48,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -75,10 +75,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -103,7 +103,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/oidc-directly.yaml b/charts/headlamp/tests/expected_templates/oidc-directly.yaml
index 8e55710efb5..4b09dca2458 100644
--- a/charts/headlamp/tests/expected_templates/oidc-directly.yaml
+++ b/charts/headlamp/tests/expected_templates/oidc-directly.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/clusterrolebinding.yaml
@@ -18,10 +18,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -39,10 +39,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -66,10 +66,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -94,7 +94,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/oidc-external-secret.yaml b/charts/headlamp/tests/expected_templates/oidc-external-secret.yaml
index 8046ae39524..f8b72a5bdff 100644
--- a/charts/headlamp/tests/expected_templates/oidc-external-secret.yaml
+++ b/charts/headlamp/tests/expected_templates/oidc-external-secret.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/clusterrolebinding.yaml
@@ -18,10 +18,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -39,10 +39,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -66,10 +66,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -94,7 +94,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
# Check if externalSecret is enabled
diff --git a/charts/headlamp/tests/expected_templates/oidc-pkce.yaml b/charts/headlamp/tests/expected_templates/oidc-pkce.yaml
new file mode 100644
index 00000000000..0f5f0d1cf46
--- /dev/null
+++ b/charts/headlamp/tests/expected_templates/oidc-pkce.yaml
@@ -0,0 +1,137 @@
+---
+# Source: headlamp/templates/serviceaccount.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: headlamp
+ namespace: default
+ labels:
+ helm.sh/chart: headlamp-0.37.0
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ app.kubernetes.io/version: "0.37.0"
+ app.kubernetes.io/managed-by: Helm
+---
+# Source: headlamp/templates/clusterrolebinding.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: headlamp-admin
+ labels:
+ helm.sh/chart: headlamp-0.37.0
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ app.kubernetes.io/version: "0.37.0"
+ app.kubernetes.io/managed-by: Helm
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cluster-admin
+subjects:
+- kind: ServiceAccount
+ name: headlamp
+ namespace: default
+---
+# Source: headlamp/templates/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ name: headlamp
+ namespace: default
+ labels:
+ helm.sh/chart: headlamp-0.37.0
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ app.kubernetes.io/version: "0.37.0"
+ app.kubernetes.io/managed-by: Helm
+spec:
+ type: ClusterIP
+
+ ports:
+ - port: 80
+ targetPort: http
+ protocol: TCP
+ name: http
+ selector:
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+---
+# Source: headlamp/templates/deployment.yaml
+# This block of code is used to extract the values from the env.
+# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: headlamp
+ namespace: default
+ labels:
+ helm.sh/chart: headlamp-0.37.0
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ app.kubernetes.io/version: "0.37.0"
+ app.kubernetes.io/managed-by: Helm
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: headlamp
+ app.kubernetes.io/instance: headlamp
+ spec:
+ serviceAccountName: headlamp
+ automountServiceAccountToken: true
+ securityContext:
+ {}
+ containers:
+ - name: headlamp
+ securityContext:
+ privileged: false
+ runAsGroup: 101
+ runAsNonRoot: true
+ runAsUser: 100
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
+ imagePullPolicy: IfNotPresent
+
+ env:
+ - name: OIDC_CLIENT_ID
+ value: testClientId
+ - name: OIDC_CLIENT_SECRET
+ value: testClientSecret
+ - name: OIDC_ISSUER_URL
+ value: testIssuerURL
+ - name: OIDC_SCOPES
+ value: testScope
+ - name: OIDC_USE_PKCE
+ value: true
+ args:
+ - "-in-cluster"
+ - "-plugins-dir=/headlamp/plugins"
+ # Check if externalSecret is disabled
+ # Check if clientID is non empty either from env or oidc.config
+ - "-oidc-client-id=$(OIDC_CLIENT_ID)"
+ # Check if clientSecret is non empty either from env or oidc.config
+ - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)"
+ # Check if issuerURL is non empty either from env or oidc.config
+ - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)"
+ # Check if scopes are non empty either from env or oidc.config
+ - "-oidc-scopes=$(OIDC_SCOPES)"
+ - "-oidc-use-pkce=$(OIDC_USE_PKCE)"
+ ports:
+ - name: http
+ containerPort: 4466
+ protocol: TCP
+ livenessProbe:
+ httpGet:
+ path: "/"
+ port: http
+ readinessProbe:
+ httpGet:
+ path: "/"
+ port: http
+ resources:
+ {}
diff --git a/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml b/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml
index 93d36fb53bd..be55526671f 100644
--- a/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml
+++ b/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/clusterrolebinding.yaml
@@ -18,10 +18,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -39,10 +39,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -66,10 +66,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -94,7 +94,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/pod-disruption.yaml b/charts/headlamp/tests/expected_templates/pod-disruption.yaml
index 6176a1be43e..570fce727e2 100644
--- a/charts/headlamp/tests/expected_templates/pod-disruption.yaml
+++ b/charts/headlamp/tests/expected_templates/pod-disruption.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
maxUnavailable: 1
@@ -26,10 +26,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/secret.yaml
@@ -47,10 +47,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -68,10 +68,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -95,10 +95,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -123,7 +123,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/security-context.yaml b/charts/headlamp/tests/expected_templates/security-context.yaml
index 18830ca82c0..0cefaa4cae3 100644
--- a/charts/headlamp/tests/expected_templates/security-context.yaml
+++ b/charts/headlamp/tests/expected_templates/security-context.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/secret.yaml
@@ -28,10 +28,10 @@ metadata:
name: headlamp-plugin-config
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
data:
plugin.yml: |
@@ -42,10 +42,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -63,10 +63,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -90,10 +90,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -129,7 +129,7 @@ spec:
runAsUser: 100
seccompProfile:
type: RuntimeDefault
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
@@ -159,13 +159,15 @@ spec:
command: ["/bin/sh", "-c"]
args:
- |
- echo "Installing headlamp-plugin globally..."
- npm install -g @headlamp-k8s/pluginctl@1.0.0
- echo "Installed headlamp-plugin successfully."
if [ -f "/config/plugin.yml" ]; then
echo "Installing plugins from config..."
cat /config/plugin.yml
- pluginctl install --config /config/plugin.yml --folderName /headlamp/plugins --watch
+ # Use a writable cache directory
+ export NPM_CONFIG_CACHE=/tmp/npm-cache
+ # Use a writable config directory
+ export NPM_CONFIG_USERCONFIG=/tmp/npm-userconfig
+ mkdir -p /tmp/npm-cache /tmp/npm-userconfig
+ npx --yes @headlamp-k8s/pluginctl@1.0.0 install --config /config/plugin.yml --folderName /headlamp/plugins --watch
fi
volumeMounts:
- name: plugins-dir
diff --git a/charts/headlamp/tests/expected_templates/tls-added.yaml b/charts/headlamp/tests/expected_templates/tls-added.yaml
index 75897e055ec..6bbdbbb0f18 100644
--- a/charts/headlamp/tests/expected_templates/tls-added.yaml
+++ b/charts/headlamp/tests/expected_templates/tls-added.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/secret.yaml
@@ -27,10 +27,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -48,10 +48,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -75,10 +75,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -103,7 +103,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/expected_templates/volumes-added.yaml b/charts/headlamp/tests/expected_templates/volumes-added.yaml
index 662717fc1ac..8e47e86cb3e 100644
--- a/charts/headlamp/tests/expected_templates/volumes-added.yaml
+++ b/charts/headlamp/tests/expected_templates/volumes-added.yaml
@@ -6,10 +6,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
---
# Source: headlamp/templates/secret.yaml
@@ -27,10 +27,10 @@ kind: ClusterRoleBinding
metadata:
name: headlamp-admin
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -48,10 +48,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
@@ -75,10 +75,10 @@ metadata:
name: headlamp
namespace: default
labels:
- helm.sh/chart: headlamp-0.36.0
+ helm.sh/chart: headlamp-0.37.0
app.kubernetes.io/name: headlamp
app.kubernetes.io/instance: headlamp
- app.kubernetes.io/version: "0.36.0"
+ app.kubernetes.io/version: "0.37.0"
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
@@ -103,7 +103,7 @@ spec:
runAsGroup: 101
runAsNonRoot: true
runAsUser: 100
- image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0"
+ image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0"
imagePullPolicy: IfNotPresent
env:
diff --git a/charts/headlamp/tests/test_cases/me-user-info-url-directly.yaml b/charts/headlamp/tests/test_cases/me-user-info-url-directly.yaml
new file mode 100644
index 00000000000..149928b4816
--- /dev/null
+++ b/charts/headlamp/tests/test_cases/me-user-info-url-directly.yaml
@@ -0,0 +1,3 @@
+env:
+ - name: ME_USER_INFO_URL
+ value: /oauth2/userinfocustom1
diff --git a/charts/headlamp/tests/test_cases/me-user-info-url.yaml b/charts/headlamp/tests/test_cases/me-user-info-url.yaml
new file mode 100644
index 00000000000..76c4f640500
--- /dev/null
+++ b/charts/headlamp/tests/test_cases/me-user-info-url.yaml
@@ -0,0 +1,4 @@
+# -- Headlamp OIDC me user info URL test case
+config:
+ oidc:
+ meUserInfoURL: /oauth2/userinfocustom2
diff --git a/charts/headlamp/tests/test_cases/oidc-pkce.yaml b/charts/headlamp/tests/test_cases/oidc-pkce.yaml
new file mode 100644
index 00000000000..f8a5ed5ec48
--- /dev/null
+++ b/charts/headlamp/tests/test_cases/oidc-pkce.yaml
@@ -0,0 +1,15 @@
+# This is a test case for the direct OIDC configuration in the Headlamp deployment.
+# The oidc.secret.create field is false to avoid creating a secret for OIDC.
+# The oidc.clientID field is a string that specifies the client ID for OIDC.
+# The oidc.clientSecret field is a string that specifies the client secret for OIDC.
+# The oidc.issuerURL field is a string that specifies the issuer URL for OIDC.
+# The oidc.scopes field is a string that specifies the scopes for OIDC.
+config:
+ oidc:
+ secret:
+ create: false
+ clientID: "testClientId"
+ clientSecret: "testClientSecret"
+ issuerURL: "testIssuerURL"
+ scopes: "testScope"
+ usePKCE: true
diff --git a/charts/headlamp/values.schema.json b/charts/headlamp/values.schema.json
index c3282d045d5..e6e86b531b9 100644
--- a/charts/headlamp/values.schema.json
+++ b/charts/headlamp/values.schema.json
@@ -207,6 +207,10 @@
"type": "string",
"description": "Scopes of the OIDC provider"
},
+ "usePKCE": {
+ "type": "boolean",
+ "description": "Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow"
+ },
"externalSecret": {
"type": "object",
"description": "External secret to use for OIDC configuration",
diff --git a/charts/headlamp/values.yaml b/charts/headlamp/values.yaml
index 4f1281dbe7d..413bed7c475 100644
--- a/charts/headlamp/values.yaml
+++ b/charts/headlamp/values.yaml
@@ -78,6 +78,8 @@ config:
validatorIssuerURL: ""
# -- Use 'access_token' instead of 'id_token' when authenticating using OIDC
useAccessToken: false
+ # -- Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow
+ usePKCE: false
# Option 3:
# @param config.oidc - External OIDC secret configuration
@@ -94,6 +96,11 @@ config:
externalSecret:
enabled: false
name: ""
+
+ # -- URL to fetch additional user info for the /me endpoint.
+ # For oauth2proxy /oauth2/userinfo can be used. Empty and it will not be used.
+ meUserInfoURL: ""
+
# -- directory to look for plugins
pluginsDir: "/headlamp/plugins"
enableHelm: false
diff --git a/docs/contributing.md b/docs/contributing.md
index f2e6c395a7f..d3c345d5eb3 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -55,8 +55,8 @@ Follow these steps when submitting a PR to ensure it meets the project’s stand
Run the following commands from your project directory:
-- `make frontend-test` - Run the test suite
-- `make frontend-lint` - Format your code to match the project
+- `npm run frontend:test` - Run the test suite
+- `npm run frontend:lint` - Format your code to match the project
These steps ensure your code is functional, well-typed, and formatted consistently.
@@ -156,11 +156,11 @@ For linting the `backend` and `frontend`, use the following commands
(respectively):
```bash
-make backend-lint
+npm run backend:lint
```
```bash
-make frontend-lint
+npm run frontend:lint
```
The linters are also run in the CI system, so any PRs you create will be
@@ -194,13 +194,13 @@ an associated story when possible.
For running the frontend tests, use the following command:
```bash
-make frontend-test
+npm run frontend:test
```
The backend uses go's testing and can be run by using the following command:
```bash
-make backend-test
+npm run backend:test
```
Tests will run as part of the CI after a Pull Request is open.
diff --git a/docs/development/architecture.md b/docs/development/architecture.md
index 2e63e6a7c54..3862d3c9c0b 100644
--- a/docs/development/architecture.md
+++ b/docs/development/architecture.md
@@ -128,23 +128,23 @@ The following components are in separate GitHub repos:
- **Plugins**: Extensible modules that add custom functionality to the UI. The Headlamp team maintains their plugins in the [headlamp-k8s/plugins repo](https://github.com/headlamp-k8s/plugins). These include plugins for projects like Flux, Backstage and Inspektor Gadget.
- **Headlamp Website**: Maintained in the [headlamp-k8s/website repo](https://github.com/headlamp-k8s/website). This contains things like the blog and the documentation. The website can be found at https://headlamp.dev/
-### Makefile task entry point
+### npm scripts entry point
-The headlamp/ repo [Makefile](https://github.com/kubernetes-sigs/headlamp/blob/main/Makefile) contains targets for building and testing different components.
+The headlamp/ repo root [package.json](https://github.com/kubernetes-sigs/headlamp/blob/main/package.json) contains scripts for building and testing different components.
Here are some examples:
```shell
-make backend
-make backend-lint
-make backend-test
-make run-backend
-make frontend
-make frontend-lint
-make frontend-test
-make run-frontend
-make app-test
-make run-app
+npm run backend:build
+npm run backend:lint
+npm run backend:test
+npm run backend:start
+npm run frontend:build
+npm run frontend:lint
+npm run frontend:test
+npm run frontend:start
+npm run app:test
+npm run start:app
```
### Frontend
diff --git a/docs/development/backend.md b/docs/development/backend.md
index 0dc214d9375..d4feb0d2387 100644
--- a/docs/development/backend.md
+++ b/docs/development/backend.md
@@ -18,13 +18,13 @@ redirects the requests to the defined proxies.
The backend (Headlamp's server) can be quickly built using:
```bash
-make backend
+npm run backend:build
```
Once built, it can be run in development mode (insecure / don't use in production) using:
```bash
-make run-backend
+npm run backend:start
```
## Lint
@@ -32,13 +32,13 @@ make run-backend
To lint the backend/ code.
```bash
-make backend-lint
+npm run backend:lint
```
This command can fix some lint issues.
```bash
-make backend-lint-fix
+npm run backend:lint:fix
```
## Format
@@ -46,23 +46,23 @@ make backend-lint-fix
To format the backend code.
```bash
-make backend-format
+npm run backend:format
```
## Test
```bash
-make backend-test
+npm run backend:test
```
Test coverage with a html report in the browser.
```bash
-make backend-coverage-html
+npm run backend:coverage:html
```
To just print a simpler coverage report to the console.
```bash
-make backend-coverage
+npm run backend:coverage
```
diff --git a/docs/development/frontend.md b/docs/development/frontend.md
index 190c304d478..6d7b091f52e 100644
--- a/docs/development/frontend.md
+++ b/docs/development/frontend.md
@@ -15,13 +15,13 @@ The frontend is written in Typescript and React, as well as a few other importan
The frontend can be quickly built using:
```bash
-make frontend
+npm run frontend:build
```
Once built, it can be run in development mode (auto-refresh) using:
```bash
-make run-frontend
+npm run frontend:start
```
This command leverages the `create-react-app`'s start script that launches
@@ -35,7 +35,7 @@ for network request, if you need the devtools for react-query, you can simply se
API documentation for TypeScript is done with [typedoc](https://typedoc.org/) and [typedoc-plugin-markdown](https://github.com/tgreyuk/typedoc-plugin-markdown), and is configured in tsconfig.json
```bash
-make docs
+npm run docs
```
The API output markdown is generated in docs/development/api and is not
@@ -49,7 +49,7 @@ Components can be discovered, developed, and tested inside the 'storybook'.
From within the [Headlamp](https://github.com/kubernetes-sigs/headlamp/) repo run:
```bash
-make storybook
+npm run frontend:storybook
```
If you are adding new stories, please wrap your story components with the `TestContext` helper
@@ -75,7 +75,7 @@ Any issues found are reported in the developer console.
To enable the alert message during development, use the following:
```bash
-REACT_APP_SKIP_A11Y=false make run-frontend
+REACT_APP_SKIP_A11Y=false npm run frontend:start
```
This shows an alert when an a11y issue is detected.
diff --git a/docs/development/i18n/contributing.md b/docs/development/i18n/contributing.md
index 0bfa660368f..1c962fe433f 100644
--- a/docs/development/i18n/contributing.md
+++ b/docs/development/i18n/contributing.md
@@ -91,7 +91,7 @@ Here's an example of using date formatting:
Create a folder using the locale code in:
`frontend/src/i18n/locales/`
-Then run `make i18n`. This command parses the translatable strings in
+Then run `npm run i18n`. This command parses the translatable strings in
the project and creates the corresponding catalog files.
Integrated components may need to be adjusted (MaterialUI/Monaco etc).
diff --git a/docs/development/index.md b/docs/development/index.md
index 4daa85bd075..ee060c93be1 100644
--- a/docs/development/index.md
+++ b/docs/development/index.md
@@ -16,46 +16,60 @@ These are the required dependencies to get started. Other dependencies are pulle
- [Node.js](https://nodejs.org/en/download/) Latest LTS (20.11.1 at time of writing). Many of us use [nvm](https://github.com/nvm-sh/nvm) for installing multiple versions of Node.
- [Go](https://go.dev/doc/install), (1.24 at time of writing)
-- [Make](https://www.gnu.org/software/make/) (GNU). Often installed by default. On Windows this can be installed with the "chocolatey" package manager that is installed with node.
- [Kubernetes](https://kubernetes.io/), we suggest [minikube](https://minikube.sigs.k8s.io/docs/) as one good K8s installation for testing locally. Other k8s installations are supported (see [platforms](../platforms.md).
## Build the code
Headlamp is composed of a `backend` and a `frontend`.
-You can build both the `backend` and `frontend` by running.
+You can build both the `backend` and `frontend` by running:
```bash
-make
+npm run build
```
Or individually:
```bash
-make backend
+npm run backend:build
```
and
```bash
-make frontend
+npm run frontend:build
```
## Run the code
-The quickest way to get the `backend` and `frontend` running for development is
-the following (respectively):
+The quickest way to get the `backend` and `frontend` running for development is to run both together:
```bash
-make run-backend
+npm start
+```
+
+Or you can run them individually in separate terminal instances:
+
+```bash
+npm run backend:start
```
and in a different terminal instance:
```bash
-make run-frontend
+npm run frontend:start
+```
+
+## Generate API documentation
+
+To generate the TypeScript API documentation:
+
+```bash
+npm run docs
```
+This generates API documentation in `docs/development/api/` using TypeDoc.
+
## Build the app
You can build the app for Linux, Windows, or Mac.
@@ -66,15 +80,15 @@ and the linux app on a linux box.
Choose the relevant command:
```bash
-make app-linux
+npm run app:package:linux
```
```bash
-make app-mac
+npm run app:package:mac
```
```bash
-make app-win
+npm run app:package:win
```
For Windows, by default it will produce an installer using [NSIS (Nullsoft Scriptable Install System)](https://sourceforge.net/projects/nsis/).
@@ -90,24 +104,24 @@ set PATH=%PATH%;C:\Program Files (x86)\WiX Toolset v3.11\bin
Then run the following command to generate the `.msi` installer:
```bash
-make app-win-msi
+npm run app:package:win:msi
```
See the generated app files in app/dist/ .
### Running the app
-If you already have **BOTH** the `backend` and `frontend` up and running, the quickest way to
+If you already have **BOTH** the `backend` and `frontend` up and running, the quickest way to
get the `app` running for development is the following:
```bash
-make run-only-app
+npm run app:start:client
```
or else you can simply do
```bash
-make run-app
+npm run start:app
```
which runs everything including the `backend`, `frontend` and `app` in parallel.
@@ -137,16 +151,16 @@ source. It will run the `frontend` from a `backend`'s static server, and
options can be appended to the main command as arguments.
```bash
-make image
+npm run image:build
```
### Custom container base images
The Dockerfile takes a build argument for the base image used. You can specify the
-base image used using the IMAGE_BASE environment variable with make.
+base image used using the IMAGE_BASE environment variable.
```bash
-IMAGE_BASE=debian:latest make image
+IMAGE_BASE=debian:latest npm run image:build
```
If no IMAGE_BASE is specified, then a default image is used (see Dockerfile for exact default image used).
@@ -171,7 +185,7 @@ If you want to make a new container image called `headlamp-k8s/headlamp:developm
you can run it like this:
```bash
-$ DOCKER_IMAGE_VERSION=development make image
+$ DOCKER_IMAGE_VERSION=development npm run image:build
...
Successfully tagged headlamp-k8s/headlamp:development
@@ -196,7 +210,7 @@ ones made in the local docker environment.
```bash
eval $(minikube docker-env)
-DOCKER_IMAGE_VERSION=development make image
+DOCKER_IMAGE_VERSION=development npm run image:build
```
#### Create a deployment yaml
diff --git a/docs/installation/base-url.md b/docs/installation/base-url.md
index be9fdf3b992..b84979a48f8 100644
--- a/docs/installation/base-url.md
+++ b/docs/installation/base-url.md
@@ -22,7 +22,7 @@ If in doubt, host Headlamp on a separate origin (domain or port, don't use the `
```bash
./backend/headlamp-server -dev -base-url /headlamp
-PUBLIC_URL="/headlamp" make run-frontend
+PUBLIC_URL="/headlamp" npm run frontend:start
```
Then go to in your browser.
@@ -30,7 +30,7 @@ Then go to in your browser.
### Static build mode
```bash
-cd frontend && npm install && npm run build && cd ..
+npm run frontend:build
./backend/headlamp-server -dev -base-url /headlamp -html-static-dir frontend/build
```
diff --git a/docs/installation/in-cluster/azure-entra-id/index.md b/docs/installation/in-cluster/azure-entra-id/index.md
index 6d4e8856974..bc5fd05343e 100644
--- a/docs/installation/in-cluster/azure-entra-id/index.md
+++ b/docs/installation/in-cluster/azure-entra-id/index.md
@@ -141,7 +141,7 @@ config:
clientID: ""
clientSecret: ""
issuerURL: "https://login.microsoftonline.com//v2.0"
- scopes: "6dae42f8-4368-4678-94ff-3960e28e3630/user.read openid email profile"
+ scopes: "6dae42f8-4368-4678-94ff-3960e28e3630/user.read,openid,email,profile"
validatorClientID: "6dae42f8-4368-4678-94ff-3960e28e3630"
validatorIssuerURL: "https://sts.windows.net//"
useAccessToken: true
@@ -149,6 +149,8 @@ config:
Replace ``,``, and ``, with your specific Azure Entra ID app registration details obtained in the steps above.
+> Note: The `scopes` string must be comma separated so each scope is passed individually to the OIDC provider.
+
4. Save the `values.yaml` file and Install Headlamp using helm with the following commands:
```shell
@@ -170,4 +172,4 @@ kubectl port-forward svc/headlamp-oidc 8000:80 -n headlamp
8. Open your web browser and go to . Click on "sign-in." After completing the login flow successfully, you'll gain access to your Kubernetes cluster using Headlamp.
-
\ No newline at end of file
+
diff --git a/docs/installation/in-cluster/oidc.md b/docs/installation/in-cluster/oidc.md
index ec1afde4dc4..d0eaba255da 100644
--- a/docs/installation/in-cluster/oidc.md
+++ b/docs/installation/in-cluster/oidc.md
@@ -96,7 +96,7 @@ For quick reference if you are already familiar with setting up Entra ID,
- Set `-oidc-client-id` to your Azure App Registration's clientID
- Set `-oidc-client-secret` to your Azure App Registration's clientSecret
- Set `-oidc-idp-issuer-url` to `https://login.microsoftonline.com//v2.0`
-- Set `-oidc-scopes` to `6dae42f8-4368-4678-94ff-3960e28e3630/user.read openid email profile`
+- Set `-oidc-scopes` to `6dae42f8-4368-4678-94ff-3960e28e3630/user.read,openid,email,profile`
- Set `--oidc-validator-idp-issuer-url` to `https://sts.windows.net//`
- Set `-oidc-validator-client-id` to `6dae42f8-4368-4678-94ff-3960e28e3630`
- Set `-oidc-use-access-token=true`
diff --git a/docs/learn/projects.md b/docs/learn/projects.md
new file mode 100644
index 00000000000..72321600814
--- /dev/null
+++ b/docs/learn/projects.md
@@ -0,0 +1,79 @@
+---
+title: Projects
+sidebar_position: 5
+---
+
+# Projects
+
+The Projects feature in Headlamp allows you to group and manage Kubernetes resources across multiple clusters. A project combines one or more clusters under a single namespace, providing a unified view and management interface. This is particularly useful for teams working on multi-cluster deployments, as it enables easier resource organization and access control.
+
+By default, Headlamp provides a cluster-centric view of your Kubernetes resources. However, with Projects, you can shift to a namespace-centric perspective that spans multiple clusters. This allows you to see all resources associated with a specific project in one place, regardless of which cluster they reside in.
+
+## Creating a Project
+
+### Option 1: Create a Project in the UI
+
+1. From the home dashboard, open **Projects**.
+2. Click **Create Project**.
+3. Fill in the required details:
+
+ - **Project Name** — A unique name for your project.
+ - **Cluster(s)** — Select one or more clusters to include.
+ - **Namespace** — Choose or create the namespace associated with this project.
+
+4. Click **Create** to finalize setup.
+
+Once created, the project appears in your list.
+Selecting it opens a detailed view that includes tabs for **Overview**, **Resources**, **Access**, and **Map**.
+
+---
+
+### Option 2: Create a Project from YAML
+
+You can also add resources to a project by importing a YAML configuration file. This associates existing clusters and resources into the project’s namespace.
+Resources defined in your YAML file are added to the **project’s namespace** automatically.
+
+1. In the **Create Project** dialog, select **From YAML**.
+2. Fill in the required details:
+
+ - **Project Name** — A unique name for your project.
+ - **Cluster(s)** — Select a cluster to add resources to.
+
+3. Choose one of the following options:
+ - **Upload File** – Import a YAML file from your computer.
+ - **Use URL** – Paste a link to a hosted YAML file.
+4. Click **Create** once the configuration loads.
+
+Once created, your projects will be listed in the "Projects" section.
+
+## Working with Projects
+
+After creating a project, you can explore it using the available tabs in the Project details view.
+
+### Overview Tab
+
+
+
+Provides a high-level summary of the project — similar to a namespace view, but extended across multiple clusters.
+Here you can see general information like labels, annotations, and linked clusters.
+
+### Resources Tab
+
+
+
+Lists most of the same resources you’d see in a cluster view, scoped to your project namespace.
+Some resource types may not appear (like Pods in certain cluster configurations).
+This tab aggregates resources from all clusters associated with the project.
+
+### Access Tab
+
+
+
+Displays and manages access controls for the project, similar to how you’d manage permissions within a namespace.
+
+### Map Tab
+
+
+
+Shows a visual map of the resources within your project.
+This view uses the same resource map as in cluster mode, but filters results to only display items belonging to your project namespace.
diff --git a/e2e-tests/README.md b/e2e-tests/README.md
index e1273ae33d4..6a6a1ec85ad 100644
--- a/e2e-tests/README.md
+++ b/e2e-tests/README.md
@@ -153,7 +153,7 @@ npx playwright test -g "404 page is present" --headed
- Inside `.github/workflows/build-container.yml` is the source line we need, locate the step for building the image and run in your terminal:
```
- DOCKER_IMAGE_VERSION=latest make image
+ DOCKER_IMAGE_VERSION=latest npm run image:build
```
2. **Tag and push the Docker image to a registry (e.g., ttl.sh):**
diff --git a/e2e-tests/package-lock.json b/e2e-tests/package-lock.json
index f1ca54ab23a..dbec72cb2f3 100644
--- a/e2e-tests/package-lock.json
+++ b/e2e-tests/package-lock.json
@@ -30,18 +30,18 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.42.1",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
- "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
+ "version": "1.56.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
+ "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true,
"dependencies": {
- "playwright": "1.42.1"
+ "playwright": "1.56.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
- "node": ">=16"
+ "node": ">=18"
}
},
"node_modules/@types/node": {
@@ -77,32 +77,32 @@
}
},
"node_modules/playwright": {
- "version": "1.42.1",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
- "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
+ "version": "1.56.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
+ "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true,
"dependencies": {
- "playwright-core": "1.42.1"
+ "playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
- "node": ">=16"
+ "node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
- "version": "1.42.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
- "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==",
+ "version": "1.56.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
+ "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
- "node": ">=16"
+ "node": ">=18"
}
},
"node_modules/undici-types": {
@@ -130,12 +130,12 @@
}
},
"@playwright/test": {
- "version": "1.42.1",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
- "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
+ "version": "1.56.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
+ "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true,
"requires": {
- "playwright": "1.42.1"
+ "playwright": "1.56.1"
}
},
"@types/node": {
@@ -160,19 +160,19 @@
"optional": true
},
"playwright": {
- "version": "1.42.1",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
- "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
+ "version": "1.56.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
+ "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
- "playwright-core": "1.42.1"
+ "playwright-core": "1.56.1"
}
},
"playwright-core": {
- "version": "1.42.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
- "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA=="
+ "version": "1.56.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
+ "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ=="
},
"undici-types": {
"version": "5.26.5",
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 4a6e7660c7d..cc0b67a2811 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -84,7 +84,7 @@
"simple-eval": "^2.0.0",
"spacetime": "^7.4.0",
"typescript": "5.6.2",
- "vite": "^6.3.6",
+ "vite": "^6.4.1",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-static-copy": "^3.1.2",
"vite-plugin-svgr": "^4.3.0"
@@ -15400,9 +15400,9 @@
"dev": true
},
"node_modules/vite": {
- "version": "6.3.6",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
- "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
diff --git a/frontend/package.json b/frontend/package.json
index 4581660e9f5..4b17cad2680 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -85,7 +85,7 @@
"simple-eval": "^2.0.0",
"spacetime": "^7.4.0",
"typescript": "5.6.2",
- "vite": "^6.3.6",
+ "vite": "^6.4.1",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-static-copy": "^3.1.2",
"vite-plugin-svgr": "^4.3.0"
diff --git a/frontend/src/components/App/AppContainer.tsx b/frontend/src/components/App/AppContainer.tsx
index 914eadd783c..05c26054aaf 100644
--- a/frontend/src/components/App/AppContainer.tsx
+++ b/frontend/src/components/App/AppContainer.tsx
@@ -22,6 +22,8 @@ import { getBaseUrl } from '../../helpers/getBaseUrl';
import { setBackendToken } from '../../helpers/getHeadlampAPIHeaders';
import { isElectron } from '../../helpers/isElectron';
import Plugins from '../../plugin/Plugins';
+import store from '../../redux/stores/store';
+import { uiSlice } from '../../redux/uiSlice';
import ReleaseNotes from '../common/ReleaseNotes/ReleaseNotes';
import { MonacoEditorLoaderInitializer } from '../monaco/MonacoEditorLoaderInitializer';
import Layout from './Layout';
@@ -32,6 +34,11 @@ window.desktopApi?.receive('backend-token', (token: string) => {
setBackendToken(token);
});
+// Listen for the open-about-dialog event from the Electron app menu
+window.desktopApi?.receive('open-about-dialog', () => {
+ store.dispatch(uiSlice.actions.setVersionDialogOpen(true));
+});
+
/**
* Validates if a redirect path is safe to use
* @param redirectPath - The path to validate
diff --git a/frontend/src/components/App/Home/ClusterTable.tsx b/frontend/src/components/App/Home/ClusterTable.tsx
index 20ed93d73a1..52e7436a7b4 100644
--- a/frontend/src/components/App/Home/ClusterTable.tsx
+++ b/frontend/src/components/App/Home/ClusterTable.tsx
@@ -22,10 +22,12 @@ import Typography from '@mui/material/Typography';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath, useHistory } from 'react-router-dom';
+import { isElectron } from '../../../helpers/isElectron';
import { formatClusterPathParam } from '../../../lib/cluster';
import { useClustersConf, useClustersVersion } from '../../../lib/k8s';
import { ApiError } from '../../../lib/k8s/api/v2/ApiError';
import { Cluster } from '../../../lib/k8s/cluster';
+import { createRouteURL } from '../../../lib/router/createRouteURL';
import { getClusterPrefixedPath } from '../../../lib/util';
import { useTypedSelector } from '../../../redux/hooks';
import { Loader } from '../../common';
@@ -145,6 +147,42 @@ export default function ClusterTable({
return ;
}
+ const clustersList = Object.values(customNameClusters);
+ if (clustersList.length === 0) {
+ return (
+
+
+
+ {t('No clusters found')}
+
+
+ {t('Add a cluster to get started.')}
+
+ {isElectron() && (
+ }
+ onClick={() => {
+ history.push(createRouteURL('addCluster'));
+ }}
+ >
+ {t('Add Cluster')}
+
+ )}
+
+ );
+ }
+
return (
{
diff --git a/frontend/src/components/App/Layout.stories.tsx b/frontend/src/components/App/Layout.stories.tsx
new file mode 100644
index 00000000000..c453eacb22a
--- /dev/null
+++ b/frontend/src/components/App/Layout.stories.tsx
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2025 The Kubernetes Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Meta, StoryFn } from '@storybook/react';
+import { delay, http, HttpResponse } from 'msw';
+import { Provider } from 'react-redux';
+import { MemoryRouter } from 'react-router-dom';
+import store from '../../redux/stores/store';
+import { TestContext } from '../../test';
+import Layout from './Layout';
+
+export default {
+ title: 'App/Layout',
+ component: Layout,
+ parameters: {
+ layout: 'fullscreen',
+ docs: {
+ description: {
+ component:
+ 'The main layout component for Headlamp. It includes the top bar, sidebar, main content area, and handles cluster configuration and routing. This is the primary layout wrapper for the application.',
+ },
+ },
+ msw: {
+ handlers: [
+ // Mock cluster config
+ http.get('http://localhost:4466/config', () =>
+ HttpResponse.json({
+ clusters: {
+ minikube: {
+ name: 'minikube',
+ meta_data: { namespace: 'default' },
+ },
+ production: {
+ name: 'production',
+ meta_data: { namespace: 'default' },
+ },
+ },
+ })
+ ),
+ // Mock plugins
+ http.get('http://localhost:4466/plugins', () => HttpResponse.json([])),
+ // Mock cluster version
+ http.get('http://localhost:4466/version', () =>
+ HttpResponse.json({
+ major: '1',
+ minor: '28',
+ gitVersion: 'v1.28.0',
+ })
+ ),
+ // Mock events
+ http.get('http://localhost:4466/*/api/v1/events', () =>
+ HttpResponse.json({
+ kind: 'EventList',
+ items: [],
+ })
+ ),
+ // Mock namespaces
+ http.get('http://localhost:4466/*/api/v1/namespaces', () =>
+ HttpResponse.json({
+ kind: 'NamespaceList',
+ items: [
+ {
+ metadata: { name: 'default', uid: '1' },
+ spec: {},
+ status: { phase: 'Active' },
+ },
+ ],
+ })
+ ),
+ // Mock CRDs
+ http.get(
+ 'http://localhost:4466/apis/apiextensions.k8s.io/v1/customresourcedefinitions',
+ () =>
+ HttpResponse.json({
+ kind: 'List',
+ items: [],
+ metadata: {},
+ })
+ ),
+ http.get(
+ 'http://localhost:4466/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions',
+ () =>
+ HttpResponse.json({
+ kind: 'List',
+ items: [],
+ metadata: {},
+ })
+ ),
+ ],
+ },
+ },
+ decorators: [
+ Story => (
+
+
+
+
+
+
+
+ ),
+ ],
+} as Meta;
+
+const Template: StoryFn = () => ;
+
+export const Default = Template.bind({});
+Default.parameters = {
+ docs: {
+ description: {
+ story: 'The default layout with sidebar, topbar, and main content area.',
+ },
+ },
+};
+
+export const WithClusterRoute = Template.bind({});
+WithClusterRoute.decorators = [
+ Story => (
+
+
+
+
+
+
+
+ ),
+];
+WithClusterRoute.parameters = {
+ docs: {
+ description: {
+ story: 'Layout when viewing a specific cluster route (e.g., /c/minikube/pods).',
+ },
+ },
+};
+
+export const LoadingState = Template.bind({});
+LoadingState.parameters = {
+ docs: {
+ description: {
+ story: 'Layout showing loading state while fetching cluster configuration.',
+ },
+ },
+ storyshots: {
+ disable: true,
+ },
+ msw: {
+ handlers: [
+ // Delay config response to show loading for 5 seconds
+ http.get('http://localhost:4466/config', async () => {
+ await delay(5000);
+ return HttpResponse.json({
+ clusters: {
+ minikube: {
+ name: 'minikube',
+ meta_data: { namespace: 'default' },
+ },
+ },
+ });
+ }),
+ http.get('http://localhost:4466/plugins', () => HttpResponse.json([])),
+ http.get('http://localhost:4466/apis/apiextensions.k8s.io/v1/customresourcedefinitions', () =>
+ HttpResponse.json({
+ kind: 'List',
+ items: [],
+ metadata: {},
+ })
+ ),
+ http.get(
+ 'http://localhost:4466/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions',
+ () =>
+ HttpResponse.json({
+ kind: 'List',
+ items: [],
+ metadata: {},
+ })
+ ),
+ ],
+ },
+};
+
+export const ErrorState = Template.bind({});
+ErrorState.parameters = {
+ docs: {
+ description: {
+ story: 'Layout showing error state when cluster configuration fails to load.',
+ },
+ },
+ msw: {
+ handlers: [
+ http.get('http://localhost:4466/config', () => HttpResponse.error()),
+ http.get('http://localhost:4466/plugins', () => HttpResponse.json([])),
+ http.get('http://localhost:4466/apis/apiextensions.k8s.io/v1/customresourcedefinitions', () =>
+ HttpResponse.json({
+ kind: 'List',
+ items: [],
+ metadata: {},
+ })
+ ),
+ http.get(
+ 'http://localhost:4466/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions',
+ () =>
+ HttpResponse.json({
+ kind: 'List',
+ items: [],
+ metadata: {},
+ })
+ ),
+ ],
+ },
+};
+
+export const MultiCluster = Template.bind({});
+MultiCluster.decorators = [
+ Story => (
+
+
+
+
+
+
+
+ ),
+];
+MultiCluster.parameters = {
+ docs: {
+ description: {
+ story: 'Layout when viewing multiple clusters simultaneously.',
+ },
+ },
+ msw: {
+ handlers: [
+ http.get('http://localhost:4466/config', () =>
+ HttpResponse.json({
+ clusters: {
+ minikube: {
+ name: 'minikube',
+ meta_data: { namespace: 'default' },
+ },
+ production: {
+ name: 'production',
+ meta_data: { namespace: 'default' },
+ },
+ staging: {
+ name: 'staging',
+ meta_data: { namespace: 'default' },
+ },
+ },
+ })
+ ),
+ http.get('http://localhost:4466/plugins', () => HttpResponse.json([])),
+ http.get('http://localhost:4466/apis/apiextensions.k8s.io/v1/customresourcedefinitions', () =>
+ HttpResponse.json({
+ kind: 'List',
+ items: [],
+ metadata: {},
+ })
+ ),
+ http.get(
+ 'http://localhost:4466/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions',
+ () =>
+ HttpResponse.json({
+ kind: 'List',
+ items: [],
+ metadata: {},
+ })
+ ),
+ ],
+ },
+};
diff --git a/frontend/src/components/App/Settings/ClusterNameEditor.stories.tsx b/frontend/src/components/App/Settings/ClusterNameEditor.stories.tsx
new file mode 100644
index 00000000000..3befad451c6
--- /dev/null
+++ b/frontend/src/components/App/Settings/ClusterNameEditor.stories.tsx
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2025 The Kubernetes Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { configureStore } from '@reduxjs/toolkit';
+import { Meta, StoryFn } from '@storybook/react';
+import { Provider } from 'react-redux';
+import { ClusterNameEditor } from './ClusterNameEditor';
+
+const getMockState = () => ({
+ plugins: { loaded: true },
+ theme: {
+ name: 'light',
+ logo: null,
+ palette: {
+ navbar: {
+ background: '#fff',
+ },
+ },
+ },
+});
+
+const meta: Meta = {
+ title: 'Settings/ClusterNameEditor',
+ component: ClusterNameEditor,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+const Template: StoryFn = args => {
+ const store = configureStore({
+ reducer: (state = getMockState()) => state,
+ preloadedState: getMockState(),
+ });
+
+ return (
+
+
+
+ );
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ cluster: 'my-cluster',
+ clusterSettings: null,
+ setClusterSettings: () => {},
+};
+
+export const WithInvalidName = Template.bind({});
+WithInvalidName.args = {
+ ...Default.args,
+ clusterSettings: {
+ currentName: 'Invalid Cluster Name',
+ },
+};
+
+export const WithNewName = Template.bind({});
+WithNewName.args = {
+ ...Default.args,
+ clusterSettings: {
+ currentName: 'new-cluster-name',
+ },
+};
diff --git a/frontend/src/components/App/Settings/ClusterNameEditor.tsx b/frontend/src/components/App/Settings/ClusterNameEditor.tsx
new file mode 100644
index 00000000000..45302bf9160
--- /dev/null
+++ b/frontend/src/components/App/Settings/ClusterNameEditor.tsx
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2025 The Kubernetes Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Box, TextField, Typography } from '@mui/material';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
+import { useHistory } from 'react-router-dom';
+import { ClusterSettings } from '../../../helpers/clusterSettings';
+import { parseKubeConfig, renameCluster } from '../../../lib/k8s/api/v1/clusterApi';
+import { Cluster } from '../../../lib/k8s/cluster';
+import { setConfig, setStatelessConfig } from '../../../redux/configSlice';
+import { findKubeconfigByClusterName } from '../../../stateless/findKubeconfigByClusterName';
+import { updateStatelessClusterKubeconfig } from '../../../stateless/updateStatelessClusterKubeconfig';
+import { ConfirmButton, ConfirmDialog, NameValueTable } from '../../common';
+import { isValidClusterNameFormat } from './util';
+
+interface ClusterNameEditorProps {
+ cluster: string;
+ clusterConf: {
+ [clusterName: string]: Cluster;
+ } | null;
+ clusterSettings: ClusterSettings | null;
+ setClusterSettings: React.Dispatch>;
+}
+
+export function ClusterNameEditor({
+ cluster,
+ clusterConf,
+ clusterSettings,
+ setClusterSettings,
+}: ClusterNameEditorProps) {
+ const { t } = useTranslation(['translation']);
+ const [customNameInUse, setCustomNameInUse] = React.useState(false);
+ const [clusterErrorDialogOpen, setClusterErrorDialogOpen] = React.useState(false);
+ const [clusterErrorDialogMessage, setClusterErrorDialogMessage] = React.useState('');
+ const [newClusterName, setNewClusterName] = React.useState(cluster || '');
+
+ const dispatch = useDispatch();
+ const history = useHistory();
+
+ React.useEffect(() => {
+ if (clusterSettings?.currentName !== cluster) {
+ setNewClusterName(clusterSettings?.currentName || '');
+ }
+ }, [cluster, clusterSettings]);
+
+ const clusterInfo = (clusterConf && clusterConf[cluster || '']) || null;
+ const source = clusterInfo?.meta_data?.source;
+ const originalName = clusterInfo?.meta_data?.originalName;
+ const displayName = originalName || (clusterInfo ? clusterInfo.name : '');
+
+ /** Note: display original name is currently only supported for non dynamic clusters from kubeconfig sources. */
+ const clusterID = clusterInfo?.meta_data?.clusterID || '';
+
+ const invalidClusterNameMessage = t(
+ "translation|Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character."
+ );
+
+ /**
+ * This function is part of a double check, this is meant to check all the cluster names currently in use as display names
+ * Note: if the metadata is not available or does not load, another check is done in the backend to ensure the name is unique in its own config
+ *
+ * @param name The name to check.
+ * @returns bool of if the name is in use.
+ */
+ function checkNameInUse(name: string) {
+ if (!clusterConf) {
+ return false;
+ }
+ /** These are the display names of the clusters, renamed clusters have their display name as the custom name */
+ const clusterNames = Object.values(clusterConf).map(cluster => cluster.name);
+
+ /** The original name of the cluster is the name used in the kubeconfig file. */
+ const originalNames = Object.values(clusterConf)
+ .map(cluster => cluster.meta_data?.originalName)
+ .filter(originalName => originalName !== undefined);
+
+ const allNames = [...clusterNames, ...originalNames];
+
+ const nameInUse = allNames.includes(name);
+
+ setCustomNameInUse(nameInUse);
+ }
+
+ function ClusterErrorDialog() {
+ return (
+ {
+ setClusterErrorDialogOpen(false);
+ }}
+ handleClose={() => {
+ setClusterErrorDialogOpen(false);
+ }}
+ hideCancelButton
+ open={clusterErrorDialogOpen}
+ title={t('translation|Error')}
+ description={clusterErrorDialogMessage}
+ confirmLabel={t('translation|Okay')}
+ >
+ );
+ }
+ // Display the original name of the cluster if it was loaded from a kubeconfig file.
+ function ClusterName() {
+ const currentName = clusterInfo?.name;
+ const originalName = clusterInfo?.meta_data?.originalName;
+ const source = clusterInfo?.meta_data?.source;
+ // Note: display original name is currently only supported for non dynamic clusters from kubeconfig sources.
+ const displayOriginalName = source === 'kubeconfig' && originalName;
+
+ return (
+ <>
+ {clusterErrorDialogOpen && }
+ {t('translation|Name')}
+ {displayOriginalName && currentName !== displayOriginalName && (
+
+ {t('translation|Original name: {{ displayName }}', {
+ displayName: displayName,
+ })}
+
+ )}
+ >
+ );
+ }
+
+ function storeNewClusterName(name: string) {
+ let actualName = name;
+ if (name === cluster) {
+ actualName = '';
+ setNewClusterName(actualName);
+ }
+
+ setClusterSettings((settings: ClusterSettings | null) => {
+ const newSettings = { ...(settings || {}) };
+ if (isValidClusterNameFormat(name)) {
+ newSettings.currentName = actualName;
+ }
+ return newSettings;
+ });
+ }
+
+ const handleUpdateClusterName = (source: string) => {
+ try {
+ renameCluster(cluster || '', newClusterName, source, clusterID)
+ .then(async config => {
+ if (cluster) {
+ const kubeconfig = await findKubeconfigByClusterName(cluster, clusterID);
+ if (kubeconfig !== null) {
+ await updateStatelessClusterKubeconfig(kubeconfig, newClusterName, cluster);
+ // Make another request for updated kubeconfig
+ const updatedKubeconfig = await findKubeconfigByClusterName(cluster, clusterID);
+ if (updatedKubeconfig !== null) {
+ parseKubeConfig({ kubeconfig: updatedKubeconfig })
+ .then((config: any) => {
+ storeNewClusterName(newClusterName);
+ dispatch(setStatelessConfig(config));
+ })
+ .catch((err: Error) => {
+ console.error('Error updating cluster name:', err.message);
+ });
+ }
+ } else {
+ dispatch(setConfig(config));
+ }
+ }
+ history.push('/');
+ window.location.reload();
+ })
+ .catch((err: Error) => {
+ console.error('Error updating cluster name:', err.message);
+ setClusterErrorDialogMessage(err.message);
+ setClusterErrorDialogOpen(true);
+ });
+ } catch (error) {
+ console.error('Error updating cluster name:', error);
+ }
+ };
+ const isValidCurrentName = isValidClusterNameFormat(newClusterName);
+
+ return (
+ ,
+ value: (
+ {
+ let value = event.target.value;
+ value = value.replace(' ', '');
+ setNewClusterName(value);
+ checkNameInUse(value);
+ }}
+ value={newClusterName}
+ placeholder={cluster}
+ error={!isValidCurrentName || customNameInUse}
+ helperText={
+
+ {!isValidCurrentName && invalidClusterNameMessage}
+ {customNameInUse &&
+ t(
+ 'translation|This custom name is already in use, please choose a different name.'
+ )}
+ {isValidCurrentName &&
+ !customNameInUse &&
+ t('translation|The current name of the cluster. You can define a custom name')}
+
+ }
+ InputProps={{
+ endAdornment: (
+
+ {
+ if (isValidCurrentName) {
+ handleUpdateClusterName(source);
+ }
+ }}
+ confirmTitle={t('translation|Change name')}
+ confirmDescription={t(
+ 'translation|Are you sure you want to change the name for "{{ clusterName }}"?',
+ { clusterName: displayName }
+ )}
+ disabled={!newClusterName || !isValidCurrentName || customNameInUse}
+ >
+ {t('translation|Apply')}
+
+
+ ),
+ onKeyPress: event => {
+ if (event.key === 'Enter' && isValidCurrentName) {
+ handleUpdateClusterName(source);
+ }
+ },
+ autoComplete: 'off',
+ sx: { maxWidth: 250 },
+ }}
+ />
+ ),
+ },
+ ]}
+ />
+ );
+}
diff --git a/frontend/src/components/App/Settings/DrawerModeSettings.tsx b/frontend/src/components/App/Settings/DrawerModeSettings.tsx
index df7195fb048..459c909843a 100644
--- a/frontend/src/components/App/Settings/DrawerModeSettings.tsx
+++ b/frontend/src/components/App/Settings/DrawerModeSettings.tsx
@@ -143,7 +143,7 @@ const OptionButton = ({
export default function DrawerModeSettings() {
const dispatch = useDispatch();
- const isDrawerEnabled = useTypedSelector(state => state.drawerMode.isDetailDrawerEnabled);
+ const isDrawerEnabled = useTypedSelector(state => state?.drawerMode?.isDetailDrawerEnabled);
return (
diff --git a/frontend/src/components/App/Settings/SettingsCluster.tsx b/frontend/src/components/App/Settings/SettingsCluster.tsx
index 207c7611d6a..538bb8e5747 100644
--- a/frontend/src/components/App/Settings/SettingsCluster.tsx
+++ b/frontend/src/components/App/Settings/SettingsCluster.tsx
@@ -32,33 +32,19 @@ import {
} from '../../../helpers/clusterSettings';
import { isElectron } from '../../../helpers/isElectron';
import { useCluster, useClustersConf } from '../../../lib/k8s';
-import { deleteCluster, parseKubeConfig, renameCluster } from '../../../lib/k8s/api/v1/clusterApi';
-import { setConfig, setStatelessConfig } from '../../../redux/configSlice';
-import { findKubeconfigByClusterName } from '../../../stateless/findKubeconfigByClusterName';
-import { updateStatelessClusterKubeconfig } from '../../../stateless/updateStatelessClusterKubeconfig';
+import { deleteCluster } from '../../../lib/k8s/api/v1/clusterApi';
+import { setConfig } from '../../../redux/configSlice';
import ConfirmButton from '../../common/ConfirmButton';
-import ConfirmDialog from '../../common/ConfirmDialog';
import Empty from '../../common/EmptyContent';
import Link from '../../common/Link';
import Loader from '../../common/Loader';
import NameValueTable from '../../common/NameValueTable';
import SectionBox from '../../common/SectionBox';
+import { ClusterNameEditor } from './ClusterNameEditor';
import ClusterSelector from './ClusterSelector';
import NodeShellSettings from './NodeShellSettings';
import { isValidNamespaceFormat } from './util';
-function isValidClusterNameFormat(name: string) {
- // We allow empty isValidClusterNameFormat just because that's the default value in our case.
- if (!name) {
- return true;
- }
-
- // Validates that the namespace is a valid DNS-1123 label and returns a boolean.
- // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
- const regex = new RegExp('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$');
- return regex.test(name);
-}
-
export default function SettingsCluster() {
const clusterConf = useClustersConf();
const clusters = Object.values(clusterConf || {}).map(cluster => cluster.name);
@@ -69,10 +55,6 @@ export default function SettingsCluster() {
const [clusterSettings, setClusterSettings] = React.useState(null);
const [cluster, setCluster] = React.useState(useCluster() || '');
const clusterFromURLRef = React.useRef('');
- const [newClusterName, setNewClusterName] = React.useState(cluster || '');
- const [clusterErrorDialogOpen, setClusterErrorDialogOpen] = React.useState(false);
- const [clusterErrorDialogMessage, setClusterErrorDialogMessage] = React.useState('');
- const [customNameInUse, setCustomNameInUse] = React.useState(false);
const theme = useTheme();
@@ -80,77 +62,6 @@ export default function SettingsCluster() {
const dispatch = useDispatch();
const location = useLocation();
- const clusterInfo = (clusterConf && clusterConf[cluster || '']) || null;
- const originalName = clusterInfo?.meta_data?.originalName;
- const displayName = originalName || (clusterInfo ? clusterInfo.name : '');
- const source = clusterInfo?.meta_data?.source;
- /** Note: display original name is currently only supported for non dynamic clusters from kubeconfig sources. */
- const clusterID = clusterInfo?.meta_data?.clusterID || '';
-
- /**
- * This function is part of a double check, this is meant to check all the cluster names currently in use as display names
- * Note: if the metadata is not available or does not load, another check is done in the backend to ensure the name is unique in its own config
- *
- * @param name The name to check.
- * @returns bool of if the name is in use.
- */
- function checkNameInUse(name: string) {
- if (!clusterConf) {
- return false;
- }
-
- /** These are the display names of the clusters, renamed clusters have their display name as the custom name */
- const clusterNames = Object.values(clusterConf).map(cluster => cluster.name);
-
- /** The original name of the cluster is the name used in the kubeconfig file. */
- const originalNames = Object.values(clusterConf)
- .map(cluster => cluster.meta_data?.originalName)
- .filter(originalName => originalName !== undefined);
-
- const allNames = [...clusterNames, ...originalNames];
-
- const nameInUse = allNames.includes(name);
-
- setCustomNameInUse(nameInUse);
- }
-
- const handleUpdateClusterName = (source: string) => {
- try {
- renameCluster(cluster || '', newClusterName, source, clusterID)
- .then(async config => {
- if (cluster) {
- const kubeconfig = await findKubeconfigByClusterName(cluster, clusterID);
- if (kubeconfig !== null) {
- await updateStatelessClusterKubeconfig(kubeconfig, newClusterName, cluster);
- // Make another request for updated kubeconfig
- const updatedKubeconfig = await findKubeconfigByClusterName(cluster, clusterID);
- if (updatedKubeconfig !== null) {
- parseKubeConfig({ kubeconfig: updatedKubeconfig })
- .then((config: any) => {
- storeNewClusterName(newClusterName);
- dispatch(setStatelessConfig(config));
- })
- .catch((err: Error) => {
- console.error('Error updating cluster name:', err.message);
- });
- }
- } else {
- dispatch(setConfig(config));
- }
- }
- history.push('/');
- window.location.reload();
- })
- .catch((err: Error) => {
- console.error('Error updating cluster name:', err.message);
- setClusterErrorDialogMessage(err.message);
- setClusterErrorDialogOpen(true);
- });
- } catch (error) {
- console.error('Error updating cluster name:', error);
- }
- };
-
const removeCluster = () => {
deleteCluster(cluster || '')
.then(config => {
@@ -191,10 +102,6 @@ export default function SettingsCluster() {
setUserDefaultNamespace(clusterSettings?.defaultNamespace || '');
}
- if (clusterSettings?.currentName !== cluster) {
- setNewClusterName(clusterSettings?.currentName || '');
- }
-
// Avoid re-initializing settings as {} just because the cluster is not yet set.
if (clusterSettings !== null) {
storeClusterSettings(cluster || '', clusterSettings);
@@ -266,33 +173,12 @@ export default function SettingsCluster() {
});
}
- function storeNewClusterName(name: string) {
- let actualName = name;
- if (name === cluster) {
- actualName = '';
- setNewClusterName(actualName);
- }
-
- setClusterSettings((settings: ClusterSettings | null) => {
- const newSettings = { ...(settings || {}) };
- if (isValidClusterNameFormat(name)) {
- newSettings.currentName = actualName;
- }
- return newSettings;
- });
- }
-
const isValidDefaultNamespace = isValidNamespaceFormat(userDefaultNamespace);
- const isValidCurrentName = isValidClusterNameFormat(newClusterName);
const isValidNewAllowedNamespace = isValidNamespaceFormat(newAllowedNamespace);
const invalidNamespaceMessage = t(
"translation|Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character."
);
- const invalidClusterNameMessage = t(
- "translation|Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character."
- );
-
// If we don't have yet a cluster name from the URL, we are still loading.
if (!clusterFromURLRef.current) {
return ;
@@ -331,47 +217,6 @@ export default function SettingsCluster() {
);
}
- function ClusterErrorDialog() {
- return (
- {
- setClusterErrorDialogOpen(false);
- }}
- handleClose={() => {
- setClusterErrorDialogOpen(false);
- }}
- hideCancelButton
- open={clusterErrorDialogOpen}
- title={t('translation|Error')}
- description={clusterErrorDialogMessage}
- confirmLabel={t('translation|Okay')}
- >
- );
- }
-
- // Display the original name of the cluster if it was loaded from a kubeconfig file.
- function ClusterName() {
- const currentName = clusterInfo?.name;
- const originalName = clusterInfo?.meta_data?.originalName;
- const source = clusterInfo?.meta_data?.source;
- // Note: display original name is currently only supported for non dynamic clusters from kubeconfig sources.
- const displayOriginalName = source === 'kubeconfig' && originalName;
-
- return (
- <>
- {clusterErrorDialogOpen && }
- {t('translation|Name')}
- {displayOriginalName && currentName !== displayOriginalName && (
-
- {t('translation|Original name: {{ displayName }}', {
- displayName: displayName,
- })}
-
- )}
- >
- );
- }
-
const defaultNamespaceLabelID = 'default-namespace-label';
const allowedNamespaceLabelID = 'allowed-namespace-label';
@@ -389,67 +234,11 @@ export default function SettingsCluster() {
{isElectron() && (
- ,
- value: (
- {
- let value = event.target.value;
- value = value.replace(' ', '');
- setNewClusterName(value);
- checkNameInUse(value);
- }}
- value={newClusterName}
- placeholder={cluster}
- error={!isValidCurrentName || customNameInUse}
- helperText={
-
- {!isValidCurrentName && invalidClusterNameMessage}
- {customNameInUse &&
- t(
- 'translation|This custom name is already in use, please choose a different name.'
- )}
- {isValidCurrentName &&
- !customNameInUse &&
- t(
- 'translation|The current name of the cluster. You can define a custom name.'
- )}
-
- }
- InputProps={{
- endAdornment: (
-
- {
- if (isValidCurrentName) {
- handleUpdateClusterName(source);
- }
- }}
- confirmTitle={t('translation|Change name')}
- confirmDescription={t(
- 'translation|Are you sure you want to change the name for "{{ clusterName }}"?',
- { clusterName: displayName }
- )}
- disabled={!newClusterName || !isValidCurrentName || customNameInUse}
- >
- {t('translation|Apply')}
-
-
- ),
- onKeyPress: event => {
- if (event.key === 'Enter' && isValidCurrentName) {
- handleUpdateClusterName(source);
- }
- },
- autoComplete: 'off',
- sx: { maxWidth: 250 },
- }}
- />
- ),
- },
- ]}
+
)}
+
+
+ -
+
+ Name
+
+
+ -
+
+
+
+
+ The current name of the cluster. You can define a custom name
+
+
+
+
+
+
+
+
+
+ -
+
+ Name
+
+
+ -
+
+
+
+
+ Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.
+
+
+
+
+
+
+
+
+
+ -
+
+ Name
+
+
+ -
+
+
+
+
+ The current name of the cluster. You can define a custom name
+
+
+
+
+
+
+