From d3356038f8e2ad664398ae1c4a69a2f1e5f9ded9 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 12 May 2026 20:43:34 +0300 Subject: [PATCH 1/4] chore(console): add vitest + testing-library setup Adds vitest with jsdom and React Testing Library to apps/console so that schema-form widgets and helpers can be unit-tested. No tests yet; the infra is wired up first to keep follow-up commits focused on behaviour. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .github/workflows/test.yaml | 32 ++ apps/console/package.json | 10 +- apps/console/test/setup.ts | 10 + apps/console/tsconfig.json | 2 +- apps/console/vitest.config.ts | 17 + package.json | 1 + pnpm-lock.yaml | 786 ++++++++++++++++++++++++++++++++++ 7 files changed, 856 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test.yaml create mode 100644 apps/console/test/setup.ts create mode 100644 apps/console/vitest.config.ts diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..c12f2f4 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,32 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm typecheck + + - name: Test + run: pnpm test diff --git a/apps/console/package.json b/apps/console/package.json index 5fcb46d..77fcd74 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -7,6 +7,8 @@ "dev": "vite", "build": "tsc --noEmit && vite build", "lint": "eslint .", + "test": "vitest run", + "test:watch": "vitest", "typecheck": "tsc --noEmit", "preview": "vite preview" }, @@ -29,12 +31,18 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.2.2", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/js-yaml": "^4.0.9", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "@vitest/ui": "^4.1.6", + "jsdom": "^29.1.1", "tailwindcss": "^4.2.2", "typescript": "~6.0.2", - "vite": "^8.0.4" + "vite": "^8.0.4", + "vitest": "^4.1.6" } } diff --git a/apps/console/test/setup.ts b/apps/console/test/setup.ts new file mode 100644 index 0000000..8d02416 --- /dev/null +++ b/apps/console/test/setup.ts @@ -0,0 +1,10 @@ +import "@testing-library/jest-dom/vitest" +import { afterEach } from "vitest" +import { cleanup } from "@testing-library/react" + +// React Testing Library auto-registers cleanup via a global `afterEach` +// hook; that hook only exists when vitest is run with `globals: true`. +// We use explicit imports in tests, so wire cleanup up manually here. +afterEach(() => { + cleanup() +}) diff --git a/apps/console/tsconfig.json b/apps/console/tsconfig.json index bf7ac6d..82e0a3d 100644 --- a/apps/console/tsconfig.json +++ b/apps/console/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", - "types": ["vite/client"], + "types": ["vite/client", "@testing-library/jest-dom"], "noEmit": true, "baseUrl": ".", "paths": { diff --git a/apps/console/vitest.config.ts b/apps/console/vitest.config.ts new file mode 100644 index 0000000..deaa099 --- /dev/null +++ b/apps/console/vitest.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "vitest/config" +import react from "@vitejs/plugin-react" +import path from "path" + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@/": path.resolve(__dirname, "./src/"), + }, + }, + test: { + environment: "jsdom", + setupFiles: ["./test/setup.ts"], + include: ["src/**/*.{test,spec}.{ts,tsx}"], + }, +}) diff --git a/package.json b/package.json index f9b6ab8..56a5c38 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "pnpm -r build", "build:console": "pnpm --filter @cozystack/console build", "lint": "pnpm -r lint", + "test": "pnpm -r --if-present test", "typecheck": "pnpm -r typecheck" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a779cd4..8014574 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,15 @@ importers: '@tailwindcss/vite': specifier: ^4.2.2 version: 4.2.2(vite@8.0.8(@types/node@24.12.2)(jiti@2.6.1)) + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -96,6 +105,12 @@ importers: '@vitejs/plugin-react': specifier: ^6.0.1 version: 6.0.1(vite@8.0.8(@types/node@24.12.2)(jiti@2.6.1)) + '@vitest/ui': + specifier: ^4.1.6 + version: 4.1.6(vitest@4.1.6) + jsdom: + specifier: ^29.1.1 + version: 29.1.1 tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -105,6 +120,9 @@ importers: vite: specifier: ^8.0.4 version: 8.0.8(@types/node@24.12.2)(jiti@2.6.1) + vitest: + specifier: ^4.1.6 + version: 4.1.6(@types/node@24.12.2)(@vitest/ui@4.1.6)(jsdom@29.1.1)(vite@8.0.8(@types/node@24.12.2)(jiti@2.6.1)) packages/k8s-client: dependencies: @@ -177,6 +195,24 @@ importers: packages: + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -298,6 +334,46 @@ packages: '@types/react': optional: true + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} @@ -345,6 +421,15 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -421,6 +506,9 @@ packages: '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@rjsf/core@5.24.13': resolution: {integrity: sha512-ONTr14s7LFIjx2VRFLuOpagL76sM/HPy6/OhdBfq6UukINmTIs6+aFN0GgcR0aXQHFDXQ7f/fel0o/SO05Htdg==} engines: {node: '>=14'} @@ -541,6 +629,9 @@ packages: '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tailwindcss/node@4.2.2': resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} @@ -643,9 +734,47 @@ packages: peerDependencies: react: ^18 || ^19 + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -741,6 +870,40 @@ packages: babel-plugin-react-compiler: optional: true + '@vitest/expect@4.1.6': + resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} + + '@vitest/mocker@4.1.6': + resolution: {integrity: sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.6': + resolution: {integrity: sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==} + + '@vitest/runner@4.1.6': + resolution: {integrity: sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==} + + '@vitest/snapshot@4.1.6': + resolution: {integrity: sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==} + + '@vitest/spy@4.1.6': + resolution: {integrity: sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==} + + '@vitest/ui@4.1.6': + resolution: {integrity: sha512-wiu5em68DfGv/2HFvI1Njr7JI2CHcBlQvereSzVG8my53PRxjTNOCsD9VOkRKrsJBDHmyuXvosxWZw7T91a2mw==} + peerDependencies: + vitest: 4.1.6 + + '@vitest/utils@4.1.6': + resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -765,13 +928,32 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -784,6 +966,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + brace-expansion@1.1.14: resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} @@ -803,6 +988,10 @@ packages: caniuse-lite@1.0.30001788: resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -841,9 +1030,20 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -853,13 +1053,26 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dompurify@3.2.7: resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} @@ -870,6 +1083,13 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -931,10 +1151,17 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -956,6 +1183,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1005,6 +1235,10 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1021,6 +1255,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1029,6 +1267,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1043,6 +1284,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -1174,6 +1424,10 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lru-cache@11.3.6: + resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1182,6 +1436,10 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1199,6 +1457,13 @@ packages: engines: {node: '>= 18'} hasBin: true + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -1209,6 +1474,10 @@ packages: monaco-editor@0.55.1: resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1227,6 +1496,9 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1243,6 +1515,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1251,6 +1526,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1266,6 +1544,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -1281,6 +1563,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -1298,6 +1583,10 @@ packages: resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1314,6 +1603,10 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1337,13 +1630,30 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1352,6 +1662,9 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} @@ -1365,10 +1678,40 @@ packages: resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} engines: {node: '>=6'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.0.30: + resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==} + + tldts@7.0.30: + resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==} + hasBin: true + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -1397,6 +1740,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1469,15 +1816,84 @@ packages: yaml: optional: true + vitest@4.1.6: + resolution: {integrity: sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.6 + '@vitest/browser-preview': 4.1.6 + '@vitest/browser-webdriverio': 4.1.6 + '@vitest/coverage-istanbul': 4.1.6 + '@vitest/coverage-v8': 4.1.6 + '@vitest/ui': 4.1.6 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -1496,6 +1912,28 @@ packages: snapshots: + '@adobe/css-tools@4.4.4': {} + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -1646,6 +2084,34 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@emnapi/core@1.9.2': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -1708,6 +2174,8 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@exodus/bytes@1.15.0': {} + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -1784,6 +2252,8 @@ snapshots: '@oxc-project/types@0.124.0': {} + '@polka/url@1.0.0-next.29': {} + '@rjsf/core@5.24.13(@rjsf/utils@5.24.13(react@19.2.5))(react@19.2.5)': dependencies: '@rjsf/utils': 5.24.13(react@19.2.5) @@ -1863,6 +2333,8 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.7': {} + '@standard-schema/spec@1.1.0': {} + '@tailwindcss/node@4.2.2': dependencies: '@jridgewell/remapping': 2.3.5 @@ -1938,11 +2410,54 @@ snapshots: '@tanstack/query-core': 5.99.1 react: 19.2.5 + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 10.4.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/js-yaml@4.0.9': {} @@ -2060,6 +2575,58 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.8(@types/node@24.12.2)(jiti@2.6.1) + '@vitest/expect@4.1.6': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.6(vite@8.0.8(@types/node@24.12.2)(jiti@2.6.1))': + dependencies: + '@vitest/spy': 4.1.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.8(@types/node@24.12.2)(jiti@2.6.1) + + '@vitest/pretty-format@4.1.6': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.6': + dependencies: + '@vitest/utils': 4.1.6 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.6': + dependencies: + '@vitest/pretty-format': 4.1.6 + '@vitest/utils': 4.1.6 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.6': {} + + '@vitest/ui@4.1.6(vitest@4.1.6)': + dependencies: + '@vitest/utils': 4.1.6 + fflate: 0.8.2 + flatted: 3.4.2 + pathe: 2.0.3 + sirv: 3.0.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vitest: 4.1.6(@types/node@24.12.2)(@vitest/ui@4.1.6)(jsdom@29.1.1)(vite@8.0.8(@types/node@24.12.2)(jiti@2.6.1)) + + '@vitest/utils@4.1.6': + dependencies: + '@vitest/pretty-format': 4.1.6 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -2084,18 +2651,34 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-regex@5.0.1: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} baseline-browser-mapping@2.10.20: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + brace-expansion@1.1.14: dependencies: balanced-match: 1.0.2 @@ -2117,6 +2700,8 @@ snapshots: caniuse-lite@1.0.30001788: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -2159,16 +2744,38 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + csstype@3.2.3: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + deep-is@0.1.4: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dompurify@3.2.7: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -2180,6 +2787,10 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.2 + entities@8.0.0: {} + + es-module-lexer@2.1.0: {} + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -2267,8 +2878,14 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} + expect-type@1.3.0: {} + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -2281,6 +2898,8 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -2320,6 +2939,12 @@ snapshots: dependencies: hermes-estree: 0.25.1 + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + ignore@5.3.2: {} ignore@7.0.5: {} @@ -2331,12 +2956,16 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-potential-custom-element-name@1.0.1: {} + isexe@2.0.0: {} jiti@2.6.1: {} @@ -2347,6 +2976,32 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@exodus/bytes': 1.15.0 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.6 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -2443,6 +3098,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + lru-cache@11.3.6: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -2451,6 +3108,8 @@ snapshots: dependencies: react: 19.2.5 + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2461,6 +3120,10 @@ snapshots: marked@14.0.0: {} + mdn-data@2.27.1: {} + + min-indent@1.0.1: {} + minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -2474,6 +3137,8 @@ snapshots: dompurify: 3.2.7 marked: 14.0.0 + mrmime@2.0.1: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -2484,6 +3149,8 @@ snapshots: object-assign@4.1.1: {} + obug@2.1.1: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2505,10 +3172,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse5@8.0.1: + dependencies: + entities: 8.0.0 + path-exists@4.0.0: {} path-key@3.1.1: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@4.0.4: {} @@ -2521,6 +3194,12 @@ snapshots: prelude-ls@1.2.1: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -2536,6 +3215,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-is@18.3.1: {} react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5): @@ -2548,6 +3229,11 @@ snapshots: react@19.2.5: {} + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + require-from-string@2.0.2: {} reselect@5.1.1: {} @@ -2575,6 +3261,10 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} semver@6.3.1: {} @@ -2589,16 +3279,34 @@ snapshots: shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + source-map-js@1.2.1: {} + stackback@0.0.2: {} + state-local@1.0.7: {} + std-env@4.1.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-json-comments@3.1.1: {} supports-color@7.2.0: dependencies: has-flag: 4.0.0 + symbol-tree@3.2.4: {} + tabbable@6.4.0: {} tailwind-merge@3.5.0: {} @@ -2607,11 +3315,33 @@ snapshots: tapable@2.3.2: {} + tinybench@2.9.0: {} + + tinyexec@1.1.2: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyrainbow@3.1.0: {} + + tldts-core@7.0.30: {} + + tldts@7.0.30: + dependencies: + tldts-core: 7.0.30 + + totalist@3.0.1: {} + + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.30 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 @@ -2638,6 +3368,8 @@ snapshots: undici-types@7.16.0: {} + undici@7.25.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -2679,12 +3411,66 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 + vitest@4.1.6(@types/node@24.12.2)(@vitest/ui@4.1.6)(jsdom@29.1.1)(vite@8.0.8(@types/node@24.12.2)(jiti@2.6.1)): + dependencies: + '@vitest/expect': 4.1.6 + '@vitest/mocker': 4.1.6(vite@8.0.8(@types/node@24.12.2)(jiti@2.6.1)) + '@vitest/pretty-format': 4.1.6 + '@vitest/runner': 4.1.6 + '@vitest/snapshot': 4.1.6 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.8(@types/node@24.12.2)(jiti@2.6.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.12.2 + '@vitest/ui': 4.1.6(vitest@4.1.6) + jsdom: 29.1.1 + transitivePeerDependencies: + - msw + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + which@2.0.2: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + yallist@3.1.1: {} yocto-queue@0.1.0: {} From 9e5196af0498efbbd69a80f7e9e333ba8495a2ad Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 12 May 2026 20:44:36 +0300 Subject: [PATCH 2/4] feat(schema-form): detect sensitive string fields by name Adds a pure helper that walks an OpenAPI/RJSF schema and tags every string field whose name matches the conventional credential patterns (access/secret keys, passwords, tokens, private keys, api keys) with a `SensitiveStringWidget` binding in the uiSchema. Cozystack chart schemas do not annotate sensitive fields with a `format: password` hint, so the detection is name-based and runs in the same style as the existing storageClass/backupClassName/disks helpers in SchemaForm. The helper lives in a separate module so it can be covered with unit tests without rendering RJSF. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- apps/console/src/lib/sensitive-fields.test.ts | 271 ++++++++++++++++++ apps/console/src/lib/sensitive-fields.ts | 169 +++++++++++ 2 files changed, 440 insertions(+) create mode 100644 apps/console/src/lib/sensitive-fields.test.ts create mode 100644 apps/console/src/lib/sensitive-fields.ts diff --git a/apps/console/src/lib/sensitive-fields.test.ts b/apps/console/src/lib/sensitive-fields.test.ts new file mode 100644 index 0000000..8a04bf7 --- /dev/null +++ b/apps/console/src/lib/sensitive-fields.test.ts @@ -0,0 +1,271 @@ +import { describe, it, expect } from "vitest" +import type { RJSFSchema, UiSchema } from "@rjsf/utils" +import { addSensitiveStringWidgets, isSensitiveFieldName } from "./sensitive-fields.ts" + +describe("isSensitiveFieldName", () => { + it("matches the canonical S3/postgres credential field names", () => { + expect(isSensitiveFieldName("s3AccessKey")).toBe(true) + expect(isSensitiveFieldName("s3SecretKey")).toBe(true) + expect(isSensitiveFieldName("secretAccessKey")).toBe(true) + expect(isSensitiveFieldName("secretAccessKeyKey")).toBe(true) + expect(isSensitiveFieldName("accessKey")).toBe(true) + expect(isSensitiveFieldName("access_key")).toBe(true) + expect(isSensitiveFieldName("secret_key")).toBe(true) + }) + + it("matches generic password/token/private-key/api-key style names", () => { + expect(isSensitiveFieldName("password")).toBe(true) + expect(isSensitiveFieldName("rootPassword")).toBe(true) + expect(isSensitiveFieldName("passwordConfirmation")).toBe(true) + expect(isSensitiveFieldName("passwd")).toBe(true) + expect(isSensitiveFieldName("token")).toBe(true) + expect(isSensitiveFieldName("bearerToken")).toBe(true) + expect(isSensitiveFieldName("api_token")).toBe(true) + expect(isSensitiveFieldName("apiKey")).toBe(true) + expect(isSensitiveFieldName("api_key")).toBe(true) + expect(isSensitiveFieldName("privateKey")).toBe(true) + expect(isSensitiveFieldName("private_key")).toBe(true) + }) + + it("is case-insensitive", () => { + expect(isSensitiveFieldName("PASSWORD")).toBe(true) + expect(isSensitiveFieldName("S3SECRETKEY")).toBe(true) + }) + + it("does not match neighbouring/non-secret names", () => { + expect(isSensitiveFieldName("storageClass")).toBe(false) + expect(isSensitiveFieldName("backupClassName")).toBe(false) + expect(isSensitiveFieldName("bucket")).toBe(false) + expect(isSensitiveFieldName("region")).toBe(false) + expect(isSensitiveFieldName("publicKey")).toBe(false) + expect(isSensitiveFieldName("keyName")).toBe(false) + expect(isSensitiveFieldName("name")).toBe(false) + }) + + it("respects word boundaries to avoid false positives on token/passwd", () => { + // `token` is matched only as the trailing word. + expect(isSensitiveFieldName("tokenAudience")).toBe(false) + expect(isSensitiveFieldName("tokenizer")).toBe(false) + expect(isSensitiveFieldName("csrfTokenName")).toBe(false) + // `passwd` likewise — `passwdFile` is a path, not a credential. + expect(isSensitiveFieldName("passwdFile")).toBe(false) + expect(isSensitiveFieldName("etcPasswdPath")).toBe(false) + }) +}) + +describe("addSensitiveStringWidgets", () => { + it("adds the widget to a top-level sensitive string field", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + s3AccessKey: { type: "string" }, + bucket: { type: "string" }, + }, + } + const result = addSensitiveStringWidgets(schema, {}) + expect(result.s3AccessKey).toEqual({ "ui:widget": "SensitiveStringWidget" }) + expect(result.bucket).toBeUndefined() + }) + + it("recurses into nested object schemas", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + backup: { + type: "object", + properties: { + s3SecretKey: { type: "string" }, + bucket: { type: "string" }, + }, + }, + }, + } + const result = addSensitiveStringWidgets(schema, {}) as Record + expect(result.backup?.s3SecretKey).toEqual({ "ui:widget": "SensitiveStringWidget" }) + expect(result.backup?.bucket).toBeUndefined() + }) + + it("masks individual string items when the array key itself is sensitive", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + apiKey: { type: "array", items: { type: "string" } }, + }, + } + const result = addSensitiveStringWidgets(schema, {}) as Record + const itemsUi = (result.apiKey as { items?: UiSchema } | undefined)?.items + expect(itemsUi?.["ui:widget"]).toBe("SensitiveStringWidget") + }) + + it("leaves array-of-strings unmasked when the array key is not sensitive", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + labels: { type: "array", items: { type: "string" } }, + }, + } + const result = addSensitiveStringWidgets(schema, {}) + expect(result.labels).toBeUndefined() + }) + + it("recurses into array items expressed as a tuple of schemas", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + rotations: { + type: "array", + items: [ + { + type: "object", + properties: { + password: { type: "string" }, + }, + }, + ], + }, + }, + } + const result = addSensitiveStringWidgets(schema, {}) as Record + const itemsUi = (result.rotations as { items?: Record } | undefined)?.items + expect(itemsUi?.password).toEqual({ "ui:widget": "SensitiveStringWidget" }) + }) + + it("recurses into array item objects", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + users: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + password: { type: "string" }, + }, + }, + }, + }, + } + const result = addSensitiveStringWidgets(schema, {}) as Record + const itemsUi = (result.users as { items?: Record } | undefined)?.items + expect(itemsUi?.password).toEqual({ "ui:widget": "SensitiveStringWidget" }) + expect(itemsUi?.name).toBeUndefined() + }) + + it("ignores non-string fields even with sensitive names", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + accessKey: { type: "integer" }, + password: { type: "boolean" }, + }, + } + const result = addSensitiveStringWidgets(schema, {}) + expect(result.accessKey).toBeUndefined() + expect(result.password).toBeUndefined() + }) + + it("ignores string fields with an enum (rendered as select)", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + password: { type: "string", enum: ["weak", "strong"] }, + }, + } + const result = addSensitiveStringWidgets(schema, {}) + expect(result.password).toBeUndefined() + }) + + it("does not overwrite an existing ui:widget on the same field", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + accessKey: { type: "string" }, + }, + } + const initial: UiSchema = { + accessKey: { "ui:widget": "SomeOtherWidget" }, + } + const result = addSensitiveStringWidgets(schema, initial) + expect(result.accessKey).toEqual({ "ui:widget": "SomeOtherWidget" }) + }) + + it("preserves unrelated uiSchema entries while adding the widget", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + s3AccessKey: { type: "string" }, + bucket: { type: "string" }, + }, + } + const initial: UiSchema = { + bucket: { "ui:placeholder": "my-bucket" }, + } + const result = addSensitiveStringWidgets(schema, initial) + expect(result.s3AccessKey).toEqual({ "ui:widget": "SensitiveStringWidget" }) + expect(result.bucket).toEqual({ "ui:placeholder": "my-bucket" }) + }) + + it("returns the input uiSchema untouched when the schema has no properties", () => { + const initial: UiSchema = { "ui:order": ["a", "b"] } + expect(addSensitiveStringWidgets({} as RJSFSchema, initial)).toEqual(initial) + }) + + it("does not mutate the input uiSchema when array items propagate the widget", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + apiKey: { type: "array", items: { type: "string" } }, + }, + } + const initial: UiSchema = { apiKey: { items: { "ui:placeholder": "foo" } } } + const snapshot: UiSchema = JSON.parse(JSON.stringify(initial)) + addSensitiveStringWidgets(schema, initial) + expect(initial).toEqual(snapshot) + }) + + it("does not mutate the input uiSchema when array items contain a sensitive object field", () => { + const schema: RJSFSchema = { + type: "object", + properties: { + users: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + password: { type: "string" }, + }, + }, + }, + }, + } + const initial: UiSchema = { + users: { items: { name: { "ui:placeholder": "alice" } } }, + } + const snapshot: UiSchema = JSON.parse(JSON.stringify(initial)) + addSensitiveStringWidgets(schema, initial) + expect(initial).toEqual(snapshot) + }) + + it("pins the current 'oneOf branches are not walked' limitation", () => { + // FIXME: extend addSensitiveStringWidgets to recurse into oneOf/anyOf/allOf. + // Once that is implemented, flip this test to expect the inner field to + // be masked instead of being left untouched. + const schema: RJSFSchema = { + type: "object", + properties: { + auth: { + oneOf: [ + { + type: "object", + properties: { password: { type: "string" } }, + }, + ], + }, + }, + } as RJSFSchema + const result = addSensitiveStringWidgets(schema, {}) + expect(result.auth).toBeUndefined() + }) +}) diff --git a/apps/console/src/lib/sensitive-fields.ts b/apps/console/src/lib/sensitive-fields.ts new file mode 100644 index 0000000..104e3e9 --- /dev/null +++ b/apps/console/src/lib/sensitive-fields.ts @@ -0,0 +1,169 @@ +import type { RJSFSchema, UiSchema } from "@rjsf/utils" + +// Word tokens that look like credentials wherever they appear in the +// field name. `password` covers `password`, `rootPassword`, +// `passwordConfirmation`; the `*key` entries cover S3-style credentials +// in any prefix (`s3AccessKey`, `myApiKey`, etc.). +const ANYWHERE_TOKENS: ReadonlySet = new Set([ + "accesskey", + "secretkey", + "secretaccesskey", + "password", + "apikey", + "privatekey", +]) + +// Word tokens that look like credentials ONLY when they are the last +// word of the field name, to avoid masking unrelated identifiers that +// happen to contain the word. `bearerToken`/`api_token` match; +// `tokenAudience`/`csrfTokenName`/`passwdFile` do not. +const SUFFIX_ONLY_TOKENS: ReadonlySet = new Set([ + "token", + "passwd", +]) + +/** + * Split a field name into lower-case word parts, treating both + * camelCase boundaries and `_` / `-` / `.` as word separators. Digits + * stay attached to the preceding letter (`s3` ⇒ `s3`). + */ +function splitWords(name: string): string[] { + return name + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .split(/[\s_\-.]+/) + .filter(Boolean) + .map((part) => part.toLowerCase()) +} + +/** + * Return every joined run of consecutive word parts, so that + * `secretAccessKey` and `secret_access_key` both produce the chunk + * `secretaccesskey` for matching, without the detector having to + * enumerate every casing/concatenation variant. + */ +function chunks(words: string[]): string[] { + const out: string[] = [] + for (let start = 0; start < words.length; start++) { + let chunk = "" + for (let end = start; end < words.length; end++) { + chunk += words[end] + out.push(chunk) + } + } + return out +} + +/** + * Returns true when a field name looks like a credential by convention. + * Cozystack chart schemas do not annotate sensitive fields with a + * `format: password` hint, so detection is purely name-based. The match + * is on word boundaries (camelCase / `_` / `-` / `.`) rather than a raw + * substring, so neighbours like `tokenAudience` or `passwdFile` stay + * out of the set. + */ +export function isSensitiveFieldName(name: string): boolean { + const words = splitWords(name) + if (words.length === 0) return false + + for (const chunk of chunks(words)) { + if (ANYWHERE_TOKENS.has(chunk)) return true + } + + const last = words[words.length - 1] + return SUFFIX_ONLY_TOKENS.has(last) +} + +interface JsonSchemaLike { + type?: string + enum?: unknown[] + properties?: Record + // JSON Schema allows `items` to be a single schema or a tuple of schemas. + items?: JsonSchemaLike | JsonSchemaLike[] +} + +/** + * Recursively walks `schema` and, for every string field whose name matches + * a sensitive pattern, sets `ui:widget` to `SensitiveStringWidget` on the + * corresponding entry in `uiSchema`. Existing widget bindings are preserved. + * + * The input `uiSchema` is not mutated; the returned object is independent. + * + * Limitation: only `properties` and `items` subtrees are walked. Fields + * inside `oneOf` / `anyOf` / `allOf` branches are NOT inspected — see the + * "pin broken behaviour" test in sensitive-fields.test.ts. Extend when a + * chart in the catalogue starts using that shape. + */ +export function addSensitiveStringWidgets( + schema: RJSFSchema, + uiSchema: UiSchema = {}, +): UiSchema { + if (!schema || typeof schema !== "object") return uiSchema + const properties = (schema as JsonSchemaLike).properties + if (!properties) return uiSchema + + const result: UiSchema = { ...uiSchema } + + for (const [key, fieldSchema] of Object.entries(properties)) { + if (!fieldSchema || typeof fieldSchema !== "object") continue + + const existing = (result[key] ?? {}) as UiSchema + const alreadyHasWidget = typeof existing["ui:widget"] === "string" + + const isLeafString = + fieldSchema.type === "string" && !Array.isArray(fieldSchema.enum) + + if (isLeafString && isSensitiveFieldName(key) && !alreadyHasWidget) { + result[key] = { ...existing, "ui:widget": "SensitiveStringWidget" } + continue + } + + if (fieldSchema.properties) { + result[key] = addSensitiveStringWidgets( + fieldSchema as RJSFSchema, + existing, + ) + continue + } + + if (fieldSchema.type === "array" && fieldSchema.items) { + const itemSchemas = Array.isArray(fieldSchema.items) + ? fieldSchema.items + : [fieldSchema.items] + // Shallow-copy so we never write through into the caller's items + // sub-object — see "does not mutate the input uiSchema" tests. + const merged: UiSchema = { ...((existing.items ?? {}) as UiSchema) } + let mutated = false + const keyIsSensitive = isSensitiveFieldName(key) + for (const itemSchema of itemSchemas) { + if (!itemSchema) continue + if (itemSchema.properties) { + const next = addSensitiveStringWidgets( + itemSchema as RJSFSchema, + merged, + ) + for (const [k, v] of Object.entries(next)) merged[k] = v + mutated = true + continue + } + // Array of scalar credentials, e.g. `apiKey: { items: {type:"string"} }`. + // The array key itself is the only signal we have for individual items, + // so propagate the widget down only when the array's name matches. + const itemIsLeafString = + itemSchema.type === "string" && !Array.isArray(itemSchema.enum) + if ( + itemIsLeafString && + keyIsSensitive && + typeof merged["ui:widget"] !== "string" + ) { + merged["ui:widget"] = "SensitiveStringWidget" + mutated = true + } + } + if (mutated) { + result[key] = { ...existing, items: merged } + } + } + } + + return result +} From de813588796acde0b8cd5411bc76cfa567c60c0a Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 12 May 2026 20:45:38 +0300 Subject: [PATCH 3/4] feat(schema-form): add reveal-toggle widget for sensitive strings Adds an RJSF widget that renders a credential field as eight bullets plus an eye-icon toggle: clicking the eye swaps the bullets for a plain text input that forwards typing through the standard onChange contract (empty string becomes undefined). When the field is disabled or readonly the toggle is disabled and editing is blocked. The visual style mirrors the SecretRow reveal control already used in the Secrets tab. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- .../components/SensitiveStringWidget.test.tsx | 174 ++++++++++++++++++ .../src/components/SensitiveStringWidget.tsx | 56 ++++++ 2 files changed, 230 insertions(+) create mode 100644 apps/console/src/components/SensitiveStringWidget.test.tsx create mode 100644 apps/console/src/components/SensitiveStringWidget.tsx diff --git a/apps/console/src/components/SensitiveStringWidget.test.tsx b/apps/console/src/components/SensitiveStringWidget.test.tsx new file mode 100644 index 0000000..f0ff91f --- /dev/null +++ b/apps/console/src/components/SensitiveStringWidget.test.tsx @@ -0,0 +1,174 @@ +import { useState } from "react" +import { describe, it, expect, vi } from "vitest" +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import type { WidgetProps } from "@rjsf/utils" +import { SensitiveStringWidget } from "./SensitiveStringWidget.tsx" + +function makeProps(overrides: Partial = {}): WidgetProps { + const base = { + id: "sensitive", + name: "sensitive", + label: "sensitive", + value: undefined as unknown, + onChange: vi.fn(), + onBlur: vi.fn(), + onFocus: vi.fn(), + required: false, + disabled: false, + readonly: false, + autofocus: false, + placeholder: "", + options: {}, + schema: { type: "string" }, + uiSchema: {}, + formContext: {}, + rawErrors: [], + hideError: false, + multiple: false, + registry: {}, + } + return { ...base, ...overrides } as unknown as WidgetProps +} + +function renderWithLabel(props: WidgetProps) { + return render( + <> + + + , + ) +} + +const TOGGLE_NAME = /toggle credential visibility/i + +describe("SensitiveStringWidget", () => { + it("renders a real control associated with its label, regardless of state", () => { + renderWithLabel(makeProps({ value: "super-secret" })) + + const input = screen.getByLabelText("access key") as HTMLInputElement + expect(input.tagName).toBe("INPUT") + expect(input.type).toBe("password") + expect(input.value).toBe("super-secret") + }) + + it("flips the input type from password to text when the toggle is pressed", async () => { + const user = userEvent.setup() + renderWithLabel(makeProps({ value: "super-secret" })) + + const toggle = screen.getByRole("button", { name: TOGGLE_NAME }) + expect(toggle).toHaveAttribute("aria-pressed", "false") + + await user.click(toggle) + + expect(toggle).toHaveAttribute("aria-pressed", "true") + expect((screen.getByLabelText("access key") as HTMLInputElement).type).toBe("text") + }) + + it("keeps the toggle's accessible name stable across state changes", async () => { + const user = userEvent.setup() + renderWithLabel(makeProps({ value: "super-secret" })) + + const toggle = screen.getByRole("button", { name: TOGGLE_NAME }) + await user.click(toggle) + + // Same button is still findable by the same stable name after pressing. + expect(screen.getByRole("button", { name: TOGGLE_NAME })).toBe(toggle) + }) + + it("hides the value again after a second toggle press", async () => { + const user = userEvent.setup() + renderWithLabel(makeProps({ value: "super-secret" })) + + const toggle = screen.getByRole("button", { name: TOGGLE_NAME }) + await user.click(toggle) + await user.click(toggle) + + expect(toggle).toHaveAttribute("aria-pressed", "false") + expect((screen.getByLabelText("access key") as HTMLInputElement).type).toBe("password") + }) + + it("carries the project's input styling in both states (no shape change on toggle)", async () => { + const user = userEvent.setup() + renderWithLabel(makeProps({ value: "super-secret" })) + + const input = screen.getByLabelText("access key") as HTMLInputElement + expect(input).toHaveClass("rounded-md") + expect(input).toHaveClass("border") + + await user.click(screen.getByRole("button", { name: TOGGLE_NAME })) + + expect(input).toHaveClass("rounded-md") + expect(input).toHaveClass("border") + }) + + it("opts the input out of browser password-manager autofill", () => { + renderWithLabel(makeProps({ value: "super-secret" })) + + const input = screen.getByLabelText("access key") as HTMLInputElement + expect(input).toHaveAttribute("autocomplete", "new-password") + }) + + it("forwards typing into onChange without requiring the toggle first", async () => { + const user = userEvent.setup() + const onChange = vi.fn() + + function Host() { + const [value, setValue] = useState("") + return ( + <> + + { + onChange(next) + setValue(next as string | undefined) + }, + })} + /> + + ) + } + render() + + await user.type(screen.getByLabelText("access key"), "abc") + + expect(onChange).toHaveBeenLastCalledWith("abc") + }) + + it("emits undefined when the input is cleared", async () => { + const user = userEvent.setup() + const onChange = vi.fn() + renderWithLabel(makeProps({ value: "secret", onChange })) + + await user.clear(screen.getByLabelText("access key") as HTMLInputElement) + + expect(onChange).toHaveBeenLastCalledWith(undefined) + }) + + it("keeps the reveal toggle usable when the form is readonly", async () => { + const user = userEvent.setup() + renderWithLabel(makeProps({ value: "secret", readonly: true })) + + const input = screen.getByLabelText("access key") as HTMLInputElement + expect(input).toHaveAttribute("readonly") + + await user.click(screen.getByRole("button", { name: TOGGLE_NAME })) + + expect(input.type).toBe("text") + expect(input.value).toBe("secret") + }) + + it("keeps the reveal toggle usable when the form is disabled", async () => { + const user = userEvent.setup() + renderWithLabel(makeProps({ value: "secret", disabled: true })) + + const input = screen.getByLabelText("access key") as HTMLInputElement + expect(input).toBeDisabled() + + await user.click(screen.getByRole("button", { name: TOGGLE_NAME })) + + expect(input.type).toBe("text") + }) +}) diff --git a/apps/console/src/components/SensitiveStringWidget.tsx b/apps/console/src/components/SensitiveStringWidget.tsx new file mode 100644 index 0000000..26e8f61 --- /dev/null +++ b/apps/console/src/components/SensitiveStringWidget.tsx @@ -0,0 +1,56 @@ +import { useState } from "react" +import { Eye, EyeOff } from "lucide-react" +import type { WidgetProps } from "@rjsf/utils" + +// Matches the styling applied to `input[type="text"]` in schema-form.css. +// The CSS rule there is keyed on the `type` attribute, so a `type="password"` +// input would otherwise fall back to native browser chrome and visibly differ +// from neighbouring fields every time the user toggles reveal. +const INPUT_CLASS = + "w-full rounded-md border border-slate-200 bg-white px-2.5 py-1.5 text-sm text-slate-900 shadow-sm outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 disabled:opacity-50 disabled:cursor-not-allowed" + +const TOGGLE_CLASS = + "flex size-8 shrink-0 items-center justify-center rounded-md text-slate-500 hover:bg-slate-100" + +export function SensitiveStringWidget(props: WidgetProps) { + const { id, value, onChange, required, disabled, readonly, autofocus, placeholder } = props + const [revealed, setRevealed] = useState(false) + + const stringValue = typeof value === "string" ? value : "" + + return ( +
+ { + // Empty string is coerced to undefined so the surrounding form + // drops the key from the spec entirely on clear, matching the + // convention used by the other custom widgets in this folder. + const next = event.target.value + onChange(next === "" ? undefined : next) + }} + className={INPUT_CLASS} + /> + +
+ ) +} From 380bd4f58fdf22bc3662b542f0df46d7734f9681 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Tue, 12 May 2026 20:46:33 +0300 Subject: [PATCH 4/4] feat(schema-form): mask access/secret keys in application forms Wires the sensitive-field detector into SchemaForm's uiSchema pipeline and registers the toggle widget under the customWidgets map. As a result, fields like postgres' s3AccessKey and s3SecretKey on the application order/edit screens render as bullets with a click-to-reveal control instead of plain text. Detection is purely additive and does not override widgets bound by earlier detectors. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- apps/console/src/components/SchemaForm.tsx | 10 +++++++--- apps/console/src/components/rjsf-templates.tsx | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/console/src/components/SchemaForm.tsx b/apps/console/src/components/SchemaForm.tsx index 5f728c2..88633e4 100644 --- a/apps/console/src/components/SchemaForm.tsx +++ b/apps/console/src/components/SchemaForm.tsx @@ -3,6 +3,7 @@ import Form from "@rjsf/core" import validator from "@rjsf/validator-ajv8" import type { RJSFSchema, UiSchema, TemplatesType } from "@rjsf/utils" import { keysOrderToUiSchema, sanitizeSchema } from "../lib/keys-order.ts" +import { addSensitiveStringWidgets } from "../lib/sensitive-fields.ts" import { customTemplates, customWidgets } from "./rjsf-templates.tsx" import { AdditionalPropertiesField } from "./AdditionalPropertiesField.tsx" import { ResourceQuotasField } from "./ResourceQuotasField.tsx" @@ -187,18 +188,21 @@ export function SchemaForm({ // Automatically add AdditionalPropertiesField for fields with additionalProperties schema const withAdditionalProps = addAdditionalPropertiesWidgets(schema, withVMDisk) + // Mask credential-shaped string fields (access/secret keys, passwords, tokens). + const withSensitive = addSensitiveStringWidgets(schema, withAdditionalProps) + // Override resourceQuotas field with structured quota editor. // Scoped to schemas where resourceQuotas has additionalProperties: {type: "string"} // (the cozystack-tenants chart shape) to avoid activating on unrelated CRDs. const rqSchema = (schema as any).properties?.resourceQuotas if (rqSchema && rqSchema.additionalProperties?.type === "string") { - withAdditionalProps.resourceQuotas = { - ...withAdditionalProps.resourceQuotas, + withSensitive.resourceQuotas = { + ...withSensitive.resourceQuotas, "ui:field": "ResourceQuotasField", } } - return withAdditionalProps + return withSensitive }, [keysOrder, schema]) const customFields = useMemo( diff --git a/apps/console/src/components/rjsf-templates.tsx b/apps/console/src/components/rjsf-templates.tsx index bfd9635..acaba28 100644 --- a/apps/console/src/components/rjsf-templates.tsx +++ b/apps/console/src/components/rjsf-templates.tsx @@ -13,6 +13,7 @@ import { StorageClassWidget } from "./StorageClassWidget.tsx" import { AdditionalPropertiesWidget } from "./AdditionalPropertiesWidget.tsx" import { VMDiskWidget } from "./VMDiskWidget.tsx" import { BackupClassWidget } from "./BackupClassWidget.tsx" +import { SensitiveStringWidget } from "./SensitiveStringWidget.tsx" function IconButton< T = any, @@ -93,4 +94,5 @@ export const customWidgets = { AdditionalPropertiesWidget: AdditionalPropertiesWidget, VMDiskWidget: VMDiskWidget, BackupClassWidget: BackupClassWidget, + SensitiveStringWidget: SensitiveStringWidget, }