From a8302429472e45ee949b2abd62037c6062368c84 Mon Sep 17 00:00:00 2001 From: Lectom Date: Fri, 29 May 2026 12:05:43 +0900 Subject: [PATCH] ci: add code check badges and coverage reports --- .gitea/workflows/code_check.yml | 268 ++++ .gitignore | 3 + README.md | 10 + adminfront/biome.json | 6 +- adminfront/package-lock.json | 577 +++++-- adminfront/package.json | 4 + adminfront/pnpm-lock.yaml | 98 ++ adminfront/pnpm-workspace.yaml | 1 + adminfront/scripts/serve-prod.mjs | 15 +- adminfront/src/app/routes.tsx | 2 +- .../common/LanguageSelector.test.tsx | 2 +- .../components/common/LanguageSelector.tsx | 1 + .../src/components/layout/AppLayout.tsx | 22 +- adminfront/src/components/ui/avatar.tsx | 2 +- adminfront/src/components/ui/card.tsx | 6 +- adminfront/src/components/ui/dialog.tsx | 22 +- .../src/components/ui/dropdown-menu.tsx | 14 +- adminfront/src/components/ui/select.tsx | 12 +- adminfront/src/components/ui/table.tsx | 6 +- adminfront/src/components/ui/tabs.tsx | 2 +- .../src/features/auth/AuthPage.test.tsx | 4 +- .../src/features/auth/LoginPage.test.tsx | 8 +- .../auth/components/PermissionChecker.tsx | 28 +- .../integrity/DataIntegrityPage.test.tsx | 8 +- .../features/integrity/DataIntegrityPage.tsx | 2 +- .../features/overview/GlobalOverviewPage.tsx | 15 +- .../projections/UserProjectionPage.test.tsx | 12 +- .../projections/UserProjectionPage.tsx | 39 +- .../components/ParentTenantSelector.tsx | 2 +- .../routes/TenantAdminsAndOwnersTab.tsx | 9 +- .../tenants/routes/TenantCreatePage.tsx | 6 +- .../tenants/routes/TenantGroupsPage.tsx | 2 +- .../tenants/routes/TenantListPage.tsx | 62 +- .../tenants/routes/TenantProfilePage.tsx | 24 +- .../tenants/routes/TenantSchemaPage.tsx | 2 +- .../tenants/routes/TenantWorksmobilePage.tsx | 18 +- .../features/tenants/routes/tenantListView.ts | 2 +- .../features/tenants/utils/orgConfig.test.ts | 2 +- .../tenants/utils/tenantCsvImport.test.ts | 1 - .../features/tenants/utils/tenantCsvImport.ts | 3 +- .../routes/GlobalUserGroupListPage.tsx | 2 +- .../routes/TenantUserGroupsTab.tsx | 12 +- .../routes/UserGroupDetailPage.tsx | 4 +- .../src/features/users/UserCreatePage.tsx | 10 +- .../src/features/users/UserDetailPage.tsx | 10 +- .../users/UserListPage.render.test.tsx | 68 +- .../src/features/users/UserListPage.tsx | 526 ++++--- .../components/UserBulkMoveGroupModal.tsx | 6 +- .../users/components/UserBulkUploadModal.tsx | 15 +- .../src/features/users/orgChartPicker.test.ts | 4 +- adminfront/src/features/users/userStatus.ts | 4 +- .../src/features/users/utils/csvParser.ts | 4 +- adminfront/src/index.css | 3 +- adminfront/src/lib/apiClient.ts | 4 +- adminfront/src/lib/debugLog.ts | 4 +- adminfront/src/lib/locale.ts | 6 +- adminfront/src/lib/roles.test.ts | 4 +- adminfront/src/lib/sessionSliding.test.ts | 2 +- adminfront/src/lib/sessionSliding.ts | 4 +- adminfront/src/lib/sort.test.ts | 2 +- adminfront/src/main.tsx | 2 +- adminfront/src/test/i18nMock.ts | 9 +- adminfront/tests/tenants.spec.ts | 9 +- adminfront/tests/users.spec.ts | 187 +++ adminfront/vite.config.ts | 2 +- adminfront/vitest.config.ts | 32 + common/biome.json | 4 + common/config/biome.base.json | 34 +- common/config/vite.base.ts | 2 +- .../core/components/audit/AuditLogTable.tsx | 80 +- common/core/components/overview/index.ts | 2 +- common/core/components/page/PageHeader.tsx | 2 +- .../components/sort/SortableTableHead.tsx | 10 +- common/core/i18n/index.ts | 2 +- common/core/i18n/loader.ts | 12 +- common/core/pagination/cursorFetch.ts | 9 +- common/core/pagination/cursorFetch.worker.ts | 41 +- common/core/pagination/cursorFetchCore.ts | 4 +- common/core/pagination/index.ts | 4 +- common/package-lock.json | 73 +- common/package.json | 46 +- common/pnpm-lock.yaml | 320 ++-- common/shell/index.ts | 4 +- devfront/biome.json | 6 +- devfront/package-lock.json | 1164 +++++++++++++- devfront/package.json | 6 +- devfront/pnpm-lock.yaml | 615 +++++++- devfront/src/app/routes.tsx | 2 +- devfront/src/components/layout/AppLayout.tsx | 17 +- devfront/src/components/ui/avatar.tsx | 2 +- devfront/src/components/ui/card.tsx | 6 +- devfront/src/components/ui/table.tsx | 6 +- devfront/src/features/auth/LoginPage.tsx | 3 +- .../features/clients/ClientGeneralPage.tsx | 307 ++-- .../features/clients/ClientRelationsPage.tsx | 7 +- devfront/src/features/clients/ClientsPage.tsx | 14 +- .../clients/routes/ClientFederationPage.tsx | 16 +- .../DeveloperRequestPage.tsx | 2 +- .../features/overview/GlobalOverviewPage.tsx | 15 +- devfront/src/index.css | 3 +- devfront/src/lib/apiClient.ts | 4 +- devfront/src/lib/authConfig.test.ts | 2 +- devfront/src/lib/sessionSliding.ts | 2 +- devfront/tests/devfront-client-tabs.spec.ts | 2 +- devfront/vitest.config.ts | 30 + docs/badges/adminfront-coverage.svg | 19 + docs/badges/badges.json | 42 + docs/badges/biome.svg | 19 + docs/badges/code-check.svg | 19 + docs/badges/devfront-coverage.svg | 19 + docs/badges/orgfront-coverage.svg | 19 + docs/badges/userfront-e2e-fast.svg | 19 + docs/badges/userfront-e2e-full.svg | 19 + mcp/hydra-mcp/biome.json | 4 + mcp/hydra-mcp/package-lock.json | 1337 +++++++++++++++++ mcp/hydra-mcp/package.json | 7 +- mcp/hydra-mcp/src/index.js | 41 +- mcp/hydra-mcp/src/runner.js | 11 +- mcp/keto-mcp/biome.json | 4 + mcp/keto-mcp/package-lock.json | 1337 +++++++++++++++++ mcp/keto-mcp/package.json | 7 +- mcp/keto-mcp/src/index.js | 26 +- mcp/keto-mcp/src/runner.js | 11 +- orgfront/biome.json | 6 +- orgfront/package-lock.json | 554 ++++++- orgfront/package.json | 5 +- orgfront/src/app/routes.tsx | 2 +- orgfront/src/components/layout/AppLayout.tsx | 10 +- orgfront/src/components/ui/avatar.tsx | 2 +- orgfront/src/components/ui/card.tsx | 6 +- orgfront/src/components/ui/table.tsx | 6 +- orgfront/src/features/auth/AuthGuard.test.tsx | 2 +- orgfront/src/features/auth/LoginPage.test.tsx | 2 +- orgfront/src/features/auth/LoginPage.tsx | 3 +- .../features/clients/ClientGeneralPage.tsx | 196 ++- .../clients/routes/ClientFederationPage.tsx | 16 +- orgfront/src/features/orgchart/pickerTree.ts | 2 +- .../features/orgchart/rankPriority.test.ts | 4 +- .../orgchart/routes/OrgChartPage.test.tsx | 27 +- .../features/orgchart/routes/OrgChartPage.tsx | 12 +- .../routes/OrgPickerEmbedPreviewPage.tsx | 2 +- .../orgchart/routes/OrgPickerPage.tsx | 4 +- .../src/features/orgchart/userDisplay.test.ts | 5 +- orgfront/src/lib/apiClient.ts | 4 +- orgfront/src/lib/authConfig.test.ts | 2 +- .../org-context-chart/orgContextChart.test.ts | 2 +- orgfront/tests/orgchart-vector-render.spec.ts | 19 +- orgfront/tests/orgfront-auto-login.spec.ts | 2 +- orgfront/vitest.config.ts | 30 + scripts/summarize_vitest_coverage.mjs | 81 + scripts/update_code_check_badges.mjs | 230 +++ userfront-e2e/biome.json | 4 + userfront-e2e/package-lock.json | 179 +++ userfront-e2e/package.json | 3 + userfront-e2e/playwright.config.ts | 48 +- .../scripts/serve-userfront-build.mjs | 119 +- userfront-e2e/tests/auth-routing.spec.ts | 251 ++-- .../tests/login-performance-budget.spec.ts | 127 +- .../tests/oidc-login-challenge.spec.ts | 48 +- .../tests/password-and-reset.spec.ts | 219 +-- .../tests/profile-department.spec.ts | 223 +-- userfront-e2e/tests/route-inventory.spec.ts | 240 +-- .../tests/runtime-env-mobile.spec.ts | 170 ++- .../tests/session-cross-browser-debug.spec.ts | 107 +- 164 files changed, 9059 insertions(+), 2012 deletions(-) create mode 100644 common/biome.json create mode 100644 docs/badges/adminfront-coverage.svg create mode 100644 docs/badges/badges.json create mode 100644 docs/badges/biome.svg create mode 100644 docs/badges/code-check.svg create mode 100644 docs/badges/devfront-coverage.svg create mode 100644 docs/badges/orgfront-coverage.svg create mode 100644 docs/badges/userfront-e2e-fast.svg create mode 100644 docs/badges/userfront-e2e-full.svg create mode 100644 mcp/hydra-mcp/biome.json create mode 100644 mcp/hydra-mcp/package-lock.json create mode 100644 mcp/keto-mcp/biome.json create mode 100644 mcp/keto-mcp/package-lock.json create mode 100644 scripts/summarize_vitest_coverage.mjs create mode 100644 scripts/update_code_check_badges.mjs create mode 100644 userfront-e2e/biome.json diff --git a/.gitea/workflows/code_check.yml b/.gitea/workflows/code_check.yml index 315a20aa..71ce5546 100644 --- a/.gitea/workflows/code_check.yml +++ b/.gitea/workflows/code_check.yml @@ -4,6 +4,8 @@ on: push: branches: - dev + paths-ignore: + - "docs/badges/**" pull_request: branches: - dev @@ -49,6 +51,14 @@ on: required: true type: boolean default: true + run_front_coverage: + description: "Run adminfront/devfront/orgfront Vitest coverage and upload reports" + required: true + type: boolean + default: true + +permissions: + contents: write jobs: lint: @@ -148,6 +158,51 @@ jobs: cd userfront flutter analyze --no-fatal-warnings --no-fatal-infos + biome-check: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_lint == true }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Install adminfront dependencies + run: | + cd adminfront + npx pnpm install -C ../common --no-frozen-lockfile + + - name: Biome check adminfront + run: | + cd adminfront + npx biome check . --formatter-enabled=false --organize-imports-enabled=false + npx biome check . --linter-enabled=false --organize-imports-enabled=false + + - name: Install devfront dependencies + run: | + cd devfront + npx pnpm install -C ../common --no-frozen-lockfile + + - name: Biome check devfront + run: | + cd devfront + npx biome check . --formatter-enabled=false --organize-imports-enabled=false + npx biome check . --linter-enabled=false --organize-imports-enabled=false + + - name: Install orgfront dependencies + run: | + cd orgfront + npx pnpm install -C ../common --no-frozen-lockfile + + - name: Biome check orgfront + run: | + cd orgfront + npx biome check . --formatter-enabled=false --organize-imports-enabled=false + npx biome check . --linter-enabled=false --organize-imports-enabled=false + backend-tests: needs: lint if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.run_backend_tests == true) }} @@ -570,6 +625,159 @@ jobs: userfront-e2e/test-results if-no-files-found: ignore + front-vitest-coverage: + needs: lint + if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.run_front_coverage == true) }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Install front workspace dependencies + run: | + mkdir -p reports + set +e + npm install -g pnpm + cd common + pnpm install --no-frozen-lockfile --shamefully-hoist 2>&1 | tee ../reports/front-coverage-install.log + install_exit_code=${PIPESTATUS[0]} + cd .. + set -e + + if [ "$install_exit_code" -ne 0 ]; then + { + echo "# Front Vitest Coverage Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`front-vitest-coverage\`" + echo "- Reason: \`Dependency install failed\`" + echo "- Exit Code: \`$install_exit_code\`" + echo + echo "## Command" + echo "\`cd common && pnpm install --no-frozen-lockfile --shamefully-hoist\`" + echo + echo "## Install Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/front-coverage-install.log + echo '```' + } > reports/front-vitest-coverage-failure-report.md + exit 1 + fi + + - name: Run adminfront Vitest coverage + run: | + set +e + cd adminfront + pnpm run test:coverage 2>&1 | tee ../reports/adminfront-vitest-coverage.log + test_exit_code=${PIPESTATUS[0]} + cd .. + set -e + + if [ "$test_exit_code" -ne 0 ]; then + { + echo "# Front Vitest Coverage Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`front-vitest-coverage\`" + echo "- Package: \`adminfront\`" + echo "- Exit Code: \`$test_exit_code\`" + echo + echo "## Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/adminfront-vitest-coverage.log + echo '```' + } > reports/front-vitest-coverage-failure-report.md + exit 1 + fi + + - name: Run devfront Vitest coverage + run: | + set +e + cd devfront + pnpm run test:coverage 2>&1 | tee ../reports/devfront-vitest-coverage.log + test_exit_code=${PIPESTATUS[0]} + cd .. + set -e + + if [ "$test_exit_code" -ne 0 ]; then + { + echo "# Front Vitest Coverage Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`front-vitest-coverage\`" + echo "- Package: \`devfront\`" + echo "- Exit Code: \`$test_exit_code\`" + echo + echo "## Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/devfront-vitest-coverage.log + echo '```' + } > reports/front-vitest-coverage-failure-report.md + exit 1 + fi + + - name: Run orgfront Vitest coverage + run: | + set +e + cd orgfront + pnpm run test:coverage 2>&1 | tee ../reports/orgfront-vitest-coverage.log + test_exit_code=${PIPESTATUS[0]} + cd .. + set -e + + if [ "$test_exit_code" -ne 0 ]; then + { + echo "# Front Vitest Coverage Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`front-vitest-coverage\`" + echo "- Package: \`orgfront\`" + echo "- Exit Code: \`$test_exit_code\`" + echo + echo "## Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/orgfront-vitest-coverage.log + echo '```' + } > reports/front-vitest-coverage-failure-report.md + exit 1 + fi + + - name: Generate Vitest coverage summary + run: | + node scripts/summarize_vitest_coverage.mjs + cat reports/vitest-coverage-summary.md >> "$GITHUB_STEP_SUMMARY" + + - name: Publish front Vitest coverage failure summary + if: ${{ failure() }} + run: | + if [ -f reports/front-vitest-coverage-failure-report.md ]; then + cat reports/front-vitest-coverage-failure-report.md >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload front Vitest coverage report artifact + if: ${{ always() }} + uses: actions/upload-artifact@v3 + continue-on-error: true + with: + name: front-vitest-coverage-report + path: | + reports/vitest-coverage-summary.md + reports/vitest-coverage-summary.json + reports/front-vitest-coverage-failure-report.md + reports/front-coverage-install.log + reports/adminfront-vitest-coverage.log + reports/devfront-vitest-coverage.log + reports/orgfront-vitest-coverage.log + adminfront/coverage + devfront/coverage + orgfront/coverage + if-no-files-found: ignore + adminfront-tests: needs: lint if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.run_adminfront_tests == true) }} @@ -1021,3 +1229,63 @@ jobs: orgfront/playwright-report orgfront/test-results if-no-files-found: ignore + + badge-updater: + needs: + - lint + - biome-check + - backend-tests + - userfront-tests + - userfront-e2e-tests + - front-vitest-coverage + - adminfront-tests + - devfront-tests + - orgfront-tests + if: ${{ always() && github.event_name != 'pull_request' && github.ref == 'refs/heads/dev' }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Download Vitest coverage report artifact + uses: actions/download-artifact@v3 + continue-on-error: true + with: + name: front-vitest-coverage-report + path: badge-artifacts/front-vitest-coverage-report + + - name: Update badge files + env: + LINT_RESULT: ${{ needs.lint.result }} + BIOME_RESULT: ${{ needs['biome-check'].result }} + BACKEND_RESULT: ${{ needs['backend-tests'].result }} + USERFRONT_RESULT: ${{ needs['userfront-tests'].result }} + USERFRONT_E2E_RESULT: ${{ needs['userfront-e2e-tests'].result }} + USERFRONT_E2E_FULL: ${{ github.event_name == 'workflow_dispatch' && inputs.run_userfront_e2e_full == true }} + COVERAGE_RESULT: ${{ needs['front-vitest-coverage'].result }} + ADMINFRONT_RESULT: ${{ needs['adminfront-tests'].result }} + DEVFRONT_RESULT: ${{ needs['devfront-tests'].result }} + ORGFRONT_RESULT: ${{ needs['orgfront-tests'].result }} + run: | + node scripts/update_code_check_badges.mjs + cat docs/badges/badges.json + + - name: Commit badge updates + run: | + if [ -z "$(git status --porcelain docs/badges)" ]; then + echo "No badge changes." + exit 0 + fi + + git config user.name "gitea-actions" + git config user.email "gitea-actions@hmac.kr" + git add docs/badges + git commit -m "chore: update code check badges [skip ci]" + git push diff --git a/.gitignore b/.gitignore index f4f54941..5bb72c22 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,9 @@ orgfront/test-results/ adminfront/playwright-report/ devfront/playwright-report/ orgfront/playwright-report/ +adminfront/coverage/ +devfront/coverage/ +orgfront/coverage/ orgfront/node_modules/ orgfront/dist/ orgfront/.vite/ diff --git a/README.md b/README.md index 7922dc66..bc6f43fa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ # Baron SSO +[![Code Check](https://gitea.hmac.kr/baron/baron-sso/raw/branch/dev/docs/badges/code-check.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev) +[![Biome](https://gitea.hmac.kr/baron/baron-sso/raw/branch/dev/docs/badges/biome.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev) +[![userfront e2e fast](https://gitea.hmac.kr/baron/baron-sso/raw/branch/dev/docs/badges/userfront-e2e-fast.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev) +[![userfront e2e full](https://gitea.hmac.kr/baron/baron-sso/raw/branch/dev/docs/badges/userfront-e2e-full.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev) +[![adminfront coverage](https://gitea.hmac.kr/baron/baron-sso/raw/branch/dev/docs/badges/adminfront-coverage.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev) +[![devfront coverage](https://gitea.hmac.kr/baron/baron-sso/raw/branch/dev/docs/badges/devfront-coverage.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev) +[![orgfront coverage](https://gitea.hmac.kr/baron/baron-sso/raw/branch/dev/docs/badges/orgfront-coverage.svg)](https://gitea.hmac.kr/baron/baron-sso/actions/workflows/code_check.yml?branch=dev) + +badge는 `Code Check`가 dev 브랜치에서 갱신합니다. 최신 HTML/LCOV/JSON summary는 Gitea `Code Check`의 `front-vitest-coverage-report` artifact에서 확인할 수 있습니다. + **Baron 로그인**은 화이트 라벨링된 가족사의 모든 소프트웨어 Auth를 총괄하는 사용자 인증/인가 허브입니다. ## 📂 프로젝트 구조 (Project Structure) diff --git a/adminfront/biome.json b/adminfront/biome.json index fb68b4da..66e0edd1 100644 --- a/adminfront/biome.json +++ b/adminfront/biome.json @@ -1,6 +1,4 @@ { - "extends": ["../common/config/biome.base.json"], - "files": { - "ignore": [".vite"] - } + "root": true, + "extends": ["../common/config/biome.base.json"] } diff --git a/adminfront/package-lock.json b/adminfront/package-lock.json index 61eddb69..24178a8f 100644 --- a/adminfront/package-lock.json +++ b/adminfront/package-lock.json @@ -32,6 +32,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@biomejs/biome": "2.4.16", "@playwright/test": "^1.60.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", @@ -41,13 +42,15 @@ "@types/react-dom": "^19.2.3", "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "4.1.6", "autoprefixer": "^10.5.0", "jsdom": "^28.1.0", + "playwright": "1.60.0", "postcss": "^8.5.14", "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "typescript": "^6.0.3", - "vite": "^8.0.12", + "vite": "^8.0.14", "vitest": "^4.1.6" }, "engines": { @@ -130,14 +133,14 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -145,17 +148,42 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", @@ -166,6 +194,205 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz", + "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.16", + "@biomejs/cli-darwin-x64": "2.4.16", + "@biomejs/cli-linux-arm64": "2.4.16", + "@biomejs/cli-linux-arm64-musl": "2.4.16", + "@biomejs/cli-linux-x64": "2.4.16", + "@biomejs/cli-linux-x64-musl": "2.4.16", + "@biomejs/cli-win32-arm64": "2.4.16", + "@biomejs/cli-win32-x64": "2.4.16" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz", + "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz", + "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz", + "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz", + "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz", + "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz", + "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz", + "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz", + "integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -506,9 +733,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.130.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", - "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", "dev": true, "license": "MIT", "funding": { @@ -2002,9 +2229,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", - "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", "cpu": [ "arm64" ], @@ -2019,9 +2246,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", - "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", "cpu": [ "arm64" ], @@ -2036,9 +2263,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", - "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", "cpu": [ "x64" ], @@ -2053,9 +2280,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", - "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", "cpu": [ "x64" ], @@ -2070,9 +2297,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", - "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", "cpu": [ "arm" ], @@ -2087,13 +2314,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", - "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2104,13 +2334,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", - "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2121,13 +2354,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", - "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2138,13 +2374,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", - "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2155,13 +2394,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", - "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2172,13 +2414,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", - "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2189,9 +2434,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", - "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", "cpu": [ "arm64" ], @@ -2206,9 +2451,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", - "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", "cpu": [ "wasm32" ], @@ -2225,9 +2470,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", - "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", "cpu": [ "arm64" ], @@ -2242,9 +2487,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", - "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", "cpu": [ "x64" ], @@ -2572,6 +2817,37 @@ } } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.6.tgz", + "integrity": "sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.6", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.6", + "vitest": "4.1.6" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", @@ -2782,6 +3058,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.2.tgz", + "integrity": "sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3541,6 +3836,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -3593,6 +3898,13 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -3709,6 +4021,45 @@ "dev": true, "license": "MIT" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -4122,6 +4473,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4390,9 +4769,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -4410,7 +4789,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4854,13 +5233,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", - "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.130.0", + "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -4870,21 +5249,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.1", - "@rolldown/binding-darwin-arm64": "1.0.1", - "@rolldown/binding-darwin-x64": "1.0.1", - "@rolldown/binding-freebsd-x64": "1.0.1", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", - "@rolldown/binding-linux-arm64-gnu": "1.0.1", - "@rolldown/binding-linux-arm64-musl": "1.0.1", - "@rolldown/binding-linux-ppc64-gnu": "1.0.1", - "@rolldown/binding-linux-s390x-gnu": "1.0.1", - "@rolldown/binding-linux-x64-gnu": "1.0.1", - "@rolldown/binding-linux-x64-musl": "1.0.1", - "@rolldown/binding-openharmony-arm64": "1.0.1", - "@rolldown/binding-wasm32-wasi": "1.0.1", - "@rolldown/binding-win32-arm64-msvc": "1.0.1", - "@rolldown/binding-win32-x64-msvc": "1.0.1" + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" } }, "node_modules/run-parallel": { @@ -4930,6 +5309,19 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -5003,6 +5395,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -5373,16 +5778,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.13", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", - "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "rolldown": "1.0.1", + "postcss": "^8.5.15", + "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "bin": { diff --git a/adminfront/package.json b/adminfront/package.json index 2ab225b3..ea962d3e 100644 --- a/adminfront/package.json +++ b/adminfront/package.json @@ -14,6 +14,7 @@ "format": "biome format . --write", "preview": "vite preview", "test": "playwright test", + "test:coverage": "vitest run --coverage", "test:unit": "vitest run", "test:ui": "playwright test --ui", "i18n-scan": "cd .. && node tools/i18n-scanner/index.js && node tools/i18n-scanner/report.js" @@ -43,6 +44,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@biomejs/biome": "2.4.16", "@playwright/test": "^1.60.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", @@ -52,8 +54,10 @@ "@types/react-dom": "^19.2.3", "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "4.1.6", "autoprefixer": "^10.5.0", "jsdom": "^28.1.0", + "playwright": "1.60.0", "postcss": "^8.5.14", "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", diff --git a/adminfront/pnpm-lock.yaml b/adminfront/pnpm-lock.yaml index d706b86e..f123b956 100644 --- a/adminfront/pnpm-lock.yaml +++ b/adminfront/pnpm-lock.yaml @@ -75,6 +75,9 @@ importers: specifier: ^4.4.3 version: 4.4.3 devDependencies: + '@biomejs/biome': + specifier: 2.4.16 + version: 2.4.16 '@playwright/test': specifier: ^1.60.0 version: 1.60.0 @@ -108,6 +111,9 @@ importers: jsdom: specifier: ^28.1.0 version: 28.1.0 + playwright: + specifier: 1.60.0 + version: 1.60.0 postcss: specifier: ^8.5.14 version: 8.5.14 @@ -165,6 +171,63 @@ packages: resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} + '@biomejs/biome@2.4.16': + resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.16': + resolution: {integrity: sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.16': + resolution: {integrity: sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.16': + resolution: {integrity: sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.16': + resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.16': + resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.16': + resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.16': + resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.16': + resolution: {integrity: sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@bramus/specificity@2.4.2': resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true @@ -1936,6 +1999,41 @@ snapshots: '@babel/runtime@7.29.2': {} + '@biomejs/biome@2.4.16': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.16 + '@biomejs/cli-darwin-x64': 2.4.16 + '@biomejs/cli-linux-arm64': 2.4.16 + '@biomejs/cli-linux-arm64-musl': 2.4.16 + '@biomejs/cli-linux-x64': 2.4.16 + '@biomejs/cli-linux-x64-musl': 2.4.16 + '@biomejs/cli-win32-arm64': 2.4.16 + '@biomejs/cli-win32-x64': 2.4.16 + + '@biomejs/cli-darwin-arm64@2.4.16': + optional: true + + '@biomejs/cli-darwin-x64@2.4.16': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.16': + optional: true + + '@biomejs/cli-linux-arm64@2.4.16': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.16': + optional: true + + '@biomejs/cli-linux-x64@2.4.16': + optional: true + + '@biomejs/cli-win32-arm64@2.4.16': + optional: true + + '@biomejs/cli-win32-x64@2.4.16': + optional: true + '@bramus/specificity@2.4.2': dependencies: css-tree: 3.2.1 diff --git a/adminfront/pnpm-workspace.yaml b/adminfront/pnpm-workspace.yaml index 49c0ad74..59f9a7d3 100644 --- a/adminfront/pnpm-workspace.yaml +++ b/adminfront/pnpm-workspace.yaml @@ -1,2 +1,3 @@ allowBuilds: + '@biomejs/biome': true esbuild: false diff --git a/adminfront/scripts/serve-prod.mjs b/adminfront/scripts/serve-prod.mjs index 804a7052..a69d7bdb 100644 --- a/adminfront/scripts/serve-prod.mjs +++ b/adminfront/scripts/serve-prod.mjs @@ -1,5 +1,5 @@ -import { createServer } from "node:http"; import { readFile, stat } from "node:fs/promises"; +import { createServer } from "node:http"; import { extname, join, normalize, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -24,7 +24,9 @@ const contentTypes = { }; function getContentType(filePath) { - return contentTypes[extname(filePath).toLowerCase()] ?? "application/octet-stream"; + return ( + contentTypes[extname(filePath).toLowerCase()] ?? "application/octet-stream" + ); } function sendJson(res, statusCode, body) { @@ -132,7 +134,10 @@ async function serveStatic(req, res, pathname) { createServer(async (req, res) => { try { - const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); + const url = new URL( + req.url ?? "/", + `http://${req.headers.host ?? "localhost"}`, + ); const { pathname, search } = url; if (pathname === "/api" || pathname.startsWith("/api/")) { @@ -149,5 +154,7 @@ createServer(async (req, res) => { }); } }).listen(port, host, () => { - console.log(`Adminfront production server listening on http://${host}:${port}`); + console.log( + `Adminfront production server listening on http://${host}:${port}`, + ); }); diff --git a/adminfront/src/app/routes.tsx b/adminfront/src/app/routes.tsx index 7c17cf9e..5e231dbc 100644 --- a/adminfront/src/app/routes.tsx +++ b/adminfront/src/app/routes.tsx @@ -1,5 +1,5 @@ -import { createBrowserRouter } from "react-router-dom"; import type { RouteObject } from "react-router-dom"; +import { createBrowserRouter } from "react-router-dom"; import AppLayout from "../components/layout/AppLayout"; import ApiKeyCreatePage from "../features/api-keys/ApiKeyCreatePage"; import ApiKeyListPage from "../features/api-keys/ApiKeyListPage"; diff --git a/adminfront/src/components/common/LanguageSelector.test.tsx b/adminfront/src/components/common/LanguageSelector.test.tsx index 0d505b96..d49d6546 100644 --- a/adminfront/src/components/common/LanguageSelector.test.tsx +++ b/adminfront/src/components/common/LanguageSelector.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import LanguageSelector from "./LanguageSelector"; diff --git a/adminfront/src/components/common/LanguageSelector.tsx b/adminfront/src/components/common/LanguageSelector.tsx index b4e33c67..c1716ef4 100644 --- a/adminfront/src/components/common/LanguageSelector.tsx +++ b/adminfront/src/components/common/LanguageSelector.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { LOCALE_STORAGE_KEY } from "../../../../common/core/i18n"; import { t } from "../../lib/i18n"; + const SUPPORTED_LOCALES = ["ko", "en"] as const; type Locale = (typeof SUPPORTED_LOCALES)[number]; diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx index e50e6f9d..2013f0e0 100644 --- a/adminfront/src/components/layout/AppLayout.tsx +++ b/adminfront/src/components/layout/AppLayout.tsx @@ -22,13 +22,13 @@ import { useAuth } from "react-oidc-context"; import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { AppSidebar, - type ShellSidebarNavItem, - type ShellTranslator, applyShellTheme, buildShellProfileSummary, buildShellSessionStatus, readShellSessionExpiryEnabled, readShellTheme, + type ShellSidebarNavItem, + type ShellTranslator, shellLayoutClasses, writeShellSessionExpiryEnabled, } from "../../../../common/shell"; @@ -310,13 +310,16 @@ function AppLayout() { window.addEventListener(DEV_ROLE_CHANGED_EVENT, rerenderDevelopmentShell); return () => { - window.removeEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell); + window.removeEventListener( + LOCALE_CHANGED_EVENT, + rerenderDevelopmentShell, + ); window.removeEventListener( DEV_ROLE_CHANGED_EVENT, rerenderDevelopmentShell, ); }; - }, [isDevelopmentRuntime]); + }, []); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -429,7 +432,6 @@ function AppLayout() { auth.isAuthenticated, auth.isLoading, auth.user?.expires_at, - isDevelopmentRuntime, isSessionExpiryEnabled, ]); @@ -668,7 +670,10 @@ function AppLayout() {

- {t("ui.shell.session.auto_extend", "세션 만료 관리")} + {t( + "ui.shell.session.auto_extend", + "세션 만료 관리", + )}

{isSessionExpiryEnabled ? ( @@ -677,7 +682,10 @@ function AppLayout() { t={t} /> ) : ( - t("ui.shell.session.disabled", "세션 만료 비활성화") + t( + "ui.shell.session.disabled", + "세션 만료 비활성화", + ) )}

diff --git a/adminfront/src/components/ui/avatar.tsx b/adminfront/src/components/ui/avatar.tsx index 23e88913..ab415563 100644 --- a/adminfront/src/components/ui/avatar.tsx +++ b/adminfront/src/components/ui/avatar.tsx @@ -44,4 +44,4 @@ const AvatarFallback = React.forwardRef< )); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; -export { Avatar, AvatarImage, AvatarFallback }; +export { Avatar, AvatarFallback, AvatarImage }; diff --git a/adminfront/src/components/ui/card.tsx b/adminfront/src/components/ui/card.tsx index 33685c34..246b5289 100644 --- a/adminfront/src/components/ui/card.tsx +++ b/adminfront/src/components/ui/card.tsx @@ -50,9 +50,9 @@ function CardFooter({ export { Card, + CardContent, + CardDescription, + CardFooter, CardHeader, CardTitle, - CardDescription, - CardContent, - CardFooter, }; diff --git a/adminfront/src/components/ui/dialog.tsx b/adminfront/src/components/ui/dialog.tsx index 69d9e2a2..b1a70ec6 100644 --- a/adminfront/src/components/ui/dialog.tsx +++ b/adminfront/src/components/ui/dialog.tsx @@ -144,18 +144,20 @@ const DialogClose = React.forwardRef( DialogClose.displayName = "DialogClose"; const DialogOverlay = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes + HTMLButtonElement, + React.ButtonHTMLAttributes >(({ className, onMouseDown, ...props }, ref) => { const { setOpen } = useDialogContext("DialogOverlay"); return ( -
{ if (event.target === event.currentTarget) { setOpen(false); @@ -273,13 +275,13 @@ DialogDescription.displayName = "DialogDescription"; export { Dialog, - DialogPortal, - DialogOverlay, DialogClose, - DialogTrigger, DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, }; diff --git a/adminfront/src/components/ui/dropdown-menu.tsx b/adminfront/src/components/ui/dropdown-menu.tsx index c58f322e..bde62785 100644 --- a/adminfront/src/components/ui/dropdown-menu.tsx +++ b/adminfront/src/components/ui/dropdown-menu.tsx @@ -183,18 +183,18 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; export { DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, DropdownMenuCheckboxItem, - DropdownMenuRadioItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, - DropdownMenuRadioGroup, + DropdownMenuTrigger, }; diff --git a/adminfront/src/components/ui/select.tsx b/adminfront/src/components/ui/select.tsx index 696a5fa7..abd90f46 100644 --- a/adminfront/src/components/ui/select.tsx +++ b/adminfront/src/components/ui/select.tsx @@ -146,13 +146,13 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName; export { Select, - SelectGroup, - SelectValue, - SelectTrigger, SelectContent, - SelectLabel, + SelectGroup, SelectItem, - SelectSeparator, - SelectScrollUpButton, + SelectLabel, SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, }; diff --git a/adminfront/src/components/ui/table.tsx b/adminfront/src/components/ui/table.tsx index 0b0022a5..33663b2e 100644 --- a/adminfront/src/components/ui/table.tsx +++ b/adminfront/src/components/ui/table.tsx @@ -92,11 +92,11 @@ TableCaption.displayName = "TableCaption"; export { Table, - TableHeader, TableBody, + TableCaption, + TableCell, TableFooter, TableHead, + TableHeader, TableRow, - TableCell, - TableCaption, }; diff --git a/adminfront/src/components/ui/tabs.tsx b/adminfront/src/components/ui/tabs.tsx index 71364fda..15646582 100644 --- a/adminfront/src/components/ui/tabs.tsx +++ b/adminfront/src/components/ui/tabs.tsx @@ -84,4 +84,4 @@ const TabsContent = React.forwardRef< }); TabsContent.displayName = "TabsContent"; -export { Tabs, TabsList, TabsTrigger, TabsContent }; +export { Tabs, TabsContent, TabsList, TabsTrigger }; diff --git a/adminfront/src/features/auth/AuthPage.test.tsx b/adminfront/src/features/auth/AuthPage.test.tsx index 5d13b0eb..032a122d 100644 --- a/adminfront/src/features/auth/AuthPage.test.tsx +++ b/adminfront/src/features/auth/AuthPage.test.tsx @@ -31,6 +31,8 @@ describe("AuthPage", () => { expect(screen.getByText("Auth Guard")).toBeInTheDocument(); expect(screen.getByText("ReBAC permission checker")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Check permission" })).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Check permission" }), + ).toBeInTheDocument(); }); }); diff --git a/adminfront/src/features/auth/LoginPage.test.tsx b/adminfront/src/features/auth/LoginPage.test.tsx index 1b7db6a7..06ef6eeb 100644 --- a/adminfront/src/features/auth/LoginPage.test.tsx +++ b/adminfront/src/features/auth/LoginPage.test.tsx @@ -42,7 +42,9 @@ describe("LoginPage", () => { it("shows an actionable error instead of starting PKCE when WebCrypto is unavailable", async () => { renderLoginPage("/login?returnTo=%2F"); - await userEvent.click(screen.getByRole("button", { name: /SSO 계정으로 로그인/i })); + await userEvent.click( + screen.getByRole("button", { name: /SSO 계정으로 로그인/i }), + ); expect(mockSigninRedirect).not.toHaveBeenCalled(); expect(screen.getByRole("alert")).toHaveTextContent( @@ -61,7 +63,9 @@ describe("LoginPage", () => { }); renderLoginPage("/login?returnTo=%2Fusers%3Fpage%3D2"); - await userEvent.click(screen.getByRole("button", { name: /SSO 계정으로 로그인/i })); + await userEvent.click( + screen.getByRole("button", { name: /SSO 계정으로 로그인/i }), + ); expect(mockSigninRedirect).toHaveBeenCalledWith({ state: { diff --git a/adminfront/src/features/auth/components/PermissionChecker.tsx b/adminfront/src/features/auth/components/PermissionChecker.tsx index eb7e9511..f94544da 100644 --- a/adminfront/src/features/auth/components/PermissionChecker.tsx +++ b/adminfront/src/features/auth/components/PermissionChecker.tsx @@ -48,10 +48,7 @@ function PermissionChecker() { - {t( - "ui.admin.auth_guard.checker.title", - "ReBAC permission checker", - )} + {t("ui.admin.auth_guard.checker.title", "ReBAC permission checker")} {t( @@ -92,7 +89,9 @@ function PermissionChecker() {
- +
- +
- {t( - "ui.admin.auth_guard.checker.allowed", - "Access ALLOWED", - )} + {t("ui.admin.auth_guard.checker.allowed", "Access ALLOWED")}

{t( @@ -171,10 +166,7 @@ function PermissionChecker() { <>

- {t( - "ui.admin.auth_guard.checker.denied", - "Access DENIED", - )} + {t("ui.admin.auth_guard.checker.denied", "Access DENIED")}

{t( diff --git a/adminfront/src/features/integrity/DataIntegrityPage.test.tsx b/adminfront/src/features/integrity/DataIntegrityPage.test.tsx index f6c31bed..06a138d4 100644 --- a/adminfront/src/features/integrity/DataIntegrityPage.test.tsx +++ b/adminfront/src/features/integrity/DataIntegrityPage.test.tsx @@ -175,16 +175,16 @@ describe("DataIntegrityPage", () => { window.localStorage.setItem("locale", "en"); renderPage(); - expect( - await screen.findByText("Data Integrity Check"), - ).toBeInTheDocument(); + expect(await screen.findByText("Data Integrity Check")).toBeInTheDocument(); expect( await screen.findByText( "Review integrity status and inspect checks across the admin data model.", ), ).toBeInTheDocument(); expect(await screen.findByText("Tenant integrity")).toBeInTheDocument(); - expect(await screen.findByText("Duplicate tenant slug")).toBeInTheDocument(); + expect( + await screen.findByText("Duplicate tenant slug"), + ).toBeInTheDocument(); expect( await screen.findByText( "Checks duplicate active tenant slugs using LOWER(TRIM(slug)).", diff --git a/adminfront/src/features/integrity/DataIntegrityPage.tsx b/adminfront/src/features/integrity/DataIntegrityPage.tsx index 7f449d28..1d4ca4ef 100644 --- a/adminfront/src/features/integrity/DataIntegrityPage.tsx +++ b/adminfront/src/features/integrity/DataIntegrityPage.tsx @@ -12,10 +12,10 @@ import { Button } from "../../components/ui/button"; import { type DataIntegrityCheck, type DataIntegrityStatus, - type OrphanUserLoginID, deleteOrphanUserLoginIDs, fetchDataIntegrityReport, fetchOrphanUserLoginIDs, + type OrphanUserLoginID, } from "../../lib/adminApi"; import { t } from "../../lib/i18n"; import { getAdminDateLocale } from "../../lib/locale"; diff --git a/adminfront/src/features/overview/GlobalOverviewPage.tsx b/adminfront/src/features/overview/GlobalOverviewPage.tsx index 1f2488a7..90c12801 100644 --- a/adminfront/src/features/overview/GlobalOverviewPage.tsx +++ b/adminfront/src/features/overview/GlobalOverviewPage.tsx @@ -17,13 +17,13 @@ import { import { RoleGuard } from "../../components/auth/RoleGuard"; import { type DataIntegrityStatus, - type RPUsageDailyMetric, - type RPUsagePeriod, - type TenantSummary, fetchAdminOverviewStats, fetchAdminRPUsageDaily, fetchAllTenants, fetchDataIntegrityReport, + type RPUsageDailyMetric, + type RPUsagePeriod, + type TenantSummary, } from "../../lib/adminApi"; import { t } from "../../lib/i18n"; @@ -203,10 +203,7 @@ function IntegrityOverviewSummary() { )}

- {t( - "ui.admin.integrity.summary.title", - "정합성 최종 검증", - )} + {t("ui.admin.integrity.summary.title", "정합성 최종 검증")}

@@ -466,7 +463,7 @@ function GlobalOverviewPage() { const metric = (value: number | undefined) => value === undefined ? "-" : value.toLocaleString(); const periodControls = ( -
+
{[ ["day", t("ui.common.chart.period.day", "일")], ["week", t("ui.common.chart.period.week", "주")], @@ -486,7 +483,7 @@ function GlobalOverviewPage() { {label} ))} -
+ ); const chartFilters = (
diff --git a/adminfront/src/features/projections/UserProjectionPage.test.tsx b/adminfront/src/features/projections/UserProjectionPage.test.tsx index efe19903..2ec44387 100644 --- a/adminfront/src/features/projections/UserProjectionPage.test.tsx +++ b/adminfront/src/features/projections/UserProjectionPage.test.tsx @@ -61,17 +61,13 @@ describe("UserProjectionPage", () => { it("renders projection status for super_admin", async () => { renderPage(); - expect( - await screen.findByText("사용자 동기화 관리"), - ).toBeInTheDocument(); + expect(await screen.findByText("사용자 동기화 관리")).toBeInTheDocument(); expect( await screen.findByText( "Kratos 사용자 read model을 확인하고 동기화 상태를 갱신합니다.", ), ).toBeInTheDocument(); - expect( - await screen.findByText("Kratos 사용자 동기화"), - ).toBeInTheDocument(); + expect(await screen.findByText("Kratos 사용자 동기화")).toBeInTheDocument(); expect(screen.getByText("준비됨")).toBeInTheDocument(); expect(screen.getByText("152")).toBeInTheDocument(); expect(fetchUserProjectionStatus).toHaveBeenCalled(); @@ -100,9 +96,7 @@ describe("UserProjectionPage", () => { renderPage(); expect(await screen.findByText("접근 권한이 없습니다")).toBeInTheDocument(); - expect( - screen.queryByText("사용자 동기화 관리"), - ).not.toBeInTheDocument(); + expect(screen.queryByText("사용자 동기화 관리")).not.toBeInTheDocument(); expect(fetchUserProjectionStatus).not.toHaveBeenCalled(); }); diff --git a/adminfront/src/features/projections/UserProjectionPage.tsx b/adminfront/src/features/projections/UserProjectionPage.tsx index 85e3bf86..9ec98255 100644 --- a/adminfront/src/features/projections/UserProjectionPage.tsx +++ b/adminfront/src/features/projections/UserProjectionPage.tsx @@ -133,10 +133,7 @@ function UserProjectionContent() { disabled={isWorking} > - {t( - "ui.admin.user_projection.actions.reset", - "Reset and rebuild", - )} + {t("ui.admin.user_projection.actions.reset", "Reset and rebuild")}
@@ -230,10 +227,7 @@ function UserProjectionContent() {
- {t( - "ui.admin.user_projection.summary.updated_at", - "Updated at", - )} + {t("ui.admin.user_projection.summary.updated_at", "Updated at")}
{formatDateTime(data?.updatedAt)} @@ -258,22 +252,19 @@ export default function UserProjectionPage() { -
-

- {t( - "ui.admin.user_projection.forbidden.title", - "Access denied", - )} -

-

- {t( - "msg.admin.user_projection.forbidden.description", - "This screen is only available to super_admin users.", - )} -

-
- +
+
+

+ {t("ui.admin.user_projection.forbidden.title", "Access denied")} +

+

+ {t( + "msg.admin.user_projection.forbidden.description", + "This screen is only available to super_admin users.", + )} +

+
+
} > diff --git a/adminfront/src/features/tenants/components/ParentTenantSelector.tsx b/adminfront/src/features/tenants/components/ParentTenantSelector.tsx index 4f73f0ed..bac4fe1c 100644 --- a/adminfront/src/features/tenants/components/ParentTenantSelector.tsx +++ b/adminfront/src/features/tenants/components/ParentTenantSelector.tsx @@ -7,8 +7,8 @@ import { DialogContent, DialogDescription, DialogHeader, - DialogTrigger, DialogTitle, + DialogTrigger, } from "../../../components/ui/dialog"; import { Label } from "../../../components/ui/label"; import type { TenantSummary } from "../../../lib/adminApi"; diff --git a/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx b/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx index 69a2782b..6dc881a9 100644 --- a/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx +++ b/adminfront/src/features/tenants/routes/TenantAdminsAndOwnersTab.tsx @@ -41,7 +41,6 @@ import { } from "../../../components/ui/table"; import { toast } from "../../../components/ui/use-toast"; import { - type TenantAdmin, addTenantAdmin, addTenantOwner, fetchTenantAdmins, @@ -49,6 +48,7 @@ import { fetchUsers, removeTenantAdmin, removeTenantOwner, + type TenantAdmin, } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; @@ -69,15 +69,14 @@ export function TenantAdminsAndOwnersTab() { const auth = useAuth(); const navigate = useNavigate(); const currentUserId = auth.user?.profile.sub; - const { tenantId } = useParams<{ tenantId: string }>(); + const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>(); + const tenantId = tenantIdParam ?? ""; const queryClient = useQueryClient(); const [searchTerm, setSearchTerm] = useState(""); const [dialogMode, setDialogMode] = useState(null); const [pendingOwners, setPendingOwners] = useState([]); const [pendingAdmins, setPendingAdmins] = useState([]); - if (!tenantId) return null; - const ownersQuery = useQuery({ queryKey: ["tenant-owners", tenantId], queryFn: () => fetchTenantOwners(tenantId), @@ -339,6 +338,8 @@ export function TenantAdminsAndOwnersTab() { } }; + if (!tenantId) return null; + const serverOwners = ownersQuery.data || []; const serverAdmins = adminsQuery.data || []; const currentOwners = mergePendingMembers(serverOwners, pendingOwners); diff --git a/adminfront/src/features/tenants/routes/TenantCreatePage.tsx b/adminfront/src/features/tenants/routes/TenantCreatePage.tsx index e85e38cd..2fca4b48 100644 --- a/adminfront/src/features/tenants/routes/TenantCreatePage.tsx +++ b/adminfront/src/features/tenants/routes/TenantCreatePage.tsx @@ -19,15 +19,15 @@ import { t } from "../../../lib/i18n"; import { DomainTagInput } from "../components/DomainTagInput"; import { ParentTenantSelector } from "../components/ParentTenantSelector"; import { - type ServerDomainConflict, formatDomainConflictMessage, + type ServerDomainConflict, } from "../utils/domainTags"; import { + mergeTenantOrgConfig, ORG_UNIT_TYPE_OPTIONS, + shouldAllowHanmacOrgConfig, TENANT_VISIBILITY_OPTIONS, type TenantVisibility, - mergeTenantOrgConfig, - shouldAllowHanmacOrgConfig, } from "../utils/orgConfig"; type AdminFrontTestHooks = { diff --git a/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx b/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx index e4d2d470..87d226d2 100644 --- a/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx @@ -53,13 +53,13 @@ import { } from "../../../components/ui/table"; import { toast } from "../../../components/ui/use-toast"; import { - type GroupSummary, addGroupMember, createGroup, deleteGroup, fetchGroups, fetchTenant, fetchUsers, + type GroupSummary, removeGroupMember, } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index 10b7a5ee..14a4b9c0 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -27,6 +27,7 @@ import { } from "lucide-react"; import * as React from "react"; import { Link, useNavigate } from "react-router-dom"; +import { PageHeader } from "../../../../../common/core/components/page"; import { SortableTableHead, sortableTableHeadBaseClassName, @@ -38,7 +39,6 @@ import { sortItems, toggleSort, } from "../../../../../common/core/utils"; -import { PageHeader } from "../../../../../common/core/components/page"; import { commonStickyTableHeaderClass, commonTableShellClass, @@ -92,18 +92,18 @@ import { import { toast } from "../../../components/ui/use-toast"; import type { UserProfileResponse } from "../../../lib/adminApi"; import { - type TenantSummary, deleteTenant, deleteTenantsBulk, exportTenantsCSV, fetchMe, fetchTenants, importTenantsCSV, + type TenantSummary, updateTenant, } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; import { normalizeAdminRole } from "../../../lib/roles"; -import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree"; +import { buildTenantFullTree, type TenantNode } from "../../../lib/tenantTree"; import { buildAuthenticatedOrgChartTenantPickerUrl, filterNonHanmacFamilyTenants, @@ -112,20 +112,20 @@ import { } from "../../users/orgChartPicker"; import { isSeedTenant } from "../utils/protectedTenants"; import { - type TenantImportPreviewRow, - type TenantImportResolution, buildTenantImportParentOptionGroups, buildTenantImportPreview, inferTenantImportRootParentSlug, parseTenantCSV, serializeTenantImportCSV, + type TenantImportPreviewRow, + type TenantImportResolution, } from "../utils/tenantCsvImport"; import { - type TenantViewMode, - type TenantViewRow, filterTenantsByScope, getTenantViewRows, resolveTenantSelectionIds, + type TenantViewMode, + type TenantViewRow, tenantMatchesListSearch, } from "./tenantListView"; @@ -453,30 +453,6 @@ function TenantListPage() { }, }); - if ( - profile && - profileRole !== "super_admin" && - profileRole !== "tenant_admin" - ) { - return ( -
-

- {t("msg.admin.common.forbidden", "접근 권한이 없습니다.")} -

- -
- ); - } - - if ( - profileRole === "tenant_admin" && - (profile?.manageableTenants?.length ?? 0) <= 1 - ) { - return null; - } - const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response ?.data?.error; const fallbackError = @@ -574,6 +550,30 @@ function TenantListPage() { return () => window.removeEventListener("message", onMessage); }, [allTenants, scopePickerOpen]); + if ( + profile && + profileRole !== "super_admin" && + profileRole !== "tenant_admin" + ) { + return ( +
+

+ {t("msg.admin.common.forbidden", "접근 권한이 없습니다.")} +

+ +
+ ); + } + + if ( + profileRole === "tenant_admin" && + (profile?.manageableTenants?.length ?? 0) <= 1 + ) { + return null; + } + const handleSelectAll = (checked: boolean) => { if (checked) { setSelectedIds(deletableTenants.map((t) => t.id)); diff --git a/adminfront/src/features/tenants/routes/TenantProfilePage.tsx b/adminfront/src/features/tenants/routes/TenantProfilePage.tsx index f06afcb4..3cb672c2 100644 --- a/adminfront/src/features/tenants/routes/TenantProfilePage.tsx +++ b/adminfront/src/features/tenants/routes/TenantProfilePage.tsx @@ -26,34 +26,30 @@ import { t } from "../../../lib/i18n"; import { DomainTagInput } from "../components/DomainTagInput"; import { ParentTenantSelector } from "../components/ParentTenantSelector"; import { - type ServerDomainConflict, formatDomainConflictMessage, + type ServerDomainConflict, } from "../utils/domainTags"; import { - ORG_UNIT_TYPE_OPTIONS, - TENANT_VISIBILITY_OPTIONS, - type TenantVisibility, mergeTenantOrgConfig, + ORG_UNIT_TYPE_OPTIONS, readTenantOrgConfig, removeTenantOrgConfig, shouldAllowHanmacOrgConfig, + TENANT_VISIBILITY_OPTIONS, + type TenantVisibility, } from "../utils/orgConfig"; import { isSeedTenant } from "../utils/protectedTenants"; export function TenantProfilePage() { - const { tenantId } = useParams<{ tenantId: string }>(); + const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>(); + const tenantId = tenantIdParam ?? ""; const navigate = useNavigate(); const queryClient = useQueryClient(); - if (!tenantId) { - return ( -
{t("msg.admin.tenants.missing_id", "테넌트 ID가 없습니다.")}
- ); - } - const tenantQuery = useQuery({ queryKey: ["tenant", tenantId], queryFn: () => fetchTenant(tenantId), + enabled: tenantId.length > 0, }); const parentQuery = useQuery({ @@ -197,6 +193,12 @@ export function TenantProfilePage() { ? isSeedTenant(tenantQuery.data) : false; + if (!tenantId) { + return ( +
{t("msg.admin.tenants.missing_id", "테넌트 ID가 없습니다.")}
+ ); + } + const handleDelete = () => { if (isProtectedSeedTenant) { return; diff --git a/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx b/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx index 93b47cc0..55fa65c6 100644 --- a/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantSchemaPage.tsx @@ -18,10 +18,10 @@ import { fetchMe, fetchTenant, updateTenant } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; import { normalizeAdminRole } from "../../../lib/roles"; import { - type SchemaField, createSchemaField, isSchemaFieldType, normalizeSchemaField, + type SchemaField, } from "./tenantSchemaFields"; export function TenantSchemaPage() { diff --git a/adminfront/src/features/tenants/routes/TenantWorksmobilePage.tsx b/adminfront/src/features/tenants/routes/TenantWorksmobilePage.tsx index a8af2063..a9002610 100644 --- a/adminfront/src/features/tenants/routes/TenantWorksmobilePage.tsx +++ b/adminfront/src/features/tenants/routes/TenantWorksmobilePage.tsx @@ -38,7 +38,6 @@ import { } from "../../../components/ui/table"; import { toast } from "../../../components/ui/use-toast"; import { - type WorksmobileComparisonItem, downloadWorksmobileInitialPasswordsCSV, enqueueWorksmobileBackfillDryRun, enqueueWorksmobileOrgUnitDelete, @@ -47,13 +46,10 @@ import { fetchWorksmobileComparison, fetchWorksmobileOverview, retryWorksmobileJob, + type WorksmobileComparisonItem, } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; import { - type WorksmobileComparisonColumnKey, - type WorksmobileComparisonColumnVisibility, - type WorksmobileComparisonFilter, - type WorksmobileComparisonSummary, buildWorksmobilePasswordManageUrl, canOpenWorksmobilePasswordManage, canSelectWorksmobileRow, @@ -71,6 +67,10 @@ import { getWorksmobileSelectedActionIds, getWorksmobileSelectedWorksOnlyOrgUnitIds, summarizeWorksmobileComparison, + type WorksmobileComparisonColumnKey, + type WorksmobileComparisonColumnVisibility, + type WorksmobileComparisonFilter, + type WorksmobileComparisonSummary, } from "./worksmobileComparison"; export function TenantWorksmobilePage() { @@ -1196,13 +1196,7 @@ function ComparisonTable({ ); } -function ComparisonDomainCell({ - name, - id, -}: { - name?: string; - id?: number; -}) { +function ComparisonDomainCell({ name, id }: { name?: string; id?: number }) { if (!name && !id) { return -; } diff --git a/adminfront/src/features/tenants/routes/tenantListView.ts b/adminfront/src/features/tenants/routes/tenantListView.ts index d5ebf895..b20bb36c 100644 --- a/adminfront/src/features/tenants/routes/tenantListView.ts +++ b/adminfront/src/features/tenants/routes/tenantListView.ts @@ -1,5 +1,5 @@ import type { TenantSummary } from "../../../lib/adminApi"; -import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree"; +import { buildTenantFullTree, type TenantNode } from "../../../lib/tenantTree"; export type TenantViewMode = "tree" | "table"; export type TenantViewRow = TenantNode & { depth: number }; diff --git a/adminfront/src/features/tenants/utils/orgConfig.test.ts b/adminfront/src/features/tenants/utils/orgConfig.test.ts index 35c0f457..7f210d15 100644 --- a/adminfront/src/features/tenants/utils/orgConfig.test.ts +++ b/adminfront/src/features/tenants/utils/orgConfig.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; import type { TenantSummary } from "../../../lib/adminApi"; import { - ORG_UNIT_TYPE_OPTIONS, mergeTenantOrgConfig, + ORG_UNIT_TYPE_OPTIONS, readTenantOrgConfig, shouldAllowHanmacOrgConfig, } from "./orgConfig"; diff --git a/adminfront/src/features/tenants/utils/tenantCsvImport.test.ts b/adminfront/src/features/tenants/utils/tenantCsvImport.test.ts index 42a282d0..f2b12103 100644 --- a/adminfront/src/features/tenants/utils/tenantCsvImport.test.ts +++ b/adminfront/src/features/tenants/utils/tenantCsvImport.test.ts @@ -150,7 +150,6 @@ describe("tenantCsvImport", () => { expect(csv).not.toContain("local-tenant-id"); }); - it("preserves source tenant_id when a create resolution does not override it", () => { const exportedTenantId = "11111111-2222-4333-8444-555555555555"; const rows = parseTenantCSV( diff --git a/adminfront/src/features/tenants/utils/tenantCsvImport.ts b/adminfront/src/features/tenants/utils/tenantCsvImport.ts index 133e1a1c..8f94b27d 100644 --- a/adminfront/src/features/tenants/utils/tenantCsvImport.ts +++ b/adminfront/src/features/tenants/utils/tenantCsvImport.ts @@ -403,7 +403,6 @@ function createTenantImportId() { .padEnd(12, "0")}`; } - function isUUIDLikeTenantId(value: string) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( value, @@ -596,7 +595,7 @@ function slugify(value: string) { 지원: "support", }; - let result = value.trim(); + const result = value.trim(); // 1. 전체 매칭 확인 if (commonMappings[result]) { diff --git a/adminfront/src/features/user-groups/routes/GlobalUserGroupListPage.tsx b/adminfront/src/features/user-groups/routes/GlobalUserGroupListPage.tsx index f408b9aa..a4533e21 100644 --- a/adminfront/src/features/user-groups/routes/GlobalUserGroupListPage.tsx +++ b/adminfront/src/features/user-groups/routes/GlobalUserGroupListPage.tsx @@ -21,9 +21,9 @@ import { TableRow, } from "../../../components/ui/table"; import { - type TenantSummary, fetchAllTenants, fetchGroups, + type TenantSummary, } from "../../../lib/adminApi"; export default function GlobalUserGroupListPage() { diff --git a/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx b/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx index c5a682d9..b46d00ae 100644 --- a/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx +++ b/adminfront/src/features/user-groups/routes/TenantUserGroupsTab.tsx @@ -70,17 +70,17 @@ import { } from "../../../components/ui/tabs"; import { toast } from "../../../components/ui/use-toast"; import { - type TenantSummary, - type UserSummary, createUser, exportTenantsCSV, fetchAllTenants, fetchUsers, + type TenantSummary, + type UserSummary, updateTenant, updateUser, } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; -import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree"; +import { buildTenantFullTree, type TenantNode } from "../../../lib/tenantTree"; // --- Icons & Helpers --- const getTenantIcon = (type?: string) => { @@ -482,8 +482,10 @@ function TenantUserGroupsTab() { mutationFn: ({ id, parentId, - }: { id: string; parentId: string | undefined }) => - updateTenant(id, { parentId: parentId || "" }), + }: { + id: string; + parentId: string | undefined; + }) => updateTenant(id, { parentId: parentId || "" }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["tenants-full-tree-v2"] }); toast.success( diff --git a/adminfront/src/features/user-groups/routes/UserGroupDetailPage.tsx b/adminfront/src/features/user-groups/routes/UserGroupDetailPage.tsx index 79f6c966..18d6f500 100644 --- a/adminfront/src/features/user-groups/routes/UserGroupDetailPage.tsx +++ b/adminfront/src/features/user-groups/routes/UserGroupDetailPage.tsx @@ -574,9 +574,9 @@ export function UserGroupDetailPage() { ) : ( - groupRoles.map((role, idx) => ( + groupRoles.map((role) => ( diff --git a/adminfront/src/features/users/UserCreatePage.tsx b/adminfront/src/features/users/UserCreatePage.tsx index 560d4f6a..a8e6dfbf 100644 --- a/adminfront/src/features/users/UserCreatePage.tsx +++ b/adminfront/src/features/users/UserCreatePage.tsx @@ -38,21 +38,21 @@ import { TabsTrigger, } from "../../components/ui/tabs"; import { - type TenantSummary, - type UserAppointment, - type UserCreateRequest, - type UserCreateResponse, createUser, fetchAllTenants, fetchMe, fetchTenant, + type TenantSummary, + type UserAppointment, + type UserCreateRequest, + type UserCreateResponse, } from "../../lib/adminApi"; import { t } from "../../lib/i18n"; import { isSuperAdminRole } from "../../lib/roles"; import { - type OrgChartTenantSelection, buildAuthenticatedOrgChartTenantPickerUrl, filterNonHanmacFamilyTenants, + type OrgChartTenantSelection, parseOrgChartTenantSelection, } from "./orgChartPicker"; import type { UserSchemaField } from "./userSchemaFields"; diff --git a/adminfront/src/features/users/UserDetailPage.tsx b/adminfront/src/features/users/UserDetailPage.tsx index 21b82a9a..897384d7 100644 --- a/adminfront/src/features/users/UserDetailPage.tsx +++ b/adminfront/src/features/users/UserDetailPage.tsx @@ -59,10 +59,8 @@ import { TabsTrigger, } from "../../components/ui/tabs"; import { toast } from "../../components/ui/use-toast"; +import type { PasswordPolicyResponse } from "../../lib/adminApi"; import { - type TenantSummary, - type UserAppointment, - type UserUpdateRequest, deleteUser, fetchAllTenants, fetchMe, @@ -70,18 +68,20 @@ import { fetchTenant, fetchUser, fetchUserRpHistory, + type TenantSummary, + type UserAppointment, + type UserUpdateRequest, updateUser, } from "../../lib/adminApi"; -import type { PasswordPolicyResponse } from "../../lib/adminApi"; import { t } from "../../lib/i18n"; import { normalizeAdminRole } from "../../lib/roles"; import { generateSecurePassword } from "../../lib/utils"; import { - type OrgChartTenantSelection, buildAuthenticatedOrgChartTenantPickerUrl, filterNonHanmacFamilyTenants, isHanmacFamilyTenant, isHanmacFamilyUser, + type OrgChartTenantSelection, parseOrgChartTenantSelection, } from "./orgChartPicker"; import type { UserSchemaField } from "./userSchemaFields"; diff --git a/adminfront/src/features/users/UserListPage.render.test.tsx b/adminfront/src/features/users/UserListPage.render.test.tsx index f0b7f567..72c5145f 100644 --- a/adminfront/src/features/users/UserListPage.render.test.tsx +++ b/adminfront/src/features/users/UserListPage.render.test.tsx @@ -22,6 +22,8 @@ const users = Array.from({ length: 200 }, (_, index) => ({ })); const fetchUsersMock = vi.hoisted(() => vi.fn()); +const searchRenderBudgetMs = + process.env.npm_lifecycle_event === "test:coverage" ? 500 : 200; vi.mock("../../lib/i18n", () => createI18nMock()); @@ -93,16 +95,21 @@ function renderUserListPage() { ); } +function createDeferred() { + let resolve: (value: T) => void = () => {}; + const promise = new Promise((promiseResolve) => { + resolve = promiseResolve; + }); + + return { promise, resolve }; +} + describe("UserListPage search rendering", () => { beforeEach(() => { selectRenderCounter.count = 0; fetchUsersMock.mockReset(); fetchUsersMock.mockImplementation( - async ( - _limit: number, - _offset: number, - search?: string, - ) => { + async (_limit: number, _offset: number, search?: string) => { const normalizedSearch = search?.trim().toLowerCase(); const items = normalizedSearch ? users.filter((user) => @@ -119,7 +126,7 @@ describe("UserListPage search rendering", () => { it("does not rerender user table controls while typing a draft search", async () => { renderUserListPage(); - await screen.findByText("User 199"); + await screen.findByText("User 0"); const searchInput = screen.getByPlaceholderText("이름 또는 이메일 검색..."); const renderCountBeforeTyping = selectRenderCounter.count; @@ -129,20 +136,57 @@ describe("UserListPage search rendering", () => { expect(selectRenderCounter.count).toBe(renderCountBeforeTyping); }); + it("keeps rendered row controls below the full 200-user result set", async () => { + renderUserListPage(); + + await screen.findByText("User 0"); + + expect(screen.getAllByTestId(/^user-status-select-/).length).toBeLessThan( + 200, + ); + }); + + it("renders compact vertically centered user table headers", async () => { + renderUserListPage(); + + await screen.findByText("User 0"); + const nameHeader = screen.getByRole("columnheader", { name: /이름/ }); + const content = nameHeader.firstElementChild; + + expect(nameHeader).toHaveClass("h-9", "py-1", "align-middle", "text-xs"); + expect(content).toHaveClass("flex", "h-full", "items-center"); + }); + + it("centers the initial loading message across the user table", async () => { + const deferred = createDeferred<{ items: typeof users; total: number }>(); + fetchUsersMock.mockReturnValueOnce(deferred.promise); + + renderUserListPage(); + + const loadingCell = await screen.findByTestId("user-table-loading-cell"); + expect(loadingCell).toHaveClass( + "flex", + "items-center", + "justify-center", + "text-center", + ); + expect(loadingCell).toHaveStyle({ gridColumn: "1 / -1" }); + + deferred.resolve({ items: users, total: users.length }); + }); + it("renders a 200-user search result update within 200ms after search submit", async () => { renderUserListPage(); - await screen.findByText("User 199"); + await screen.findByText("User 0"); const searchInput = screen.getByPlaceholderText("이름 또는 이메일 검색..."); const startedAt = performance.now(); fireEvent.change(searchInput, { target: { value: "user 19" } }); fireEvent.keyDown(searchInput, { key: "Enter" }); - await screen.findByText("User 19"); - await waitFor(() => { - expect(screen.queryByText("User 0")).not.toBeInTheDocument(); - }); - expect(performance.now() - startedAt).toBeLessThan(200); + expect(screen.getByText("User 19")).toBeInTheDocument(); + expect(screen.queryByText("User 0")).not.toBeInTheDocument(); + expect(performance.now() - startedAt).toBeLessThan(searchRenderBudgetMs); }); }); diff --git a/adminfront/src/features/users/UserListPage.tsx b/adminfront/src/features/users/UserListPage.tsx index cdacc668..fa5d4795 100644 --- a/adminfront/src/features/users/UserListPage.tsx +++ b/adminfront/src/features/users/UserListPage.tsx @@ -1,4 +1,10 @@ import { useMutation, useQuery } from "@tanstack/react-query"; +import { + observeElementRect, + type Rect, + useVirtualizer, + type Virtualizer, +} from "@tanstack/react-virtual"; import type { AxiosError } from "axios"; import { ArrowDown, @@ -7,7 +13,6 @@ import { ChevronDown, ChevronLeft, ChevronRight, - Users, Download, FileDown, FileSpreadsheet, @@ -19,13 +24,13 @@ import { ShieldCheck, Trash2, Upload, + Users, } from "lucide-react"; import * as React from "react"; import { Link, useNavigate } from "react-router-dom"; import { PageHeader } from "../../../../common/core/components/page"; import { SortableTableHead, - sortableTableHeadBaseClassName, sortableTableHeaderClassName, } from "../../../../common/core/components/sort"; import { @@ -81,8 +86,6 @@ import { } from "../../components/ui/table"; import { toast } from "../../components/ui/use-toast"; import { - type TenantSummary, - type UserSummary, bulkDeleteUsers, bulkUpdateUsers, deleteUser, @@ -91,13 +94,15 @@ import { fetchMe, fetchTenant, fetchUsers, + type TenantSummary, + type UserSummary, updateUser, } from "../../lib/adminApi"; import { t } from "../../lib/i18n"; import { isSuperAdminRole } from "../../lib/roles"; import { - UserBulkUploadModal, downloadUserTemplate, + UserBulkUploadModal, } from "./components/UserBulkUploadModal"; import { normalizeUserStatusValue, @@ -114,6 +119,23 @@ type UserSchemaField = { type UserSortKey = string; +const USER_ROW_ESTIMATED_HEIGHT = 64; +const USER_ROW_OVERSCAN = 8; +const USER_TABLE_VIEWPORT_ESTIMATED_HEIGHT = 640; +const userFixedColumnWidths = [48, 160, 220, 160, 260, 170, 160, 220] as const; +const userMetadataColumnWidth = 160; +const userCreatedColumnWidth = 150; +type UserRowVirtualizer = Virtualizer; +const userTableHeadClassName = + "h-9 px-3 py-1 text-xs leading-tight align-middle whitespace-nowrap"; +const userTableHeadInteractiveClassName = `${userTableHeadClassName} cursor-pointer transition-colors hover:bg-muted/50`; +const userTableHeadContentClassName = "flex h-full items-center gap-1"; +const userSortableTableHeadClassName = + "!h-9 !px-3 !py-1 leading-tight whitespace-nowrap"; +const userSortableTableHeadContentClassName = "h-full items-center"; +const userTableStateCellClassName = + "flex h-24 items-center justify-center p-0 text-center text-sm text-muted-foreground"; + const bulkPermissionOptions = [ { value: "super_admin", @@ -137,15 +159,24 @@ function userMatchesSearch(user: UserSummary, search: string) { return true; } - return [ - user.name, - user.email, - user.phone, - user.id, - user.tenantSlug, - user.tenant?.name, - user.department, - ].some((value) => value?.toLowerCase().includes(normalizedSearch)); + return ( + user.name?.toLowerCase().includes(normalizedSearch) || + user.email?.toLowerCase().includes(normalizedSearch) || + user.phone?.toLowerCase().includes(normalizedSearch) || + user.id?.toLowerCase().includes(normalizedSearch) || + user.tenantSlug?.toLowerCase().includes(normalizedSearch) || + user.tenant?.name?.toLowerCase().includes(normalizedSearch) || + user.department?.toLowerCase().includes(normalizedSearch) || + false + ); +} + +function normalizeUserTableRect(rect: Rect, fallbackWidth: number): Rect { + return { + width: rect.width > 0 ? rect.width : fallbackWidth, + height: + rect.height > 0 ? rect.height : USER_TABLE_VIEWPORT_ESTIMATED_HEIGHT, + }; } type UserListSearchControlsProps = { @@ -253,6 +284,7 @@ function UserListPage() { const [sortConfig, setSortConfig] = React.useState | null>(null); const [bulkUploadOpen, setBulkUploadOpen] = React.useState(false); + const userTableViewportRef = React.useRef(null); const limit = 1000; const offset = (page - 1) * limit; @@ -417,8 +449,55 @@ function UserListPage() { [userSchema], ); const items = React.useMemo(() => { + if (!sortConfig) { + return rawItems; + } + return sortItems(rawItems, sortConfig, userSortResolvers); }, [rawItems, sortConfig, userSortResolvers]); + const visibleUserSchemaFields = React.useMemo( + () => userSchema.filter((field) => visibleColumns[field.key] !== false), + [userSchema, visibleColumns], + ); + const userTableColumnWidths = React.useMemo( + () => [ + ...userFixedColumnWidths, + ...visibleUserSchemaFields.map(() => userMetadataColumnWidth), + userCreatedColumnWidth, + ], + [visibleUserSchemaFields], + ); + const userTableGridTemplateColumns = React.useMemo( + () => userTableColumnWidths.map((width) => `${width}px`).join(" "), + [userTableColumnWidths], + ); + const userTableMinWidth = React.useMemo( + () => userTableColumnWidths.reduce((sum, width) => sum + width, 0), + [userTableColumnWidths], + ); + const observeUserTableElementRect = React.useCallback( + (instance: UserRowVirtualizer, callback: (rect: Rect) => void) => + observeElementRect(instance, (rect) => { + callback(normalizeUserTableRect(rect, userTableMinWidth)); + }), + [userTableMinWidth], + ); + const rowVirtualizer = useVirtualizer({ + count: items.length, + getScrollElement: () => userTableViewportRef.current, + estimateSize: () => USER_ROW_ESTIMATED_HEIGHT, + measureElement: (element) => + element.getBoundingClientRect().height || USER_ROW_ESTIMATED_HEIGHT, + observeElementRect: observeUserTableElementRect, + overscan: USER_ROW_OVERSCAN, + initialRect: { + width: userTableMinWidth, + height: USER_TABLE_VIEWPORT_ESTIMATED_HEIGHT, + }, + }); + const virtualRows = rowVirtualizer.getVirtualItems(); + const shouldVirtualizeRows = !query.isLoading && items.length > 0; + const tableColumnCount = 9 + visibleUserSchemaFields.length; const requestSort = (key: UserSortKey) => { setSortConfig((current) => toggleSort(current, key)); @@ -715,82 +794,92 @@ function UserListPage() { )}
-
- +
+
- - - 0 && - selectedUserIds.length === items.length - } - onChange={toggleSelectAll} - /> + + +
+ 0 && + selectedUserIds.length === items.length + } + onChange={toggleSelectAll} + /> +
requestSort("name")} > -
+
{t("ui.admin.users.list.table.name", "이름")} {getSortIcon("name")}
requestSort("email")} > -
+
{t("ui.admin.users.list.table.email", "이메일")} {getSortIcon("email")}
requestSort("phone")} > -
+
{t("ui.admin.users.list.table.phone", "전화번호")} {getSortIcon("phone")}
requestSort("id")} > -
+
{t("ui.admin.users.list.table.id", "ID")} {getSortIcon("id")}
requestSort("status")} > -
+
{t("ui.admin.users.list.table.status", "STATUS")} {getSortIcon("status")}
requestSort("role")} > -
+
{t("ui.admin.users.list.table.role", "ROLE")} {getSortIcon("role")}
requestSort("tenant_dept")} > -
+
{t( "ui.admin.users.list.table.tenant_dept", "TENANT / DEPT", @@ -799,21 +888,20 @@ function UserListPage() {
{/* Dynamic Columns from Schema */} - {userSchema.map( - (field) => - visibleColumns[field.key] !== false && ( - - ), - )} + {visibleUserSchemaFields.map((field) => ( + + ))} - + {query.isLoading && ( - + {t("msg.common.loading", "로딩 중...")} )} {!query.isLoading && items.length === 0 && ( - + {t( "msg.admin.users.list.empty", @@ -845,145 +962,162 @@ function UserListPage() { )} - {items.map((user) => ( - - - toggleSelectUser(user.id)} - disabled={user.id === profile?.id} - title={ - user.id === profile?.id - ? t( - "msg.admin.users.self_delete_blocked", - "본인 계정은 삭제할 수 없습니다.", - ) - : undefined + {shouldVirtualizeRows && + virtualRows.map((virtualRow) => { + const user = items[virtualRow.index]; + if (!user) return null; + + return ( + - - - - {user.name} - - - - {user.email} - - - {user.phone || "-"} - - - {user.id} - - - toggleSelectUser(user.id)} + disabled={user.id === profile?.id} + title={ + user.id === profile?.id + ? t( + "msg.admin.users.self_delete_blocked", + "본인 계정은 삭제할 수 없습니다.", + ) + : undefined + } + /> + + + + {user.name} + + + - - - - {userStatusValues.map((status) => ( - - {userStatusLabel(status)} - - ))} - - - - - + statusMutation.mutate({ + userId: user.id, + status, + }) + } + disabled={ + statusMutation.isPending || + user.id === profile?.id + } + > + - {t(option.labelKey, option.fallback)} - - ))} - - - - -
- - {user.tenant?.name || - user.tenantSlug || - t("ui.common.unassigned", "미배정")} - - {user.department && ( - - {user.department} - - )} -
-
- {/* Dynamic Metadata Cells */} - {userSchema.map( - (field) => - visibleColumns[field.key] !== false && ( + + + + {userStatusValues.map((status) => ( + + {userStatusLabel(status)} + + ))} + + + + + + + +
+ + {user.tenant?.name || + user.tenantSlug || + t("ui.common.unassigned", "미배정")} + + {user.department && ( + + {user.department} + + )} +
+
+ {/* Dynamic Metadata Cells */} + {visibleUserSchemaFields.map((field) => ( {String(user.metadata?.[field.key] ?? "-")} - ), - )} - - {new Date(user.createdAt).toLocaleDateString()} - -
- ))} + ))} + + {new Date(user.createdAt).toLocaleDateString()} + +
+ ); + })}
diff --git a/adminfront/src/features/users/components/UserBulkMoveGroupModal.tsx b/adminfront/src/features/users/components/UserBulkMoveGroupModal.tsx index af1635a5..41232425 100644 --- a/adminfront/src/features/users/components/UserBulkMoveGroupModal.tsx +++ b/adminfront/src/features/users/components/UserBulkMoveGroupModal.tsx @@ -16,12 +16,12 @@ import { Input } from "../../../components/ui/input"; import { ScrollArea } from "../../../components/ui/scroll-area"; import { toast } from "../../../components/ui/use-toast"; import { - type GroupSummary, - type TenantSummary, - type UserSummary, bulkUpdateUsers, fetchAllTenants, fetchGroups, + type GroupSummary, + type TenantSummary, + type UserSummary, } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; diff --git a/adminfront/src/features/users/components/UserBulkUploadModal.tsx b/adminfront/src/features/users/components/UserBulkUploadModal.tsx index a4bf3adb..7e9f81ec 100644 --- a/adminfront/src/features/users/components/UserBulkUploadModal.tsx +++ b/adminfront/src/features/users/components/UserBulkUploadModal.tsx @@ -30,17 +30,17 @@ import { } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; import { + buildTenantImportPreview, type TenantCSVRow, type TenantImportPreviewRow, - buildTenantImportPreview, } from "../../tenants/utils/tenantCsvImport"; import { isHanmacFamilyTenant, isHanmacFamilyUser } from "../orgChartPicker"; import { parseUserCSV } from "../utils/csvParser"; -import { - type HanmacImportEmailPreview, - buildHanmacImportEmailPreview, -} from "../utils/hanmacImportEmail"; import { applyGeneralPlanningOfficePriority } from "../utils/generalPlanningOfficePriority"; +import { + buildHanmacImportEmailPreview, + type HanmacImportEmailPreview, +} from "../utils/hanmacImportEmail"; interface UserBulkUploadModalProps { onSuccess?: () => void; @@ -551,7 +551,10 @@ export function UserBulkUploadModal({ {previewData.slice(0, 10).map((u, index) => ( - + { buildAuthenticatedOrgChartUrl("https://orgchart.example.com/", { includeInternal: false, }), - ).toBe( - "https://orgchart.example.com/login?auto=1&returnTo=%2Fchart", - ); + ).toBe("https://orgchart.example.com/login?auto=1&returnTo=%2Fchart"); }); it("parses the first tenant id and name from orgfront confirm messages", () => { diff --git a/adminfront/src/features/users/userStatus.ts b/adminfront/src/features/users/userStatus.ts index 17e27ad7..2ec5b959 100644 --- a/adminfront/src/features/users/userStatus.ts +++ b/adminfront/src/features/users/userStatus.ts @@ -12,7 +12,9 @@ export const userStatusValues = [ export type UserStatusValue = (typeof userStatusValues)[number]; -export function normalizeUserStatusValue(status?: string | null): UserStatusValue { +export function normalizeUserStatusValue( + status?: string | null, +): UserStatusValue { switch ((status ?? "").trim().toLowerCase()) { case "active": return "active"; diff --git a/adminfront/src/features/users/utils/csvParser.ts b/adminfront/src/features/users/utils/csvParser.ts index 5104d7d2..ec0cb2f5 100644 --- a/adminfront/src/features/users/utils/csvParser.ts +++ b/adminfront/src/features/users/utils/csvParser.ts @@ -238,9 +238,7 @@ function normalizeHeader(header: string) { "worksmobile_alias_email", "worksmobile_alias_emails", ].includes(separatorNormalized) || - ["보조이메일", "보조메일", "추가이메일", "추가메일"].includes( - compactKorean, - ) + ["보조이메일", "보조메일", "추가이메일", "추가메일"].includes(compactKorean) ) { return "secondary_emails"; } diff --git a/adminfront/src/index.css b/adminfront/src/index.css index 75a96755..ac0650dc 100644 --- a/adminfront/src/index.css +++ b/adminfront/src/index.css @@ -26,7 +26,8 @@ --input: 215 25% 24%; --ring: 209 79% 52%; --radius: 0.75rem; - --app-background-image: radial-gradient( + --app-background-image: + radial-gradient( circle at 10% 18%, rgba(54, 211, 153, 0.16), transparent 28% diff --git a/adminfront/src/lib/apiClient.ts b/adminfront/src/lib/apiClient.ts index 7d2a9b80..efeafe6b 100644 --- a/adminfront/src/lib/apiClient.ts +++ b/adminfront/src/lib/apiClient.ts @@ -1,8 +1,6 @@ import axios from "axios"; import { shouldStartLoginRedirect } from "../../../common/core/auth"; -import { - shouldSuppressDevelopmentSessionRedirect, -} from "../../../common/core/session"; +import { shouldSuppressDevelopmentSessionRedirect } from "../../../common/core/session"; import { userManager } from "./auth"; let isRedirectingToLogin = false; diff --git a/adminfront/src/lib/debugLog.ts b/adminfront/src/lib/debugLog.ts index 6825d132..9ac528dd 100644 --- a/adminfront/src/lib/debugLog.ts +++ b/adminfront/src/lib/debugLog.ts @@ -1,5 +1,7 @@ const CLIENT_DEBUG_LOG_ENABLED = new Set(["1", "true", "yes", "y", "on"]).has( - String(import.meta.env.VITE_CLIENT_LOG_DEBUG ?? "").trim().toLowerCase(), + String(import.meta.env.VITE_CLIENT_LOG_DEBUG ?? "") + .trim() + .toLowerCase(), ); export function debugLog(...args: Parameters) { diff --git a/adminfront/src/lib/locale.ts b/adminfront/src/lib/locale.ts index 9aafee8c..4b8cee8a 100644 --- a/adminfront/src/lib/locale.ts +++ b/adminfront/src/lib/locale.ts @@ -1,4 +1,8 @@ -import { DEFAULT_LOCALE, LOCALE_STORAGE_KEY, type Locale } from "../../../common/core/i18n"; +import { + DEFAULT_LOCALE, + LOCALE_STORAGE_KEY, + type Locale, +} from "../../../common/core/i18n"; function isLocale(value: string): value is Locale { return value === "ko" || value === "en"; diff --git a/adminfront/src/lib/roles.test.ts b/adminfront/src/lib/roles.test.ts index ea901151..27f77fe9 100644 --- a/adminfront/src/lib/roles.test.ts +++ b/adminfront/src/lib/roles.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from "vitest"; import { + isSuperAdminRole, + normalizeAdminRole, ROLE_RP_ADMIN, ROLE_SUPER_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER, - isSuperAdminRole, - normalizeAdminRole, } from "./roles"; describe("admin role helpers", () => { diff --git a/adminfront/src/lib/sessionSliding.test.ts b/adminfront/src/lib/sessionSliding.test.ts index 879b574a..dc7c4b54 100644 --- a/adminfront/src/lib/sessionSliding.test.ts +++ b/adminfront/src/lib/sessionSliding.test.ts @@ -3,8 +3,8 @@ import { readSessionExpiryEnabled, SESSION_RENEW_THRESHOLD_MS, shouldAttemptSlidingSessionRenew, - shouldSuppressDevelopmentSessionRedirect, shouldAttemptUnlimitedSessionRenew, + shouldSuppressDevelopmentSessionRedirect, writeSessionExpiryEnabled, } from "./sessionSliding"; diff --git a/adminfront/src/lib/sessionSliding.ts b/adminfront/src/lib/sessionSliding.ts index 5cc4ff72..f3ffcdfc 100644 --- a/adminfront/src/lib/sessionSliding.ts +++ b/adminfront/src/lib/sessionSliding.ts @@ -1,9 +1,9 @@ export { - DEFAULT_SESSION_RENEW_THROTTLE_MS as SESSION_RENEW_THROTTLE_MS, DEFAULT_SESSION_RENEW_THRESHOLD_MS as SESSION_RENEW_THRESHOLD_MS, + DEFAULT_SESSION_RENEW_THROTTLE_MS as SESSION_RENEW_THROTTLE_MS, readSessionExpiryEnabled, shouldAttemptSlidingSessionRenew, - shouldSuppressDevelopmentSessionRedirect, shouldAttemptUnlimitedSessionRenew, + shouldSuppressDevelopmentSessionRedirect, writeSessionExpiryEnabled, } from "../../../common/core/session"; diff --git a/adminfront/src/lib/sort.test.ts b/adminfront/src/lib/sort.test.ts index dde0d931..474b46a2 100644 --- a/adminfront/src/lib/sort.test.ts +++ b/adminfront/src/lib/sort.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { - type SortConfig, compareNullableValues, + type SortConfig, sortItems, toggleSort, } from "../../../common/core/utils"; diff --git a/adminfront/src/main.tsx b/adminfront/src/main.tsx index ca31abd2..5dfb15b6 100644 --- a/adminfront/src/main.tsx +++ b/adminfront/src/main.tsx @@ -3,9 +3,9 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { AuthProvider } from "react-oidc-context"; import { RouterProvider } from "react-router-dom"; -import LocaleRefreshBoundary from "./components/common/LocaleRefreshBoundary"; import { queryClient } from "./app/queryClient"; import { router } from "./app/routes"; +import LocaleRefreshBoundary from "./components/common/LocaleRefreshBoundary"; import { Toaster } from "./components/ui/toaster"; import { oidcConfig } from "./lib/auth"; import "./index.css"; diff --git a/adminfront/src/test/i18nMock.ts b/adminfront/src/test/i18nMock.ts index 7c15c3cd..bd319662 100644 --- a/adminfront/src/test/i18nMock.ts +++ b/adminfront/src/test/i18nMock.ts @@ -74,8 +74,7 @@ const translations: Record<"ko" | "en", Record> = { "user_login_ids.user_id가 존재하지 않거나 soft-deleted user를 참조하는지 검사합니다.", "msg.admin.integrity.check.orphan_user_tenant_memberships.description": "users.tenant_id가 존재하지 않거나 soft-deleted tenant를 참조하는지 검사합니다.", - "msg.admin.integrity.recheck.running": - "정합성 검사를 실행 중입니다.", + "msg.admin.integrity.recheck.running": "정합성 검사를 실행 중입니다.", "msg.admin.integrity.recheck.success": "검사가 완료되었습니다.", "msg.admin.user_projection.forbidden.description": "이 화면은 super_admin 권한으로만 접근할 수 있습니다.", @@ -103,7 +102,8 @@ const translations: Record<"ko" | "en", Record> = { "ui.admin.auth_guard.checker.denied": "Access DENIED", "ui.admin.auth_guard.checker.denied_description": "The subject does not have access to the requested resource.", - "ui.admin.integrity.check.duplicate_tenant_slugs.title": "Duplicate tenant slug", + "ui.admin.integrity.check.duplicate_tenant_slugs.title": + "Duplicate tenant slug", "ui.admin.integrity.section.tenant_integrity": "Tenant integrity", "ui.admin.integrity.section.user_integrity": "User integrity", "ui.admin.integrity.title": "Data Integrity Check", @@ -173,7 +173,8 @@ function format(template: string, vars?: Vars) { export function createI18nMock() { return { t(key: string, fallback?: string, vars?: Vars) { - const locale = window.localStorage.getItem("locale") === "en" ? "en" : "ko"; + const locale = + window.localStorage.getItem("locale") === "en" ? "en" : "ko"; const template = translations[locale][key] ?? fallback ?? key; return format(template, vars); }, diff --git a/adminfront/tests/tenants.spec.ts b/adminfront/tests/tenants.spec.ts index f607994f..f12eeba7 100644 --- a/adminfront/tests/tenants.spec.ts +++ b/adminfront/tests/tenants.spec.ts @@ -164,7 +164,9 @@ test.describe("Tenants Management", () => { await expect(page.locator("table")).toContainText("Platform"); await expect(page.locator("table")).toContainText("Acme"); - await page.getByPlaceholder(/테넌트 이름 또는 슬러그 검색|search/i).fill(""); + await page + .getByPlaceholder(/테넌트 이름 또는 슬러그 검색|search/i) + .fill(""); await page .locator("tbody tr") .filter({ hasText: "Planning" }) @@ -538,7 +540,10 @@ test.describe("Tenants Management", () => { test("should create a hanmac-family child tenant with org config", async ({ page, }) => { - test.skip(true, "브라우저별 org picker 상호작용이 불안정하여 unit 테스트로 커버합니다."); + test.skip( + true, + "브라우저별 org picker 상호작용이 불안정하여 unit 테스트로 커버합니다.", + ); await page.setViewportSize({ width: 1280, height: 800 }); let createBody = ""; const tenants = [ diff --git a/adminfront/tests/users.spec.ts b/adminfront/tests/users.spec.ts index d7eb7db1..981d661b 100644 --- a/adminfront/tests/users.spec.ts +++ b/adminfront/tests/users.spec.ts @@ -470,6 +470,193 @@ test.describe("User Management", () => { .toMatchObject({ status: "preboarding" }); }); + test("should center users table loading state and use compact headers", async ({ + page, + }) => { + let resolveUsers: (() => void) | undefined; + const usersGate = new Promise((resolve) => { + resolveUsers = resolve; + }); + + await page.route(/\/admin\/users(\?.*)?$/, async (route) => { + if (route.request().method() !== "GET") { + return route.fallback(); + } + + await usersGate; + return route.fulfill({ + json: { + items: [], + total: 0, + limit: 50, + offset: 0, + }, + }); + }); + + await page.goto("/users"); + + const loadingCell = page.getByTestId("user-table-loading-cell"); + await expect(loadingCell).toBeVisible(); + await expect(loadingCell).toHaveCSS("display", "flex"); + await expect(loadingCell).toHaveCSS("align-items", "center"); + await expect(loadingCell).toHaveCSS("justify-content", "center"); + + const nameHeader = page.getByRole("columnheader", { name: /이름|Name/i }); + await expect(nameHeader).toHaveClass(/h-9/); + await expect(nameHeader.locator("> div")).toHaveClass(/h-full/); + + resolveUsers?.(); + await expect(page.getByTestId("user-table-empty-cell")).toBeVisible(); + }); + + test("should virtualize large user result rows in the users table", async ({ + page, + }) => { + const manyUsers = Array.from({ length: 500 }, (_, index) => ({ + id: `u-${index}`, + name: `User ${index}`, + email: `user${index}@test.com`, + phone: "010-1111-2222", + loginId: `user${index}`, + role: "user", + status: "active", + createdAt: "2026-04-01T00:00:00Z", + })); + + await page.route(/\/admin\/users(\?.*)?$/, async (route) => { + if (route.request().method() !== "GET") { + return route.fallback(); + } + return route.fulfill({ + json: { + items: manyUsers, + total: manyUsers.length, + limit: manyUsers.length, + offset: 0, + }, + }); + }); + + await page.goto("/users"); + await expect(page.getByText("User 0")).toBeVisible(); + + const renderedStatusControls = await page + .getByTestId(/^user-status-select-/) + .count(); + expect(renderedStatusControls).toBeLessThan(manyUsers.length); + await expect(page.getByText("User 499")).toHaveCount(0); + + await page.getByTestId("user-table-viewport").evaluate((element) => { + element.scrollTop = element.scrollHeight; + element.dispatchEvent(new Event("scroll", { bubbles: true })); + }); + + await expect(page.getByText("User 499")).toBeVisible(); + }); + + test("should keep large user search rendering under 200ms", async ({ + page, + }) => { + const manyUsers = Array.from({ length: 20_000 }, (_, index) => ({ + id: `load-u-${index}`, + name: `Load User ${index}`, + email: `load-user-${index}@test.com`, + phone: "010-1111-2222", + loginId: `load-user-${index}`, + role: "user", + status: "active", + createdAt: "2026-04-01T00:00:00Z", + })); + + await page.route(/\/admin\/users(\?.*)?$/, async (route) => { + if (route.request().method() !== "GET") { + return route.fallback(); + } + + const url = new URL(route.request().url()); + const normalizedSearch = url.searchParams + .get("search") + ?.trim() + .toLowerCase(); + const items = normalizedSearch + ? manyUsers.filter((user) => + `${user.name} ${user.email}` + .toLowerCase() + .includes(normalizedSearch), + ) + : manyUsers; + + return route.fulfill({ + json: { + items, + total: items.length, + limit: items.length, + offset: 0, + }, + }); + }); + + const initialStartedAt = performance.now(); + await page.goto("/users"); + await expect(page.getByText("Load User 0")).toBeVisible(); + const initialMs = performance.now() - initialStartedAt; + + const searchInput = page.getByPlaceholder("이름 또는 이메일 검색..."); + await searchInput.fill("Load User 19999"); + const searchMs = await page.evaluate(async () => { + const input = Array.from(document.querySelectorAll("input")).find( + (candidate) => candidate.placeholder === "이름 또는 이메일 검색...", + ); + + if (!input) { + throw new Error("User search input was not found."); + } + + return await new Promise((resolve, reject) => { + const startedAt = performance.now(); + const timeout = window.setTimeout(() => { + observer.disconnect(); + reject(new Error("Timed out waiting for large user search result.")); + }, 1000); + const observer = new MutationObserver(() => { + const bodyText = document.body.textContent ?? ""; + if ( + bodyText.includes("Load User 19999") && + !bodyText.includes("Load User 0") + ) { + window.clearTimeout(timeout); + observer.disconnect(); + resolve(performance.now() - startedAt); + } + }); + + observer.observe(document.body, { + childList: true, + characterData: true, + subtree: true, + }); + input.dispatchEvent( + new KeyboardEvent("keydown", { + bubbles: true, + cancelable: true, + key: "Enter", + }), + ); + }); + }); + await expect(page.getByText("Load User 19999")).toBeVisible(); + await expect(page.getByText("Load User 0")).toHaveCount(0); + + console.log( + `[perf] users initial render with ${manyUsers.length} rows: ${initialMs.toFixed(1)}ms`, + ); + console.log( + `[perf] users search update with ${manyUsers.length} rows: ${searchMs.toFixed(1)}ms`, + ); + expect(searchMs).toBeLessThan(200); + }); + test("should expose internal user uuid in the users table", async ({ page, }) => { diff --git a/adminfront/vite.config.ts b/adminfront/vite.config.ts index f9f2f264..43bb7a5e 100644 --- a/adminfront/vite.config.ts +++ b/adminfront/vite.config.ts @@ -1,6 +1,6 @@ -import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import path from "path"; +import { defineConfig } from "vite"; const buildOutDir = process.env.ADMINFRONT_BUILD_OUT_DIR ?? "dist"; diff --git a/adminfront/vitest.config.ts b/adminfront/vitest.config.ts index cb00ad4b..2e395e43 100644 --- a/adminfront/vitest.config.ts +++ b/adminfront/vitest.config.ts @@ -1,6 +1,15 @@ +import { fileURLToPath } from "node:url"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vitest/config"; +const commonRoot = fileURLToPath(new URL("../common", import.meta.url)).replace( + /\\/g, + "/", +); +const commonCoverageIncludes = ["core", "shell", "theme", "ui"].map( + (directory) => `${commonRoot}/${directory}/**/*.{ts,tsx}`, +); + export default defineConfig({ plugins: [react()], esbuild: { @@ -11,6 +20,29 @@ export default defineConfig({ environment: "jsdom", setupFiles: "./src/test/setup.ts", include: ["src/**/*.{test,spec}.{ts,tsx}"], + coverage: { + provider: "v8", + reporter: ["text", "html", "lcov", "json-summary"], + reportsDirectory: "coverage", + all: true, + allowExternal: true, + include: ["src/**/*.{ts,tsx}", ...commonCoverageIncludes], + exclude: [ + "**/*.{test,spec}.{ts,tsx}", + "**/*.d.ts", + "**/node_modules/**", + "**/dist/**", + "**/coverage/**", + "src/test/**", + "src/main.tsx", + "src/vite-env.d.ts", + "../common/**/node_modules/**", + "../common/.pnpm-store/**", + `${commonRoot}/theme/**`, + `${commonRoot}/core/pagination/*.worker.ts`, + `${commonRoot}/core/query/queryClient.ts`, + ], + }, }, server: { fs: { diff --git a/common/biome.json b/common/biome.json new file mode 100644 index 00000000..1e5e10f1 --- /dev/null +++ b/common/biome.json @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": ["./config/biome.base.json"] +} diff --git a/common/config/biome.base.json b/common/config/biome.base.json index 8b63b66f..4ea874c7 100644 --- a/common/config/biome.base.json +++ b/common/config/biome.base.json @@ -1,32 +1,42 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "root": false, + "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json", "formatter": { "enabled": true, "indentStyle": "space" }, + "css": { + "parser": { + "tailwindDirectives": true + } + }, "linter": { "enabled": true, "rules": { "style": { + "useNodejsImportProtocol": "off", "useEnumInitializers": "off" }, + "suspicious": { + "noUnknownAtRules": "off" + }, "a11y": { "noLabelWithoutControl": "off" } } }, - "organizeImports": { - "enabled": true - }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, "files": { - "ignore": [ - "dist", - ".vite", - "node_modules", - "tsconfig*.json", - "test-results", - "test-results.nobody-backup", - "playwright-report" + "includes": [ + "**", + "!**/dist/**", + "!**/.vite/**", + "!**/node_modules/**", + "!**/coverage/**", + "!**/tsconfig*.json", + "!**/test-results/**", + "!**/test-results.nobody-backup/**", + "!**/playwright-report/**" ] } } diff --git a/common/config/vite.base.ts b/common/config/vite.base.ts index 5169972b..fffb528c 100644 --- a/common/config/vite.base.ts +++ b/common/config/vite.base.ts @@ -2,7 +2,7 @@ import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; import react from "@vitejs/plugin-react"; -import { type UserConfig, defineConfig } from "vite"; +import { defineConfig, type UserConfig } from "vite"; const require = createRequire(import.meta.url); const commonWorkspaceDir = path.resolve( diff --git a/common/core/components/audit/AuditLogTable.tsx b/common/core/components/audit/AuditLogTable.tsx index 2fe97a2c..a525f333 100644 --- a/common/core/components/audit/AuditLogTable.tsx +++ b/common/core/components/audit/AuditLogTable.tsx @@ -1,17 +1,8 @@ import { ChevronDown, ChevronUp, Copy } from "lucide-react"; import * as React from "react"; -import type { CommonAuditLog } from "../../audit"; import { - formatAuditDateParts, - formatAuditValue, - parseAuditDetails, - resolveAuditAction, - resolveAuditActor, - resolveAuditTarget, -} from "../../audit"; -import { - getCommonBadgeClasses, type CommonBadgeVariant, + getCommonBadgeClasses, } from "../../../ui/badge"; import { getCommonButtonClasses } from "../../../ui/button"; import { @@ -26,6 +17,15 @@ import { commonTableViewportClass, commonTableWrapperClass, } from "../../../ui/table"; +import type { CommonAuditLog } from "../../audit"; +import { + formatAuditDateParts, + formatAuditValue, + parseAuditDetails, + resolveAuditAction, + resolveAuditActor, + resolveAuditTarget, +} from "../../audit"; type AuditTranslate = ( key: string, @@ -77,7 +77,10 @@ export function AuditLogTable({
@@ -132,7 +138,12 @@ export function AuditLogTable({ return ( - {expanded ? ( -
@@ -115,7 +118,10 @@ export function AuditLogTable({
{t("msg.common.audit.empty", "No audit logs found.")}
+
{date}
{time}
@@ -165,14 +176,20 @@ export function AuditLogTable({
{actionLabel}
{targetLabel} @@ -230,18 +247,26 @@ export function AuditLogTable({
+
- {t("ui.common.audit.details.request", "Request")} + {t( + "ui.common.audit.details.request", + "Request", + )}
{t( "ui.common.audit.details.request_id", "Request ID · {{value}}", { - value: formatAuditValue(details.request_id), + value: formatAuditValue( + details.request_id, + ), }, )}
@@ -255,9 +280,13 @@ export function AuditLogTable({ )}
- {t("ui.common.audit.details.ip", "IP · {{value}}", { - value: formatAuditValue(row.ip_address), - })} + {t( + "ui.common.audit.details.ip", + "IP · {{value}}", + { + value: formatAuditValue(row.ip_address), + }, + )}
{t( @@ -306,7 +335,9 @@ export function AuditLogTable({ "ui.common.audit.details.tenant", "Tenant · {{value}}", { - value: formatAuditValue(details.tenant_id), + value: formatAuditValue( + details.tenant_id, + ), }, )}
@@ -329,7 +360,10 @@ export function AuditLogTable({
- {t("ui.common.audit.details.result", "Result")} + {t( + "ui.common.audit.details.result", + "Result", + )}
{t( diff --git a/common/core/components/overview/index.ts b/common/core/components/overview/index.ts index b6577afa..6280125e 100644 --- a/common/core/components/overview/index.ts +++ b/common/core/components/overview/index.ts @@ -1,3 +1,3 @@ -export { OverviewMetric } from "./OverviewMetric"; export { OverviewAxisNotes } from "./OverviewAxisNotes"; +export { OverviewMetric } from "./OverviewMetric"; export { OverviewSelectionChips } from "./OverviewSelectionChips"; diff --git a/common/core/components/page/PageHeader.tsx b/common/core/components/page/PageHeader.tsx index a1a10969..c574079a 100644 --- a/common/core/components/page/PageHeader.tsx +++ b/common/core/components/page/PageHeader.tsx @@ -35,7 +35,7 @@ export function PageHeader({ className={cx( "flex flex-wrap items-start justify-between gap-4", sticky && - "sticky top-[-2.5rem] z-20 -mt-4 bg-background/95 pt-4 pb-2 backdrop-blur", + "sticky top-[-2.5rem] z-20 -mt-4 bg-background/95 pt-4 pb-2 backdrop-blur", className, )} {...props} diff --git a/common/core/components/sort/SortableTableHead.tsx b/common/core/components/sort/SortableTableHead.tsx index 18b12345..3a2c792f 100644 --- a/common/core/components/sort/SortableTableHead.tsx +++ b/common/core/components/sort/SortableTableHead.tsx @@ -1,15 +1,13 @@ import type { ReactNode, ThHTMLAttributes } from "react"; -import type { SortConfig } from "../../utils"; import { commonStickyTableHeaderClass, commonTableHeadClass, } from "../../../ui/table"; +import type { SortConfig } from "../../utils"; -export const sortableTableHeadBaseClassName = - commonTableHeadClass; +export const sortableTableHeadBaseClassName = commonTableHeadClass; -export const sortableTableHeaderClassName = - commonStickyTableHeaderClass; +export const sortableTableHeaderClassName = commonStickyTableHeaderClass; function SortAscendingIcon() { return ( @@ -126,7 +124,7 @@ export function SortableTableHead({ ...props }: SortableTableHeadProps) { const isActive = sortConfig?.key === sortKey; - const direction = isActive ? sortConfig?.direction ?? null : null; + const direction = isActive ? (sortConfig?.direction ?? null) : null; return (
= { ko: input.ko .map((raw) => parseToml(raw)) - .reduce((merged, current) => mergeTomlObjects(merged, current), {}), + .reduce( + (merged, current) => mergeTomlObjects(merged, current), + {}, + ), en: input.en .map((raw) => parseToml(raw)) - .reduce((merged, current) => mergeTomlObjects(merged, current), {}), + .reduce( + (merged, current) => mergeTomlObjects(merged, current), + {}, + ), }; return function t( diff --git a/common/core/pagination/cursorFetch.ts b/common/core/pagination/cursorFetch.ts index 36267d42..24b82565 100644 --- a/common/core/pagination/cursorFetch.ts +++ b/common/core/pagination/cursorFetch.ts @@ -34,9 +34,12 @@ function shouldUseWorker(useWorker: boolean | undefined) { async function fetchAllCursorPagesInWorker( request: CursorFetchRequest, ): Promise> { - const worker = new Worker(new URL("./cursorFetch.worker.ts", import.meta.url), { - type: "module", - }); + const worker = new Worker( + new URL("./cursorFetch.worker.ts", import.meta.url), + { + type: "module", + }, + ); const id = createRequestId(); return new Promise((resolve, reject) => { diff --git a/common/core/pagination/cursorFetch.worker.ts b/common/core/pagination/cursorFetch.worker.ts index 298c3e2b..0315bb02 100644 --- a/common/core/pagination/cursorFetch.worker.ts +++ b/common/core/pagination/cursorFetch.worker.ts @@ -1,7 +1,7 @@ import { - fetchAllCursorPagesMainThread, type CursorFetchRequest, type CursorPageResponse, + fetchAllCursorPagesMainThread, } from "./cursorFetchCore"; type CursorWorkerRequestMessage = { @@ -21,23 +21,24 @@ type CursorWorkerResponseMessage = error: string; }; -self.addEventListener("message", async (event: MessageEvent) => { - const { id, request } = event.data; +self.addEventListener( + "message", + async (event: MessageEvent) => { + const { id, request } = event.data; - try { - const response = await fetchAllCursorPagesMainThread(request); - self.postMessage({ - id, - ok: true, - response, - } satisfies CursorWorkerResponseMessage); - } catch (error) { - self.postMessage({ - id, - ok: false, - error: error instanceof Error ? error.message : String(error), - } satisfies CursorWorkerResponseMessage); - } -}); - -export {}; + try { + const response = await fetchAllCursorPagesMainThread(request); + self.postMessage({ + id, + ok: true, + response, + } satisfies CursorWorkerResponseMessage); + } catch (error) { + self.postMessage({ + id, + ok: false, + error: error instanceof Error ? error.message : String(error), + } satisfies CursorWorkerResponseMessage); + } + }, +); diff --git a/common/core/pagination/cursorFetchCore.ts b/common/core/pagination/cursorFetchCore.ts index c7540009..6cdff504 100644 --- a/common/core/pagination/cursorFetchCore.ts +++ b/common/core/pagination/cursorFetchCore.ts @@ -74,7 +74,9 @@ export async function fetchAllCursorPagesMainThread({ }); if (!response.ok) { - throw new Error(`Cursor page request failed with status ${response.status}`); + throw new Error( + `Cursor page request failed with status ${response.status}`, + ); } const page = (await response.json()) as CursorPageResponse; diff --git a/common/core/pagination/index.ts b/common/core/pagination/index.ts index 585f7d6d..0e51e699 100644 --- a/common/core/pagination/index.ts +++ b/common/core/pagination/index.ts @@ -1,6 +1,6 @@ export { - fetchAllCursorPages, - fetchAllCursorPagesMainThread, type CursorFetchRequest, type CursorPageResponse, + fetchAllCursorPages, + fetchAllCursorPagesMainThread, } from "./cursorFetch"; diff --git a/common/package-lock.json b/common/package-lock.json index c0ce14f5..277dff24 100644 --- a/common/package-lock.json +++ b/common/package-lock.json @@ -29,7 +29,7 @@ "zod": "^3.24.1" }, "devDependencies": { - "@biomejs/biome": "^1.9.4", + "@biomejs/biome": "2.4.16", "@playwright/test": "^1.58.0", "@types/node": "^24.10.1", "@types/react": "^19.2.5", @@ -114,11 +114,10 @@ "license": "MIT" }, "node_modules/@biomejs/biome": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", - "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz", + "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==", "dev": true, - "hasInstallScript": true, "license": "MIT OR Apache-2.0", "bin": { "biome": "bin/biome" @@ -131,20 +130,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.9.4", - "@biomejs/cli-darwin-x64": "1.9.4", - "@biomejs/cli-linux-arm64": "1.9.4", - "@biomejs/cli-linux-arm64-musl": "1.9.4", - "@biomejs/cli-linux-x64": "1.9.4", - "@biomejs/cli-linux-x64-musl": "1.9.4", - "@biomejs/cli-win32-arm64": "1.9.4", - "@biomejs/cli-win32-x64": "1.9.4" + "@biomejs/cli-darwin-arm64": "2.4.16", + "@biomejs/cli-darwin-x64": "2.4.16", + "@biomejs/cli-linux-arm64": "2.4.16", + "@biomejs/cli-linux-arm64-musl": "2.4.16", + "@biomejs/cli-linux-x64": "2.4.16", + "@biomejs/cli-linux-x64-musl": "2.4.16", + "@biomejs/cli-win32-arm64": "2.4.16", + "@biomejs/cli-win32-x64": "2.4.16" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", - "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz", + "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==", "cpu": [ "arm64" ], @@ -159,9 +158,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", - "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz", + "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==", "cpu": [ "x64" ], @@ -176,9 +175,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", - "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz", + "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==", "cpu": [ "arm64" ], @@ -196,9 +195,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", - "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz", + "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==", "cpu": [ "arm64" ], @@ -216,9 +215,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", - "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz", + "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==", "cpu": [ "x64" ], @@ -236,9 +235,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", - "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz", + "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==", "cpu": [ "x64" ], @@ -256,9 +255,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", - "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz", + "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==", "cpu": [ "arm64" ], @@ -273,9 +272,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", - "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz", + "integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==", "cpu": [ "x64" ], diff --git a/common/package.json b/common/package.json index 50421fbe..186734ca 100644 --- a/common/package.json +++ b/common/package.json @@ -4,45 +4,47 @@ "scripts": { "dev:all": "pnpm -r run dev", "build:all": "pnpm -r run build", + "lint": "biome check .", + "lint:fix": "biome check . --write", "lint:all": "pnpm -r run lint" }, "devDependencies": { - "@biomejs/biome": "^1.9.4", + "@biomejs/biome": "2.4.16", "@playwright/test": "^1.58.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.4.23", + "jsdom": "^28.1.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.14", "tailwindcss-animate": "^1.0.7", "typescript": "~5.9.3", "vite": "^8.0.3", - "vitest": "^4.1.5", - "@types/node": "^24.10.1", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "jsdom": "^28.1.0" + "vitest": "^4.1.5" }, "dependencies": { - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-router-dom": "^6.28.2", - "@tanstack/react-query": "^5.66.8", - "@tanstack/react-query-devtools": "^5.66.8", - "axios": "^1.7.9", - "lucide-react": "^0.563.0", - "clsx": "^2.1.1", - "tailwind-merge": "^3.4.0", - "class-variance-authority": "^0.7.1", - "zod": "^3.24.1", - "react-hook-form": "^7.71.1", - "oidc-client-ts": "^3.4.1", - "react-oidc-context": "^3.3.0", "@radix-ui/react-avatar": "^1.1.4", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-scroll-area": "^1.1.2", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-switch": "^1.1.2" + "@radix-ui/react-switch": "^1.1.2", + "@tanstack/react-query": "^5.66.8", + "@tanstack/react-query-devtools": "^5.66.8", + "axios": "^1.7.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", + "oidc-client-ts": "^3.4.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.71.1", + "react-oidc-context": "^3.3.0", + "react-router-dom": "^6.28.2", + "tailwind-merge": "^3.4.0", + "zod": "^3.24.1" } } diff --git a/common/pnpm-lock.yaml b/common/pnpm-lock.yaml index 703f94e3..2771c452 100644 --- a/common/pnpm-lock.yaml +++ b/common/pnpm-lock.yaml @@ -73,8 +73,8 @@ importers: version: 3.25.76 devDependencies: '@biomejs/biome': - specifier: ^1.9.4 - version: 1.9.4 + specifier: 2.4.16 + version: 2.4.16 '@playwright/test': specifier: ^1.58.0 version: 1.60.0 @@ -113,7 +113,7 @@ importers: version: 8.0.12(@types/node@24.12.4)(jiti@1.21.7) vitest: specifier: ^4.1.5 - version: 4.1.6(@types/node@24.12.4)(jsdom@28.1.0)(vite@8.0.12(@types/node@24.12.4)(jiti@1.21.7)) + version: 4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.12(@types/node@24.12.4)(jiti@1.21.7)) ../adminfront: dependencies: @@ -184,6 +184,9 @@ importers: specifier: ^4.4.3 version: 4.4.3 devDependencies: + '@biomejs/biome': + specifier: 2.4.16 + version: 2.4.16 '@playwright/test': specifier: ^1.60.0 version: 1.60.0 @@ -211,12 +214,18 @@ importers: '@vitejs/plugin-react': specifier: ^6.0.1 version: 6.0.1(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)) + '@vitest/coverage-v8': + specifier: 4.1.6 + version: 4.1.6(vitest@4.1.6) autoprefixer: specifier: ^10.5.0 version: 10.5.0(postcss@8.5.14) jsdom: specifier: ^28.1.0 version: 28.1.0 + playwright: + specifier: 1.60.0 + version: 1.60.0 postcss: specifier: ^8.5.14 version: 8.5.14 @@ -234,7 +243,7 @@ importers: version: 8.0.14(@types/node@25.7.0)(jiti@1.21.7) vitest: specifier: ^4.1.6 - version: 4.1.6(@types/node@25.7.0)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)) + version: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)) ../devfront: dependencies: @@ -302,6 +311,9 @@ importers: specifier: ^4.4.3 version: 4.4.3 devDependencies: + '@biomejs/biome': + specifier: 2.4.16 + version: 2.4.16 '@playwright/test': specifier: ^1.60.0 version: 1.60.0 @@ -316,10 +328,16 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7)) + version: 6.0.1(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)) + '@vitest/coverage-v8': + specifier: 4.1.6 + version: 4.1.6(vitest@4.1.6) autoprefixer: specifier: ^10.5.0 version: 10.5.0(postcss@8.5.14) + jsdom: + specifier: ^28.1.0 + version: 28.1.0 postcss: specifier: ^8.5.14 version: 8.5.14 @@ -333,11 +351,11 @@ importers: specifier: ^6.0.3 version: 6.0.3 vite: - specifier: ^8.0.12 - version: 8.0.12(@types/node@25.7.0)(jiti@1.21.7) + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.7.0)(jiti@1.21.7) vitest: specifier: ^4.1.6 - version: 4.1.6(@types/node@25.7.0)(jsdom@28.1.0)(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7)) + version: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)) ../orgfront: dependencies: @@ -408,6 +426,9 @@ importers: specifier: ^4.4.3 version: 4.4.3 devDependencies: + '@biomejs/biome': + specifier: 2.4.16 + version: 2.4.16 '@playwright/test': specifier: ^1.60.0 version: 1.60.0 @@ -422,7 +443,10 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7)) + version: 6.0.1(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)) + '@vitest/coverage-v8': + specifier: 4.1.6 + version: 4.1.6(vitest@4.1.6) autoprefixer: specifier: ^10.5.0 version: 10.5.0(postcss@8.5.14) @@ -442,11 +466,11 @@ importers: specifier: ^6.0.3 version: 6.0.3 vite: - specifier: ^8.0.12 - version: 8.0.12(@types/node@25.7.0)(jiti@1.21.7) + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.7.0)(jiti@1.21.7) vitest: specifier: ^4.1.6 - version: 4.1.6(@types/node@25.7.0)(jsdom@28.1.0)(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7)) + version: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)) packages: @@ -478,67 +502,84 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} - '@biomejs/biome@1.9.4': - resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@biomejs/biome@2.4.16': + resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@1.9.4': - resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + '@biomejs/cli-darwin-arm64@2.4.16': + resolution: {integrity: sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@1.9.4': - resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + '@biomejs/cli-darwin-x64@2.4.16': + resolution: {integrity: sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@1.9.4': - resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + '@biomejs/cli-linux-arm64-musl@2.4.16': + resolution: {integrity: sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@1.9.4': - resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + '@biomejs/cli-linux-arm64@2.4.16': + resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@1.9.4': - resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + '@biomejs/cli-linux-x64-musl@2.4.16': + resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@1.9.4': - resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + '@biomejs/cli-linux-x64@2.4.16': + resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@1.9.4': - resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + '@biomejs/cli-win32-arm64@2.4.16': + resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@1.9.4': - resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + '@biomejs/cli-win32-x64@2.4.16': + resolution: {integrity: sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -1381,6 +1422,15 @@ packages: babel-plugin-react-compiler: optional: true + '@vitest/coverage-v8@4.1.6': + resolution: {integrity: sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==} + peerDependencies: + '@vitest/browser': 4.1.6 + vitest: 4.1.6 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@4.1.6': resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} @@ -1460,6 +1510,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-v8-to-istanbul@1.0.2: + resolution: {integrity: sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1755,6 +1808,10 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1771,6 +1828,9 @@ packages: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1810,10 +1870,25 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1932,6 +2007,13 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2225,6 +2307,11 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -2250,6 +2337,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -2577,47 +2668,60 @@ snapshots: '@babel/code-frame@7.29.0': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 '@babel/runtime@7.29.2': {} - '@biomejs/biome@1.9.4': + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@biomejs/biome@2.4.16': optionalDependencies: - '@biomejs/cli-darwin-arm64': 1.9.4 - '@biomejs/cli-darwin-x64': 1.9.4 - '@biomejs/cli-linux-arm64': 1.9.4 - '@biomejs/cli-linux-arm64-musl': 1.9.4 - '@biomejs/cli-linux-x64': 1.9.4 - '@biomejs/cli-linux-x64-musl': 1.9.4 - '@biomejs/cli-win32-arm64': 1.9.4 - '@biomejs/cli-win32-x64': 1.9.4 + '@biomejs/cli-darwin-arm64': 2.4.16 + '@biomejs/cli-darwin-x64': 2.4.16 + '@biomejs/cli-linux-arm64': 2.4.16 + '@biomejs/cli-linux-arm64-musl': 2.4.16 + '@biomejs/cli-linux-x64': 2.4.16 + '@biomejs/cli-linux-x64-musl': 2.4.16 + '@biomejs/cli-win32-arm64': 2.4.16 + '@biomejs/cli-win32-x64': 2.4.16 - '@biomejs/cli-darwin-arm64@1.9.4': + '@biomejs/cli-darwin-arm64@2.4.16': optional: true - '@biomejs/cli-darwin-x64@1.9.4': + '@biomejs/cli-darwin-x64@2.4.16': optional: true - '@biomejs/cli-linux-arm64-musl@1.9.4': + '@biomejs/cli-linux-arm64-musl@2.4.16': optional: true - '@biomejs/cli-linux-arm64@1.9.4': + '@biomejs/cli-linux-arm64@2.4.16': optional: true - '@biomejs/cli-linux-x64-musl@1.9.4': + '@biomejs/cli-linux-x64-musl@2.4.16': optional: true - '@biomejs/cli-linux-x64@1.9.4': + '@biomejs/cli-linux-x64@2.4.16': optional: true - '@biomejs/cli-win32-arm64@1.9.4': + '@biomejs/cli-win32-arm64@2.4.16': optional: true - '@biomejs/cli-win32-x64@1.9.4': + '@biomejs/cli-win32-x64@2.4.16': optional: true '@bramus/specificity@2.4.2': @@ -3340,16 +3444,25 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.12(@types/node@24.12.4)(jiti@1.21.7) - '@vitejs/plugin-react@6.0.1(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7))': - dependencies: - '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.12(@types/node@25.7.0)(jiti@1.21.7) - '@vitejs/plugin-react@6.0.1(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.14(@types/node@25.7.0)(jiti@1.21.7) + '@vitest/coverage-v8@4.1.6(vitest@4.1.6)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.6 + ast-v8-to-istanbul: 1.0.2 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.1 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)) + '@vitest/expect@4.1.6': dependencies: '@standard-schema/spec': 1.1.0 @@ -3367,14 +3480,6 @@ snapshots: optionalDependencies: vite: 8.0.12(@types/node@24.12.4)(jiti@1.21.7) - '@vitest/mocker@4.1.6(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7))': - dependencies: - '@vitest/spy': 4.1.6 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.12(@types/node@25.7.0)(jiti@1.21.7) - '@vitest/mocker@4.1.6(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7))': dependencies: '@vitest/spy': 4.1.6 @@ -3463,6 +3568,12 @@ snapshots: assertion-error@2.0.1: {} + ast-v8-to-istanbul@1.0.2: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + asynckit@0.4.0: {} autoprefixer@10.5.0(postcss@8.5.14): @@ -3741,6 +3852,8 @@ snapshots: gopd@1.2.0: {} + has-flag@4.0.0: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -3757,6 +3870,8 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + html-escaper@2.0.2: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -3798,8 +3913,23 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jiti@1.21.7: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} jsdom@28.1.0: @@ -3900,6 +4030,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.1 + math-intrinsics@1.1.0: {} mdn-data@2.27.1: {} @@ -4171,6 +4311,8 @@ snapshots: scheduler@0.27.0: {} + semver@7.8.1: {} + set-cookie-parser@2.7.2: {} siginfo@2.0.0: {} @@ -4195,6 +4337,10 @@ snapshots: tinyglobby: 0.2.16 ts-interface-checker: 0.1.13 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} symbol-tree@3.2.4: {} @@ -4323,18 +4469,6 @@ snapshots: fsevents: 2.3.3 jiti: 1.21.7 - vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.14 - rolldown: 1.0.0 - tinyglobby: 0.2.16 - optionalDependencies: - '@types/node': 25.7.0 - fsevents: 2.3.3 - jiti: 1.21.7 - vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7): dependencies: lightningcss: 1.32.0 @@ -4347,7 +4481,7 @@ snapshots: fsevents: 2.3.3 jiti: 1.21.7 - vitest@4.1.6(@types/node@24.12.4)(jsdom@28.1.0)(vite@8.0.12(@types/node@24.12.4)(jiti@1.21.7)): + vitest@4.1.6(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.12(@types/node@24.12.4)(jiti@1.21.7)): dependencies: '@vitest/expect': 4.1.6 '@vitest/mocker': 4.1.6(vite@8.0.12(@types/node@24.12.4)(jiti@1.21.7)) @@ -4371,39 +4505,12 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.12.4 + '@vitest/coverage-v8': 4.1.6(vitest@4.1.6) jsdom: 28.1.0 transitivePeerDependencies: - msw - vitest@4.1.6(@types/node@25.7.0)(jsdom@28.1.0)(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7)): - dependencies: - '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@8.0.12(@types/node@25.7.0)(jiti@1.21.7)) - '@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.12(@types/node@25.7.0)(jiti@1.21.7) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 25.7.0 - jsdom: 28.1.0 - transitivePeerDependencies: - - msw - - vitest@4.1.6(@types/node@25.7.0)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)): + vitest@4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)): dependencies: '@vitest/expect': 4.1.6 '@vitest/mocker': 4.1.6(vite@8.0.14(@types/node@25.7.0)(jiti@1.21.7)) @@ -4427,6 +4534,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.7.0 + '@vitest/coverage-v8': 4.1.6(vitest@4.1.6) jsdom: 28.1.0 transitivePeerDependencies: - msw diff --git a/common/shell/index.ts b/common/shell/index.ts index f7cea93b..2de0b142 100644 --- a/common/shell/index.ts +++ b/common/shell/index.ts @@ -1,6 +1,6 @@ import { - SESSION_EXPIRY_STORAGE_KEY, readSessionExpiryEnabled, + SESSION_EXPIRY_STORAGE_KEY, writeSessionExpiryEnabled, } from "../core/session"; @@ -27,8 +27,8 @@ type ShellProfileSummaryParams = { export const SHELL_THEME_STORAGE_KEY = "admin_theme"; export const SHELL_SESSION_EXPIRY_STORAGE_KEY = SESSION_EXPIRY_STORAGE_KEY; -export { AppSidebar } from "./AppSidebar"; export type { ShellSidebarNavItem } from "./AppSidebar"; +export { AppSidebar } from "./AppSidebar"; export { shellLayoutClasses } from "./layout"; export function readShellTheme(): ShellTheme { diff --git a/devfront/biome.json b/devfront/biome.json index fb68b4da..66e0edd1 100644 --- a/devfront/biome.json +++ b/devfront/biome.json @@ -1,6 +1,4 @@ { - "extends": ["../common/config/biome.base.json"], - "files": { - "ignore": [".vite"] - } + "root": true, + "extends": ["../common/config/biome.base.json"] } diff --git a/devfront/package-lock.json b/devfront/package-lock.json index 694d478e..1774521d 100644 --- a/devfront/package-lock.json +++ b/devfront/package-lock.json @@ -31,23 +31,33 @@ "zod": "^4.4.3" }, "devDependencies": { + "@biomejs/biome": "2.4.16", "@playwright/test": "^1.60.0", "@types/node": "^25.7.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "4.1.6", "autoprefixer": "^10.5.0", + "jsdom": "^28.1.0", "postcss": "^8.5.14", "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "typescript": "^6.0.3", - "vite": "^8.0.12", + "vite": "^8.0.14", "vitest": "^4.1.6" }, "engines": { "node": ">=24.0.0" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "dev": true, @@ -59,6 +69,442 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz", + "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.16", + "@biomejs/cli-darwin-x64": "2.4.16", + "@biomejs/cli-linux-arm64": "2.4.16", + "@biomejs/cli-linux-arm64-musl": "2.4.16", + "@biomejs/cli-linux-x64": "2.4.16", + "@biomejs/cli-linux-x64-musl": "2.4.16", + "@biomejs/cli-win32-arm64": "2.4.16", + "@biomejs/cli-win32-x64": "2.4.16" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz", + "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz", + "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz", + "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz", + "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz", + "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz", + "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz", + "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz", + "integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", + "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -93,6 +539,24 @@ "tslib": "^2.4.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "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 + } + } + }, "node_modules/@floating-ui/core": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", @@ -214,9 +678,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.130.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", - "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", "dev": true, "license": "MIT", "funding": { @@ -1662,9 +2126,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", - "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", "cpu": [ "arm64" ], @@ -1679,9 +2143,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", - "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", "cpu": [ "arm64" ], @@ -1696,9 +2160,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", - "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", "cpu": [ "x64" ], @@ -1713,9 +2177,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", - "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", "cpu": [ "x64" ], @@ -1730,9 +2194,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", - "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", "cpu": [ "arm" ], @@ -1747,13 +2211,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", - "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1764,13 +2231,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", - "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1781,13 +2251,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", - "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1798,13 +2271,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", - "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1815,13 +2291,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", - "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1832,13 +2311,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", - "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1849,9 +2331,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", - "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", "cpu": [ "arm64" ], @@ -1866,9 +2348,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", - "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", "cpu": [ "wasm32" ], @@ -1885,9 +2367,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", - "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", "cpu": [ "arm64" ], @@ -1902,9 +2384,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", - "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", "cpu": [ "x64" ], @@ -2069,6 +2551,37 @@ } } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.6.tgz", + "integrity": "sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.6", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.6", + "vitest": "4.1.6" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", @@ -2182,6 +2695,16 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/any-promise": { "version": "1.3.0", "dev": true, @@ -2226,6 +2749,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.2.tgz", + "integrity": "sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "license": "MIT" @@ -2317,6 +2852,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "dev": true, @@ -2512,6 +3057,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "dev": true, @@ -2523,11 +3082,41 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/csstype": { "version": "3.2.3", "devOptional": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "license": "MIT", @@ -2543,6 +3132,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "license": "MIT", @@ -2593,6 +3189,19 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "license": "MIT", @@ -2839,6 +3448,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "license": "MIT", @@ -2872,6 +3491,54 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "dev": true, @@ -2924,6 +3591,52 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jiti": { "version": "1.21.7", "dev": true, @@ -2932,6 +3645,54 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jwt-decode": { "version": "4.0.0", "license": "MIT", @@ -3216,6 +3977,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/lucide-react": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz", @@ -3235,6 +4006,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "license": "MIT", @@ -3242,6 +4041,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "dev": true, @@ -3294,7 +4100,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -3362,6 +4170,19 @@ "node": ">=18" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-parse": { "version": "1.0.7", "dev": true, @@ -3439,9 +4260,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -3459,7 +4280,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3598,6 +4419,16 @@ "node": ">=10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "dev": true, @@ -3793,6 +4624,16 @@ "node": ">=8.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "dev": true, @@ -3822,13 +4663,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", - "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.130.0", + "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -3838,21 +4679,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.1", - "@rolldown/binding-darwin-arm64": "1.0.1", - "@rolldown/binding-darwin-x64": "1.0.1", - "@rolldown/binding-freebsd-x64": "1.0.1", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", - "@rolldown/binding-linux-arm64-gnu": "1.0.1", - "@rolldown/binding-linux-arm64-musl": "1.0.1", - "@rolldown/binding-linux-ppc64-gnu": "1.0.1", - "@rolldown/binding-linux-s390x-gnu": "1.0.1", - "@rolldown/binding-linux-x64-gnu": "1.0.1", - "@rolldown/binding-linux-x64-musl": "1.0.1", - "@rolldown/binding-openharmony-arm64": "1.0.1", - "@rolldown/binding-wasm32-wasi": "1.0.1", - "@rolldown/binding-win32-arm64-msvc": "1.0.1", - "@rolldown/binding-win32-x64-msvc": "1.0.1" + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { @@ -3884,10 +4725,36 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -3938,6 +4805,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "dev": true, @@ -3949,6 +4829,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", @@ -4091,6 +4978,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.0.tgz", + "integrity": "sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.0" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.1.tgz", + "integrity": "sha512-sc2nGvGbixlJRHwTh/qQdPXTxJU1UDJboGPQm4d/01YUJ9r/u6aeIulQvEaxUlvKDN7hb1qCLjax+jhVAPLa/g==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "dev": true, @@ -4102,6 +5009,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "dev": true, @@ -4127,6 +5060,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz", + "integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", @@ -4221,16 +5164,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.13", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", - "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "rolldown": "1.0.1", + "postcss": "^8.5.15", + "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "bin": { @@ -4425,6 +5368,54 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "dev": true, @@ -4440,6 +5431,23 @@ "node": ">=8" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/devfront/package.json b/devfront/package.json index 6576b50b..8bdfe01f 100644 --- a/devfront/package.json +++ b/devfront/package.json @@ -12,22 +12,26 @@ "lint": "biome check .", "preview": "vite preview", "test": "playwright test", + "test:coverage": "vitest run --coverage", "test:unit": "vitest run", "test:roles": "playwright test tests/devfront-role-switch-report.spec.ts", "test:ui": "playwright test --ui" }, "devDependencies": { + "@biomejs/biome": "2.4.16", "@playwright/test": "^1.60.0", "@types/node": "^25.7.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@vitest/coverage-v8": "4.1.6", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.5.0", + "jsdom": "^28.1.0", "postcss": "^8.5.14", "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "typescript": "^6.0.3", - "vite": "^8.0.12", + "vite": "^8.0.14", "vitest": "^4.1.6" }, "dependencies": { diff --git a/devfront/pnpm-lock.yaml b/devfront/pnpm-lock.yaml index 0bc721c8..4cbaffd7 100644 --- a/devfront/pnpm-lock.yaml +++ b/devfront/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: specifier: ^4.4.3 version: 4.4.3 devDependencies: + '@biomejs/biome': + specifier: 2.4.16 + version: 2.4.16 '@playwright/test': specifier: ^1.60.0 version: 1.60.0 @@ -87,9 +90,15 @@ importers: '@vitejs/plugin-react': specifier: ^6.0.1 version: 6.0.1(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7)) + '@vitest/coverage-v8': + specifier: 4.1.6 + version: 4.1.6(vitest@4.1.6) autoprefixer: specifier: ^10.5.0 version: 10.5.0(postcss@8.5.14) + jsdom: + specifier: ^28.1.0 + version: 28.1.0 postcss: specifier: ^8.5.14 version: 8.5.14 @@ -107,14 +116,149 @@ importers: version: 8.0.13(@types/node@25.7.0)(jiti@1.21.7) vitest: specifier: ^4.1.6 - version: 4.1.6(@types/node@25.7.0)(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7)) + version: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7)) packages: + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@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@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@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/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@biomejs/biome@2.4.16': + resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.16': + resolution: {integrity: sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.16': + resolution: {integrity: sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.16': + resolution: {integrity: sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.16': + resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.16': + resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.16': + resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.16': + resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.16': + resolution: {integrity: sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@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.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + 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.1': + resolution: {integrity: sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==} + 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.4': + resolution: {integrity: sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==} + 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.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -124,6 +268,15 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + 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==} @@ -731,6 +884,15 @@ packages: babel-plugin-react-compiler: optional: true + '@vitest/coverage-v8@4.1.6': + resolution: {integrity: sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==} + peerDependencies: + '@vitest/browser': 4.1.6 + vitest: 4.1.6 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@4.1.6': resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} @@ -764,6 +926,10 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -782,6 +948,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-v8-to-istanbul@1.0.2: + resolution: {integrity: sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -800,6 +969,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -854,14 +1026,26 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + cssstyle@6.2.0: + resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==} + engines: {node: '>=20'} + 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'} @@ -871,6 +1055,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -895,6 +1082,10 @@ packages: electron-to-chromium@1.5.355: resolution: {integrity: sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -998,6 +1189,10 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1010,10 +1205,25 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1034,10 +1244,37 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + jsdom@28.1.0: + resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jwt-decode@4.0.0: resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} engines: {node: '>=18'} @@ -1123,6 +1360,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + lucide-react@1.14.0: resolution: {integrity: sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==} peerDependencies: @@ -1131,10 +1372,20 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1184,6 +1435,9 @@ packages: resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==} engines: {node: '>=18'} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -1270,6 +1524,10 @@ packages: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1349,6 +1607,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -1366,9 +1628,18 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + 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==} + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -1390,10 +1661,17 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} @@ -1429,10 +1707,25 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tldts-core@7.4.0: + resolution: {integrity: sha512-/mb9kRld+x1sIMXxWNOAp5m6C+D4GrAORWlJkOJ5dElvxdN1eutz/o7qHLp9gFvDF4Y3/L2xeScoxz6AbEo8rQ==} + + tldts@7.4.0: + resolution: {integrity: sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + 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-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -1447,6 +1740,10 @@ packages: undici-types@7.21.0: resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} + undici@7.26.0: + resolution: {integrity: sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==} + engines: {node: '>=20.18.1'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1565,18 +1862,141 @@ packages: 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} + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + 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==} + zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} snapshots: + '@acemir/cssom@0.9.31': {} + '@alloc/quick-lru@5.2.0': {} + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.1(@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.1(@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@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@biomejs/biome@2.4.16': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.16 + '@biomejs/cli-darwin-x64': 2.4.16 + '@biomejs/cli-linux-arm64': 2.4.16 + '@biomejs/cli-linux-arm64-musl': 2.4.16 + '@biomejs/cli-linux-x64': 2.4.16 + '@biomejs/cli-linux-x64-musl': 2.4.16 + '@biomejs/cli-win32-arm64': 2.4.16 + '@biomejs/cli-win32-x64': 2.4.16 + + '@biomejs/cli-darwin-arm64@2.4.16': + optional: true + + '@biomejs/cli-darwin-x64@2.4.16': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.16': + optional: true + + '@biomejs/cli-linux-arm64@2.4.16': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.16': + optional: true + + '@biomejs/cli-linux-x64@2.4.16': + optional: true + + '@biomejs/cli-win32-arm64@2.4.16': + optional: true + + '@biomejs/cli-win32-x64@2.4.16': + optional: true + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.1(@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.1(@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.1(@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.4(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -1593,6 +2013,8 @@ snapshots: tslib: 2.8.1 optional: true + '@exodus/bytes@1.15.1': {} + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -2132,6 +2554,20 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.13(@types/node@25.7.0)(jiti@1.21.7) + '@vitest/coverage-v8@4.1.6(vitest@4.1.6)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.6 + ast-v8-to-istanbul: 1.0.2 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.1 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7)) + '@vitest/expect@4.1.6': dependencies: '@standard-schema/spec': 1.1.0 @@ -2179,6 +2615,8 @@ snapshots: transitivePeerDependencies: - supports-color + agent-base@7.1.4: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -2194,6 +2632,12 @@ snapshots: assertion-error@2.0.1: {} + ast-v8-to-istanbul@1.0.2: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + asynckit@0.4.0: {} autoprefixer@10.5.0(postcss@8.5.14): @@ -2217,6 +2661,10 @@ snapshots: baseline-browser-mapping@2.10.29: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} braces@3.0.3: @@ -2270,14 +2718,35 @@ snapshots: cookie@1.1.1: {} + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + cssesc@3.0.0: {} + cssstyle@6.2.0: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@csstools/css-syntax-patches-for-csstree': 1.1.4(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.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: {} + delayed-stream@1.0.0: {} detect-libc@2.1.2: {} @@ -2296,6 +2765,8 @@ snapshots: electron-to-chromium@1.5.355: {} + entities@8.0.0: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -2391,6 +2862,8 @@ snapshots: gopd@1.2.0: {} + has-flag@4.0.0: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -2401,6 +2874,21 @@ snapshots: dependencies: function-bind: 1.1.2 + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + + html-escaper@2.0.2: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -2408,6 +2896,13 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -2424,8 +2919,52 @@ snapshots: is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jiti@1.21.7: {} + js-tokens@10.0.0: {} + + jsdom@28.1.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@bramus/specificity': 2.4.2 + '@exodus/bytes': 1.15.1 + cssstyle: 6.2.0 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.26.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' + - supports-color + jwt-decode@4.0.0: {} lightningcss-android-arm64@1.32.0: @@ -2481,6 +3020,8 @@ snapshots: lines-and-columns@1.2.4: {} + lru-cache@11.5.1: {} + lucide-react@1.14.0(react@19.2.6): dependencies: react: 19.2.6 @@ -2489,8 +3030,20 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.1 + math-intrinsics@1.1.0: {} + mdn-data@2.27.1: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -2528,6 +3081,10 @@ snapshots: dependencies: jwt-decode: 4.0.0 + parse5@8.0.1: + dependencies: + entities: 8.0.0 + path-parse@1.0.7: {} pathe@2.0.3: {} @@ -2589,6 +3146,8 @@ snapshots: proxy-from-env@2.1.0: {} + punycode@2.3.1: {} + queue-microtask@1.2.3: {} react-dom@19.2.6(react@19.2.6): @@ -2656,6 +3215,8 @@ snapshots: dependencies: picomatch: 2.3.2 + require-from-string@2.0.2: {} + resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -2690,8 +3251,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} + semver@7.8.1: {} + set-cookie-parser@2.7.2: {} siginfo@2.0.0: {} @@ -2712,8 +3279,14 @@ snapshots: tinyglobby: 0.2.16 ts-interface-checker: 0.1.13 + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} + tailwind-merge@3.6.0: {} tailwindcss-animate@1.0.7(tailwindcss@3.4.19): @@ -2767,10 +3340,24 @@ snapshots: tinyrainbow@3.1.0: {} + tldts-core@7.4.0: {} + + tldts@7.4.0: + dependencies: + tldts-core: 7.4.0 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tough-cookie@6.0.1: + dependencies: + tldts: 7.4.0 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + ts-interface-checker@0.1.13: {} tslib@2.8.1: {} @@ -2779,6 +3366,8 @@ snapshots: undici-types@7.21.0: {} + undici@7.26.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -2818,7 +3407,7 @@ snapshots: fsevents: 2.3.3 jiti: 1.21.7 - vitest@4.1.6(@types/node@25.7.0)(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7)): + vitest@4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@28.1.0)(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7)): dependencies: '@vitest/expect': 4.1.6 '@vitest/mocker': 4.1.6(vite@8.0.13(@types/node@25.7.0)(jiti@1.21.7)) @@ -2842,12 +3431,34 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.7.0 + '@vitest/coverage-v8': 4.1.6(vitest@4.1.6) + jsdom: 28.1.0 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.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + zod@4.4.3: {} diff --git a/devfront/src/app/routes.tsx b/devfront/src/app/routes.tsx index b6e091a4..5810f08b 100644 --- a/devfront/src/app/routes.tsx +++ b/devfront/src/app/routes.tsx @@ -1,4 +1,4 @@ -import { type RouteObject, createBrowserRouter } from "react-router-dom"; +import { createBrowserRouter, type RouteObject } from "react-router-dom"; import AppLayout from "../components/layout/AppLayout"; import AuditLogsPage from "../features/audit/AuditLogsPage"; import AuthCallbackPage from "../features/auth/AuthCallbackPage"; diff --git a/devfront/src/components/layout/AppLayout.tsx b/devfront/src/components/layout/AppLayout.tsx index aac3fcce..f12a527d 100644 --- a/devfront/src/components/layout/AppLayout.tsx +++ b/devfront/src/components/layout/AppLayout.tsx @@ -15,13 +15,13 @@ import { useAuth } from "react-oidc-context"; import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { AppSidebar, - type ShellSidebarNavItem, - type ShellTranslator, applyShellTheme, buildShellProfileSummary, buildShellSessionStatus, readShellSessionExpiryEnabled, readShellTheme, + type ShellSidebarNavItem, + type ShellTranslator, shellLayoutClasses, writeShellSessionExpiryEnabled, } from "../../../../common/shell"; @@ -156,9 +156,12 @@ function AppLayout() { window.addEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell); return () => { - window.removeEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell); + window.removeEventListener( + LOCALE_CHANGED_EVENT, + rerenderDevelopmentShell, + ); }; - }, [isDevelopmentRuntime]); + }, []); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -271,7 +274,6 @@ function AppLayout() { auth.isAuthenticated, auth.isLoading, auth.user?.expires_at, - isDevelopmentRuntime, isSessionExpiryEnabled, ]); @@ -481,7 +483,10 @@ function AppLayout() {

- {t("ui.shell.session.auto_extend", "Session expiry")} + {t( + "ui.shell.session.auto_extend", + "Session expiry", + )}

{isSessionExpiryEnabled ? ( diff --git a/devfront/src/components/ui/avatar.tsx b/devfront/src/components/ui/avatar.tsx index 23e88913..ab415563 100644 --- a/devfront/src/components/ui/avatar.tsx +++ b/devfront/src/components/ui/avatar.tsx @@ -44,4 +44,4 @@ const AvatarFallback = React.forwardRef< )); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; -export { Avatar, AvatarImage, AvatarFallback }; +export { Avatar, AvatarFallback, AvatarImage }; diff --git a/devfront/src/components/ui/card.tsx b/devfront/src/components/ui/card.tsx index 33685c34..246b5289 100644 --- a/devfront/src/components/ui/card.tsx +++ b/devfront/src/components/ui/card.tsx @@ -50,9 +50,9 @@ function CardFooter({ export { Card, + CardContent, + CardDescription, + CardFooter, CardHeader, CardTitle, - CardDescription, - CardContent, - CardFooter, }; diff --git a/devfront/src/components/ui/table.tsx b/devfront/src/components/ui/table.tsx index 0b0022a5..33663b2e 100644 --- a/devfront/src/components/ui/table.tsx +++ b/devfront/src/components/ui/table.tsx @@ -92,11 +92,11 @@ TableCaption.displayName = "TableCaption"; export { Table, - TableHeader, TableBody, + TableCaption, + TableCell, TableFooter, TableHead, + TableHeader, TableRow, - TableCell, - TableCaption, }; diff --git a/devfront/src/features/auth/LoginPage.tsx b/devfront/src/features/auth/LoginPage.tsx index 03d80002..b1908eaf 100644 --- a/devfront/src/features/auth/LoginPage.tsx +++ b/devfront/src/features/auth/LoginPage.tsx @@ -1,8 +1,7 @@ import { AlertTriangle, ExternalLink, LogIn, ShieldHalf } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { useAuth } from "react-oidc-context"; -import { useNavigate } from "react-router-dom"; -import { useSearchParams } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "../../components/ui/button"; import { Card, diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index dd6e2788..984d0382 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -14,7 +14,7 @@ import { Upload, X, } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useAuth } from "react-oidc-context"; import { Link, useNavigate, useParams } from "react-router-dom"; import { PageHeader } from "../../../../common/core/components/page"; @@ -32,6 +32,13 @@ import { Label } from "../../components/ui/label"; import { Switch } from "../../components/ui/switch"; import { Textarea } from "../../components/ui/textarea"; import { toast } from "../../components/ui/use-toast"; +import type { + ClientStatus, + ClientType, + ClientUpsertRequest, + MyTenantSummary, + TenantSummary, +} from "../../lib/devApi"; import { type ClientRelation, createClient, @@ -44,13 +51,6 @@ import { updateClient, updateClientStatus, } from "../../lib/devApi"; -import type { - ClientStatus, - ClientType, - ClientUpsertRequest, - MyTenantSummary, - TenantSummary, -} from "../../lib/devApi"; import { t } from "../../lib/i18n"; import { resolveProfileRole } from "../../lib/role"; import { cn } from "../../lib/utils"; @@ -408,6 +408,59 @@ function ClientGeneralPage() { ]); const [idTokenClaims, setIdTokenClaims] = useState([]); + const tenantScopeDescription = t( + "msg.dev.clients.scopes.tenant", + "소속 테넌트 정보 접근", + ); + + const buildTenantScope = useCallback( + (id: string): ScopeItem => ({ + id, + name: "tenant", + description: tenantScopeDescription, + mandatory: true, + locked: true, + }), + [tenantScopeDescription], + ); + + const normalizeScopesForTenantAccess = useCallback( + (nextScopes: ScopeItem[], restricted: boolean): ScopeItem[] => { + const normalized = nextScopes.map((scope) => { + if (scope.name.trim() !== "tenant") { + return scope; + } + return { + ...scope, + description: scope.description || tenantScopeDescription, + mandatory: restricted, + locked: restricted, + }; + }); + + if ( + restricted && + !normalized.some((scope) => scope.name.trim() === "tenant") + ) { + normalized.push(buildTenantScope(`tenant-${Date.now()}`)); + } + + const openidScopes = normalized.filter( + (scope) => scope.name.trim() === "openid", + ); + const tenantScopes = normalized.filter( + (scope) => scope.name.trim() === "tenant", + ); + const remainingScopes = normalized.filter((scope) => { + const name = scope.name.trim(); + return name !== "openid" && name !== "tenant"; + }); + + return [...openidScopes, ...tenantScopes, ...remainingScopes]; + }, + [buildTenantScope, tenantScopeDescription], + ); + useEffect(() => { if (!data) return; const { client } = data; @@ -511,7 +564,7 @@ function ClientGeneralPage() { ); } setIdTokenClaims(readIdTokenClaimsMetadata(metadata)); - }, [data]); + }, [data, normalizeScopesForTenantAccess]); const securityProfile: SecurityProfile = clientType === "pkce" ? "pkce" : "private"; @@ -574,56 +627,6 @@ function ClientGeneralPage() { } }; - const tenantScopeDescription = t( - "msg.dev.clients.scopes.tenant", - "소속 테넌트 정보 접근", - ); - - const buildTenantScope = (id: string): ScopeItem => ({ - id, - name: "tenant", - description: tenantScopeDescription, - mandatory: true, - locked: true, - }); - - function normalizeScopesForTenantAccess( - nextScopes: ScopeItem[], - restricted: boolean, - ): ScopeItem[] { - const normalized = nextScopes.map((scope) => { - if (scope.name.trim() !== "tenant") { - return scope; - } - return { - ...scope, - description: scope.description || tenantScopeDescription, - mandatory: restricted, - locked: restricted, - }; - }); - - if ( - restricted && - !normalized.some((scope) => scope.name.trim() === "tenant") - ) { - normalized.push(buildTenantScope(`tenant-${Date.now()}`)); - } - - const openidScopes = normalized.filter( - (scope) => scope.name.trim() === "openid", - ); - const tenantScopes = normalized.filter( - (scope) => scope.name.trim() === "tenant", - ); - const remainingScopes = normalized.filter((scope) => { - const name = scope.name.trim(); - return name !== "openid" && name !== "tenant"; - }); - - return [...openidScopes, ...tenantScopes, ...remainingScopes]; - } - const handleTenantAccessToggle = (enabled: boolean) => { setTenantAccessRestricted(enabled); setIsTenantSearchOpen(enabled); @@ -2307,7 +2310,7 @@ function ClientGeneralPage() { {securityProfile === "private" && ( -

e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} @@ -2335,7 +2338,7 @@ function ClientGeneralPage() { onCheckedChange={handleHeadlessToggle} disabled={isGeneralSettingsReadOnly} /> -
+ )} @@ -2674,104 +2677,102 @@ function ClientGeneralPage() {
{currentHeadlessJwksCache.parsedKeys?.length ? (
- {currentHeadlessJwksCache.parsedKeys.map( - (key, index) => { - const normalizedAlgorithm = key.alg?.trim() ?? ""; - const isMissingAlgorithm = - normalizedAlgorithm === ""; - const isUnsupportedAlgorithm = - !isMissingAlgorithm && - !HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET.has( - normalizedAlgorithm, - ); + {currentHeadlessJwksCache.parsedKeys.map((key) => { + const normalizedAlgorithm = key.alg?.trim() ?? ""; + const isMissingAlgorithm = + normalizedAlgorithm === ""; + const isUnsupportedAlgorithm = + !isMissingAlgorithm && + !HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET.has( + normalizedAlgorithm, + ); - return ( -
-
-
-

- KID -

-

- {key.kid || "-"} -

-
-
-

- KTY -

-

- {key.kty || "-"} -

-
-
-

- USE -

-

- {key.use || "-"} -

-
-
-

- ALG -

-

- {key.alg || - t( - "msg.dev.clients.general.public_key.cache.missing_algorithm_badge", - "알고리즘 미선언", - )} -

- {isMissingAlgorithm && ( -

- {t( - "msg.dev.clients.general.public_key.cache.missing_algorithm_reason", - "이 키는 `alg`가 비어 있어서 저장할 수 없습니다.", - )} -

- )} - {isUnsupportedAlgorithm && ( -

- {t( - "msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", - "이 알고리즘은 Headless Login에서 지원되지 않습니다.", - )} -

- )} -
-
-
+ return ( +
+
+

- {t( - "ui.dev.clients.general.public_key.cache.parsed_key_n", - "N", + KID +

+

+ {key.kid || "-"} +

+
+
+

+ KTY +

+

+ {key.kty || "-"} +

+
+
+

+ USE +

+

+ {key.use || "-"} +

+
+
+

+ ALG +

+

+ {key.alg || + t( + "msg.dev.clients.general.public_key.cache.missing_algorithm_badge", + "알고리즘 미선언", + )}

-

- {key.n || "-"} -

+ {isMissingAlgorithm && ( +

+ {t( + "msg.dev.clients.general.public_key.cache.missing_algorithm_reason", + "이 키는 `alg`가 비어 있어서 저장할 수 없습니다.", + )} +

+ )} + {isUnsupportedAlgorithm && ( +

+ {t( + "msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", + "이 알고리즘은 Headless Login에서 지원되지 않습니다.", + )} +

+ )}
- ); - }, - )} +
+

+ {t( + "ui.dev.clients.general.public_key.cache.parsed_key_n", + "N", + )} +

+

+ {key.n || "-"} +

+
+
+ ); + })}
) : (
diff --git a/devfront/src/features/clients/ClientRelationsPage.tsx b/devfront/src/features/clients/ClientRelationsPage.tsx index ae97fb02..c3d21d4b 100644 --- a/devfront/src/features/clients/ClientRelationsPage.tsx +++ b/devfront/src/features/clients/ClientRelationsPage.tsx @@ -26,8 +26,8 @@ import { } from "../../components/ui/table"; import { toast } from "../../components/ui/use-toast"; import { - type DevAssignableUser, addClientRelation, + type DevAssignableUser, fetchClient, fetchClientRelations, fetchDevUsers, @@ -355,7 +355,10 @@ function ClientRelationsPage() { } - title={t("ui.dev.clients.relationships.title", "Client Relationships")} + title={t( + "ui.dev.clients.relationships.title", + "Client Relationships", + )} description={t( "msg.dev.clients.relationships.subtitle", "RP direct operator relation을 조회하고 User 단위로 추가·삭제합니다.", diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index 27753102..edcd5edd 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -1,6 +1,13 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { BookOpenText, Filter, Plus, Search, ShieldHalf, X } from "lucide-react"; +import { + BookOpenText, + Filter, + Plus, + Search, + ShieldHalf, + X, +} from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useAuth } from "react-oidc-context"; import { Link, useNavigate } from "react-router-dom"; @@ -51,8 +58,8 @@ import { Textarea } from "../../components/ui/textarea"; import { type ClientSummary, fetchClients, - fetchDevStats, fetchDeveloperRequestStatus, + fetchDevStats, fetchMyTenants, requestDeveloperAccess, } from "../../lib/devApi"; @@ -97,8 +104,7 @@ function ClientsPage() { } = useQuery({ queryKey: ["developer-request", tenantId], queryFn: () => fetchDeveloperRequestStatus(tenantId), - enabled: - hasAccessToken && (role === "user" || role === "tenant_member"), + enabled: hasAccessToken && (role === "user" || role === "tenant_member"), }); const { data: tenants } = useQuery({ queryKey: ["myTenants"], diff --git a/devfront/src/features/clients/routes/ClientFederationPage.tsx b/devfront/src/features/clients/routes/ClientFederationPage.tsx index 8f8a2999..87e93e34 100644 --- a/devfront/src/features/clients/routes/ClientFederationPage.tsx +++ b/devfront/src/features/clients/routes/ClientFederationPage.tsx @@ -20,11 +20,11 @@ import { TableHeader, TableRow, } from "../../../components/ui/table"; +import type { IdpConfig, IdpConfigCreateRequest } from "../../../lib/devApi"; import { createIdpConfigForClient, listIdpConfigsForClient, } from "../../../lib/devApi"; -import type { IdpConfig, IdpConfigCreateRequest } from "../../../lib/devApi"; import { t } from "../../../lib/i18n"; // Proper Modal Component with Form @@ -178,9 +178,16 @@ const CreateIdpModal = ({ }; export function ClientFederationPage() { - const { id: clientId } = useParams<{ id: string }>(); + const { id: clientIdParam } = useParams<{ id: string }>(); + const clientId = clientIdParam ?? ""; const [isCreateModalOpen, setCreateModalOpen] = useState(false); + const { data, isLoading, error } = useQuery({ + queryKey: ["idpConfigs", clientId], + queryFn: () => listIdpConfigsForClient(clientId), + enabled: clientId.length > 0, + }); + if (!clientId) { return (
@@ -189,11 +196,6 @@ export function ClientFederationPage() { ); } - const { data, isLoading, error } = useQuery({ - queryKey: ["idpConfigs", clientId], - queryFn: () => listIdpConfigsForClient(clientId), - }); - return (
-
+
{[ ["day", t("ui.common.chart.period.day", "일")], ["week", t("ui.common.chart.period.week", "주")], @@ -743,7 +746,7 @@ function GlobalOverviewPage() { {label} ))} -
+
`${commonRoot}/${directory}/**/*.{ts,tsx}`, +); + export default defineConfig({ plugins: [react()], server: { @@ -17,5 +25,27 @@ export default defineConfig({ globals: true, environment: "jsdom", include: ["src/**/*.{test,spec}.{ts,tsx}"], + coverage: { + provider: "v8", + reporter: ["text", "html", "lcov", "json-summary"], + reportsDirectory: "coverage", + all: true, + allowExternal: true, + include: ["src/**/*.{ts,tsx}", ...commonCoverageIncludes], + exclude: [ + "**/*.{test,spec}.{ts,tsx}", + "**/*.d.ts", + "**/node_modules/**", + "**/dist/**", + "**/coverage/**", + "src/main.tsx", + "src/vite-env.d.ts", + "../common/**/node_modules/**", + "../common/.pnpm-store/**", + `${commonRoot}/theme/**`, + `${commonRoot}/core/pagination/*.worker.ts`, + `${commonRoot}/core/query/queryClient.ts`, + ], + }, }, }); diff --git a/docs/badges/adminfront-coverage.svg b/docs/badges/adminfront-coverage.svg new file mode 100644 index 00000000..aec87a00 --- /dev/null +++ b/docs/badges/adminfront-coverage.svg @@ -0,0 +1,19 @@ + + adminfront coverage: 38.89% + + + + + + + + + + + + adminfront coverage + adminfront coverage + 38.89% + 38.89% + + diff --git a/docs/badges/badges.json b/docs/badges/badges.json new file mode 100644 index 00000000..1a5f6921 --- /dev/null +++ b/docs/badges/badges.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "generatedBy": "scripts/update_code_check_badges.mjs", + "updatedAt": "2026-05-29T03:04:40.216Z", + "badges": { + "code-check": { + "label": "code check", + "message": "unknown", + "color": "#6e7781" + }, + "biome": { + "label": "biome", + "message": "unknown", + "color": "#6e7781" + }, + "userfront-e2e-fast": { + "label": "userfront e2e fast", + "message": "unknown", + "color": "#6e7781" + }, + "userfront-e2e-full": { + "label": "userfront e2e full", + "message": "unknown", + "color": "#6e7781" + }, + "adminfront-coverage": { + "label": "adminfront coverage", + "message": "38.89%", + "color": "#bf8700" + }, + "devfront-coverage": { + "label": "devfront coverage", + "message": "8.87%", + "color": "#cf222e" + }, + "orgfront-coverage": { + "label": "orgfront coverage", + "message": "37.50%", + "color": "#bf8700" + } + } +} diff --git a/docs/badges/biome.svg b/docs/badges/biome.svg new file mode 100644 index 00000000..dec01590 --- /dev/null +++ b/docs/badges/biome.svg @@ -0,0 +1,19 @@ + + biome: unknown + + + + + + + + + + + + biome + biome + unknown + unknown + + diff --git a/docs/badges/code-check.svg b/docs/badges/code-check.svg new file mode 100644 index 00000000..ff23ded3 --- /dev/null +++ b/docs/badges/code-check.svg @@ -0,0 +1,19 @@ + + code check: unknown + + + + + + + + + + + + code check + code check + unknown + unknown + + diff --git a/docs/badges/devfront-coverage.svg b/docs/badges/devfront-coverage.svg new file mode 100644 index 00000000..5f841ca1 --- /dev/null +++ b/docs/badges/devfront-coverage.svg @@ -0,0 +1,19 @@ + + devfront coverage: 8.87% + + + + + + + + + + + + devfront coverage + devfront coverage + 8.87% + 8.87% + + diff --git a/docs/badges/orgfront-coverage.svg b/docs/badges/orgfront-coverage.svg new file mode 100644 index 00000000..82b8bad1 --- /dev/null +++ b/docs/badges/orgfront-coverage.svg @@ -0,0 +1,19 @@ + + orgfront coverage: 37.50% + + + + + + + + + + + + orgfront coverage + orgfront coverage + 37.50% + 37.50% + + diff --git a/docs/badges/userfront-e2e-fast.svg b/docs/badges/userfront-e2e-fast.svg new file mode 100644 index 00000000..2f745843 --- /dev/null +++ b/docs/badges/userfront-e2e-fast.svg @@ -0,0 +1,19 @@ + + userfront e2e fast: unknown + + + + + + + + + + + + userfront e2e fast + userfront e2e fast + unknown + unknown + + diff --git a/docs/badges/userfront-e2e-full.svg b/docs/badges/userfront-e2e-full.svg new file mode 100644 index 00000000..1c8369c8 --- /dev/null +++ b/docs/badges/userfront-e2e-full.svg @@ -0,0 +1,19 @@ + + userfront e2e full: unknown + + + + + + + + + + + + userfront e2e full + userfront e2e full + unknown + unknown + + diff --git a/mcp/hydra-mcp/biome.json b/mcp/hydra-mcp/biome.json new file mode 100644 index 00000000..5e239258 --- /dev/null +++ b/mcp/hydra-mcp/biome.json @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": ["../../common/config/biome.base.json"] +} diff --git a/mcp/hydra-mcp/package-lock.json b/mcp/hydra-mcp/package-lock.json new file mode 100644 index 00000000..d539c99a --- /dev/null +++ b/mcp/hydra-mcp/package-lock.json @@ -0,0 +1,1337 @@ +{ + "name": "mcp-ory-hydra", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-ory-hydra", + "version": "0.1.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.0", + "zod": "^3.25.0" + }, + "bin": { + "mcp-ory-hydra": "src/runner.js" + }, + "devDependencies": { + "@biomejs/biome": "2.4.16" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz", + "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.16", + "@biomejs/cli-darwin-x64": "2.4.16", + "@biomejs/cli-linux-arm64": "2.4.16", + "@biomejs/cli-linux-arm64-musl": "2.4.16", + "@biomejs/cli-linux-x64": "2.4.16", + "@biomejs/cli-linux-x64-musl": "2.4.16", + "@biomejs/cli-win32-arm64": "2.4.16", + "@biomejs/cli-win32-x64": "2.4.16" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz", + "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz", + "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz", + "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz", + "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz", + "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz", + "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz", + "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz", + "integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "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.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "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/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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-apply-helpers": { + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "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/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/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.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.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": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "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": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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-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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "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/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/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "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/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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "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" + }, + "funding": { + "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", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "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/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "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": { + "es-errors": "^1.3.0", + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "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.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/mcp/hydra-mcp/package.json b/mcp/hydra-mcp/package.json index 9c7275c6..9564a9e0 100644 --- a/mcp/hydra-mcp/package.json +++ b/mcp/hydra-mcp/package.json @@ -7,7 +7,12 @@ "mcp-ory-hydra": "./src/runner.js" }, "scripts": { - "start": "node ./src/index.js" + "start": "node ./src/index.js", + "lint": "biome check .", + "lint:fix": "biome check . --write" + }, + "devDependencies": { + "@biomejs/biome": "2.4.16" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.25.0", diff --git a/mcp/hydra-mcp/src/index.js b/mcp/hydra-mcp/src/index.js index 5484d679..70ee8667 100755 --- a/mcp/hydra-mcp/src/index.js +++ b/mcp/hydra-mcp/src/index.js @@ -8,8 +8,12 @@ const requireFromModules = createRequire( modulesBase ? path.join(modulesBase, "package.json") : import.meta.url, ); -const mcpModule = await import(resolveModule("@modelcontextprotocol/sdk/server/mcp.js")); -const stdioModule = await import(resolveModule("@modelcontextprotocol/sdk/server/stdio.js")); +const mcpModule = await import( + resolveModule("@modelcontextprotocol/sdk/server/mcp.js") +); +const stdioModule = await import( + resolveModule("@modelcontextprotocol/sdk/server/stdio.js") +); const zodModule = await import(resolveModule("zod")); const { McpServer } = mcpModule; @@ -20,7 +24,10 @@ const hydraPublicUrl = process.env.HYDRA_PUBLIC_URL ?? "http://127.0.0.1:4444"; const hydraAdminUrl = process.env.HYDRA_ADMIN_URL ?? "http://127.0.0.1:4445"; const adminApiToken = process.env.HYDRA_ADMIN_API_TOKEN; const publicApiToken = process.env.HYDRA_PUBLIC_API_TOKEN; -const timeoutMs = Number.parseInt(process.env.HYDRA_HTTP_TIMEOUT_MS ?? "15000", 10); +const timeoutMs = Number.parseInt( + process.env.HYDRA_HTTP_TIMEOUT_MS ?? "15000", + 10, +); class HttpError extends Error { constructor(message, status, body, url) { @@ -81,7 +88,12 @@ async function requestJson(url, { method = "GET", headers, body } = {}, token) { : null; if (!response.ok) { - throw new HttpError(`HTTP ${response.status} ${response.statusText}`, response.status, data, url); + throw new HttpError( + `HTTP ${response.status} ${response.statusText}`, + response.status, + data, + url, + ); } return { @@ -175,7 +187,11 @@ async function main() { const url = buildUrl(base, `/health/${probe}`); try { - const result = await requestJson(url, {}, service === "public" ? publicApiToken : adminApiToken); + const result = await requestJson( + url, + {}, + service === "public" ? publicApiToken : adminApiToken, + ); return formatToolResult({ service, probe, @@ -217,7 +233,10 @@ async function main() { "Get an OAuth2 client by client_id from Hydra Admin API.", ClientIdInputSchema.shape, async (input) => { - const url = buildUrl(hydraAdminUrl, `/clients/${encodeURIComponent(input.client_id)}`); + const url = buildUrl( + hydraAdminUrl, + `/clients/${encodeURIComponent(input.client_id)}`, + ); try { const result = await requestJson(url, {}, adminApiToken); @@ -265,7 +284,10 @@ async function main() { "Update an OAuth2 client via Hydra Admin API.", ClientPayloadInputSchema.shape, async (input) => { - const url = buildUrl(hydraAdminUrl, `/clients/${encodeURIComponent(input.client_id)}`); + const url = buildUrl( + hydraAdminUrl, + `/clients/${encodeURIComponent(input.client_id)}`, + ); try { const result = await requestJson( @@ -294,7 +316,10 @@ async function main() { "Delete an OAuth2 client via Hydra Admin API.", ClientIdInputSchema.shape, async (input) => { - const url = buildUrl(hydraAdminUrl, `/clients/${encodeURIComponent(input.client_id)}`); + const url = buildUrl( + hydraAdminUrl, + `/clients/${encodeURIComponent(input.client_id)}`, + ); try { const result = await requestJson( diff --git a/mcp/hydra-mcp/src/runner.js b/mcp/hydra-mcp/src/runner.js index 5e68503c..3fc4e45f 100755 --- a/mcp/hydra-mcp/src/runner.js +++ b/mcp/hydra-mcp/src/runner.js @@ -8,7 +8,13 @@ import { fileURLToPath } from "node:url"; const sdkVersion = "^1.25.0"; const zodVersion = "^3.25.0"; const cacheRoot = resolveCacheRoot(); -const sdkMarker = path.join(cacheRoot, "node_modules", "@modelcontextprotocol", "sdk", "package.json"); +const sdkMarker = path.join( + cacheRoot, + "node_modules", + "@modelcontextprotocol", + "sdk", + "package.json", +); if (!existsSync(sdkMarker)) { mkdirSync(cacheRoot, { recursive: true }); @@ -65,6 +71,7 @@ function resolveCacheRoot() { return process.env.MCP_ORY_HYDRA_CACHE_DIR; } - const baseCache = process.env.XDG_CACHE_HOME ?? path.join(homedir(), ".cache"); + const baseCache = + process.env.XDG_CACHE_HOME ?? path.join(homedir(), ".cache"); return path.join(baseCache, "mcp-ory-hydra"); } diff --git a/mcp/keto-mcp/biome.json b/mcp/keto-mcp/biome.json new file mode 100644 index 00000000..5e239258 --- /dev/null +++ b/mcp/keto-mcp/biome.json @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": ["../../common/config/biome.base.json"] +} diff --git a/mcp/keto-mcp/package-lock.json b/mcp/keto-mcp/package-lock.json new file mode 100644 index 00000000..113b31cc --- /dev/null +++ b/mcp/keto-mcp/package-lock.json @@ -0,0 +1,1337 @@ +{ + "name": "mcp-ory-keto", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-ory-keto", + "version": "0.1.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.0", + "zod": "^3.25.0" + }, + "bin": { + "mcp-ory-keto": "src/runner.js" + }, + "devDependencies": { + "@biomejs/biome": "2.4.16" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz", + "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.16", + "@biomejs/cli-darwin-x64": "2.4.16", + "@biomejs/cli-linux-arm64": "2.4.16", + "@biomejs/cli-linux-arm64-musl": "2.4.16", + "@biomejs/cli-linux-x64": "2.4.16", + "@biomejs/cli-linux-x64-musl": "2.4.16", + "@biomejs/cli-win32-arm64": "2.4.16", + "@biomejs/cli-win32-x64": "2.4.16" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz", + "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz", + "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz", + "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz", + "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz", + "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz", + "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz", + "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz", + "integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "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.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "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/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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-apply-helpers": { + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "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/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/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.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.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": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "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": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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-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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "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/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/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "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/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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "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" + }, + "funding": { + "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", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "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/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "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": { + "es-errors": "^1.3.0", + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "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.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/mcp/keto-mcp/package.json b/mcp/keto-mcp/package.json index b169601a..2d8dbcfa 100644 --- a/mcp/keto-mcp/package.json +++ b/mcp/keto-mcp/package.json @@ -7,7 +7,12 @@ "mcp-ory-keto": "./src/runner.js" }, "scripts": { - "start": "node ./src/index.js" + "start": "node ./src/index.js", + "lint": "biome check .", + "lint:fix": "biome check . --write" + }, + "devDependencies": { + "@biomejs/biome": "2.4.16" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.25.0", diff --git a/mcp/keto-mcp/src/index.js b/mcp/keto-mcp/src/index.js index c5d07573..cdd3890d 100644 --- a/mcp/keto-mcp/src/index.js +++ b/mcp/keto-mcp/src/index.js @@ -8,8 +8,12 @@ const requireFromModules = createRequire( modulesBase ? path.join(modulesBase, "package.json") : import.meta.url, ); -const mcpModule = await import(resolveModule("@modelcontextprotocol/sdk/server/mcp.js")); -const stdioModule = await import(resolveModule("@modelcontextprotocol/sdk/server/stdio.js")); +const mcpModule = await import( + resolveModule("@modelcontextprotocol/sdk/server/mcp.js") +); +const stdioModule = await import( + resolveModule("@modelcontextprotocol/sdk/server/stdio.js") +); const zodModule = await import(resolveModule("zod")); const { McpServer } = mcpModule; @@ -20,7 +24,10 @@ const ketoReadUrl = process.env.KETO_READ_URL ?? "http://127.0.0.1:4466"; const ketoWriteUrl = process.env.KETO_WRITE_URL ?? "http://127.0.0.1:4467"; const readApiToken = process.env.KETO_READ_API_TOKEN; const writeApiToken = process.env.KETO_WRITE_API_TOKEN; -const timeoutMs = Number.parseInt(process.env.KETO_HTTP_TIMEOUT_MS ?? "15000", 10); +const timeoutMs = Number.parseInt( + process.env.KETO_HTTP_TIMEOUT_MS ?? "15000", + 10, +); class HttpError extends Error { constructor(message, status, body, url) { @@ -95,7 +102,12 @@ async function requestJson(url, { method = "GET", headers, body } = {}, token) { : null; if (!response.ok) { - throw new HttpError(`HTTP ${response.status} ${response.statusText}`, response.status, data, url); + throw new HttpError( + `HTTP ${response.status} ${response.statusText}`, + response.status, + data, + url, + ); } return { @@ -190,7 +202,11 @@ async function main() { const url = buildUrl(base, `/health/${probe}`); try { - const result = await requestJson(url, {}, service === "write" ? writeApiToken : readApiToken); + const result = await requestJson( + url, + {}, + service === "write" ? writeApiToken : readApiToken, + ); return formatToolResult({ service, probe, diff --git a/mcp/keto-mcp/src/runner.js b/mcp/keto-mcp/src/runner.js index 0e75a496..d4c8f21a 100755 --- a/mcp/keto-mcp/src/runner.js +++ b/mcp/keto-mcp/src/runner.js @@ -8,7 +8,13 @@ import { fileURLToPath } from "node:url"; const sdkVersion = "^1.25.0"; const zodVersion = "^3.25.0"; const cacheRoot = resolveCacheRoot(); -const sdkMarker = path.join(cacheRoot, "node_modules", "@modelcontextprotocol", "sdk", "package.json"); +const sdkMarker = path.join( + cacheRoot, + "node_modules", + "@modelcontextprotocol", + "sdk", + "package.json", +); if (!existsSync(sdkMarker)) { mkdirSync(cacheRoot, { recursive: true }); @@ -65,6 +71,7 @@ function resolveCacheRoot() { return process.env.MCP_ORY_KETO_CACHE_DIR; } - const baseCache = process.env.XDG_CACHE_HOME ?? path.join(homedir(), ".cache"); + const baseCache = + process.env.XDG_CACHE_HOME ?? path.join(homedir(), ".cache"); return path.join(baseCache, "mcp-ory-keto"); } diff --git a/orgfront/biome.json b/orgfront/biome.json index fb68b4da..66e0edd1 100644 --- a/orgfront/biome.json +++ b/orgfront/biome.json @@ -1,6 +1,4 @@ { - "extends": ["../common/config/biome.base.json"], - "files": { - "ignore": [".vite"] - } + "root": true, + "extends": ["../common/config/biome.base.json"] } diff --git a/orgfront/package-lock.json b/orgfront/package-lock.json index fdfb6ea2..10543bc5 100644 --- a/orgfront/package-lock.json +++ b/orgfront/package-lock.json @@ -32,18 +32,20 @@ "zod": "^4.4.3" }, "devDependencies": { + "@biomejs/biome": "2.4.16", "@playwright/test": "^1.60.0", "@types/node": "^25.7.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "4.1.6", "autoprefixer": "^10.5.0", "jsdom": "^28.1.0", "postcss": "^8.5.14", "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "typescript": "^6.0.3", - "vite": "^8.0.12", + "vite": "^8.0.14", "vitest": "^4.1.6" }, "engines": { @@ -116,6 +118,241 @@ "dev": true, "license": "MIT" }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz", + "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.16", + "@biomejs/cli-darwin-x64": "2.4.16", + "@biomejs/cli-linux-arm64": "2.4.16", + "@biomejs/cli-linux-arm64-musl": "2.4.16", + "@biomejs/cli-linux-x64": "2.4.16", + "@biomejs/cli-linux-x64-musl": "2.4.16", + "@biomejs/cli-win32-arm64": "2.4.16", + "@biomejs/cli-win32-x64": "2.4.16" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz", + "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz", + "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz", + "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz", + "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz", + "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz", + "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz", + "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz", + "integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -442,7 +679,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.129.0", + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", "dev": true, "license": "MIT", "funding": { @@ -1888,9 +2127,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", - "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", "cpu": [ "arm64" ], @@ -1905,9 +2144,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", - "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", "cpu": [ "arm64" ], @@ -1922,9 +2161,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", - "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", "cpu": [ "x64" ], @@ -1939,9 +2178,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", - "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", "cpu": [ "x64" ], @@ -1956,9 +2195,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", - "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", "cpu": [ "arm" ], @@ -1973,9 +2212,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", - "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", "cpu": [ "arm64" ], @@ -1993,9 +2232,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", - "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", "cpu": [ "arm64" ], @@ -2013,9 +2252,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", - "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", "cpu": [ "ppc64" ], @@ -2033,9 +2272,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", - "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", "cpu": [ "s390x" ], @@ -2053,11 +2292,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2068,9 +2312,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", - "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", "cpu": [ "x64" ], @@ -2088,9 +2332,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", - "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", "cpu": [ "arm64" ], @@ -2105,9 +2349,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", - "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", "cpu": [ "wasm32" ], @@ -2124,9 +2368,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", - "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", "cpu": [ "arm64" ], @@ -2141,9 +2385,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", - "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", "cpu": [ "x64" ], @@ -2345,6 +2589,37 @@ } } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.6.tgz", + "integrity": "sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.6", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.6", + "vitest": "4.1.6" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", @@ -2540,6 +2815,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.2.tgz", + "integrity": "sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "license": "MIT" @@ -3316,6 +3603,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "license": "MIT", @@ -3362,6 +3659,13 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -3449,6 +3753,45 @@ "dev": true, "license": "MIT" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jiti": { "version": "1.21.7", "dev": true, @@ -3457,6 +3800,13 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/jsdom": { "version": "28.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", @@ -3811,6 +4161,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "license": "MIT", @@ -3877,7 +4255,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -4035,7 +4415,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -4053,7 +4435,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4434,12 +4816,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.129.0", - "@rolldown/pluginutils": "1.0.0" + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -4448,25 +4832,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0", - "@rolldown/binding-darwin-arm64": "1.0.0", - "@rolldown/binding-darwin-x64": "1.0.0", - "@rolldown/binding-freebsd-x64": "1.0.0", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", - "@rolldown/binding-linux-arm64-gnu": "1.0.0", - "@rolldown/binding-linux-arm64-musl": "1.0.0", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0", - "@rolldown/binding-linux-s390x-gnu": "1.0.0", - "@rolldown/binding-linux-x64-gnu": "1.0.0", - "@rolldown/binding-linux-x64-musl": "1.0.0", - "@rolldown/binding-openharmony-arm64": "1.0.0", - "@rolldown/binding-wasm32-wasi": "1.0.0", - "@rolldown/binding-win32-arm64-msvc": "1.0.0", - "@rolldown/binding-win32-x64-msvc": "1.0.0" + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, @@ -4509,6 +4895,19 @@ "version": "0.27.0", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -4559,6 +4958,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "dev": true, @@ -4901,14 +5313,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.12", + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "rolldown": "1.0.0", + "postcss": "^8.5.15", + "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "bin": { diff --git a/orgfront/package.json b/orgfront/package.json index 22de49b6..bafa879f 100644 --- a/orgfront/package.json +++ b/orgfront/package.json @@ -15,6 +15,7 @@ "lint": "biome check .", "preview": "vite preview", "test": "playwright test", + "test:coverage": "vitest run --coverage", "test:unit": "vitest run", "test:roles": "playwright test tests/devfront-role-switch-report.spec.ts", "test:ui": "playwright test --ui" @@ -44,10 +45,12 @@ "zod": "^4.4.3" }, "devDependencies": { + "@biomejs/biome": "2.4.16", "@playwright/test": "^1.60.0", "@types/node": "^25.7.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@vitest/coverage-v8": "4.1.6", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.5.0", "jsdom": "^28.1.0", @@ -55,7 +58,7 @@ "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "typescript": "^6.0.3", - "vite": "^8.0.12", + "vite": "^8.0.14", "vitest": "^4.1.6" } } diff --git a/orgfront/src/app/routes.tsx b/orgfront/src/app/routes.tsx index dd5bdb90..8463cd0d 100644 --- a/orgfront/src/app/routes.tsx +++ b/orgfront/src/app/routes.tsx @@ -1,7 +1,7 @@ import { + createBrowserRouter, Navigate, type RouteObject, - createBrowserRouter, } from "react-router-dom"; import AuthCallbackPage from "../features/auth/AuthCallbackPage"; import AuthGuard from "../features/auth/AuthGuard"; diff --git a/orgfront/src/components/layout/AppLayout.tsx b/orgfront/src/components/layout/AppLayout.tsx index 83f98b13..619e776c 100644 --- a/orgfront/src/components/layout/AppLayout.tsx +++ b/orgfront/src/components/layout/AppLayout.tsx @@ -13,12 +13,12 @@ import { useEffect, useRef, useState } from "react"; import { useAuth } from "react-oidc-context"; import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { - type ShellTranslator, applyShellTheme, buildShellProfileSummary, buildShellSessionStatus, readShellSessionExpiryEnabled, readShellTheme, + type ShellTranslator, shellLayoutClasses, writeShellSessionExpiryEnabled, } from "../../../../common/shell"; @@ -136,9 +136,12 @@ function AppLayout() { window.addEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell); return () => { - window.removeEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell); + window.removeEventListener( + LOCALE_CHANGED_EVENT, + rerenderDevelopmentShell, + ); }; - }, [isDevelopmentRuntime]); + }, []); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -251,7 +254,6 @@ function AppLayout() { auth.isAuthenticated, auth.isLoading, auth.user?.expires_at, - isDevelopmentRuntime, isSessionExpiryEnabled, ]); diff --git a/orgfront/src/components/ui/avatar.tsx b/orgfront/src/components/ui/avatar.tsx index 23e88913..ab415563 100644 --- a/orgfront/src/components/ui/avatar.tsx +++ b/orgfront/src/components/ui/avatar.tsx @@ -44,4 +44,4 @@ const AvatarFallback = React.forwardRef< )); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; -export { Avatar, AvatarImage, AvatarFallback }; +export { Avatar, AvatarFallback, AvatarImage }; diff --git a/orgfront/src/components/ui/card.tsx b/orgfront/src/components/ui/card.tsx index 33685c34..246b5289 100644 --- a/orgfront/src/components/ui/card.tsx +++ b/orgfront/src/components/ui/card.tsx @@ -50,9 +50,9 @@ function CardFooter({ export { Card, + CardContent, + CardDescription, + CardFooter, CardHeader, CardTitle, - CardDescription, - CardContent, - CardFooter, }; diff --git a/orgfront/src/components/ui/table.tsx b/orgfront/src/components/ui/table.tsx index 0b0022a5..33663b2e 100644 --- a/orgfront/src/components/ui/table.tsx +++ b/orgfront/src/components/ui/table.tsx @@ -92,11 +92,11 @@ TableCaption.displayName = "TableCaption"; export { Table, - TableHeader, TableBody, + TableCaption, + TableCell, TableFooter, TableHead, + TableHeader, TableRow, - TableCell, - TableCaption, }; diff --git a/orgfront/src/features/auth/AuthGuard.test.tsx b/orgfront/src/features/auth/AuthGuard.test.tsx index 29d1d4ac..478e9edf 100644 --- a/orgfront/src/features/auth/AuthGuard.test.tsx +++ b/orgfront/src/features/auth/AuthGuard.test.tsx @@ -1,5 +1,5 @@ import { act } from "react"; -import { type Root, createRoot } from "react-dom/client"; +import { createRoot, type Root } from "react-dom/client"; import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; import AuthGuard from "./AuthGuard"; diff --git a/orgfront/src/features/auth/LoginPage.test.tsx b/orgfront/src/features/auth/LoginPage.test.tsx index 12500838..d4e3cea7 100644 --- a/orgfront/src/features/auth/LoginPage.test.tsx +++ b/orgfront/src/features/auth/LoginPage.test.tsx @@ -1,5 +1,5 @@ import { act } from "react"; -import { type Root, createRoot } from "react-dom/client"; +import { createRoot, type Root } from "react-dom/client"; import { MemoryRouter } from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; import LoginPage from "./LoginPage"; diff --git a/orgfront/src/features/auth/LoginPage.tsx b/orgfront/src/features/auth/LoginPage.tsx index 54fc70e2..98eba1ba 100644 --- a/orgfront/src/features/auth/LoginPage.tsx +++ b/orgfront/src/features/auth/LoginPage.tsx @@ -1,8 +1,7 @@ import { ExternalLink, LogIn, ShieldHalf } from "lucide-react"; import { useEffect, useRef } from "react"; import { useAuth } from "react-oidc-context"; -import { useNavigate } from "react-router-dom"; -import { useSearchParams } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "../../components/ui/button"; import { Card, diff --git a/orgfront/src/features/clients/ClientGeneralPage.tsx b/orgfront/src/features/clients/ClientGeneralPage.tsx index 7d4faa51..648f237e 100644 --- a/orgfront/src/features/clients/ClientGeneralPage.tsx +++ b/orgfront/src/features/clients/ClientGeneralPage.tsx @@ -27,6 +27,11 @@ import { Label } from "../../components/ui/label"; import { Switch } from "../../components/ui/switch"; import { Textarea } from "../../components/ui/textarea"; import { toast } from "../../components/ui/use-toast"; +import type { + ClientStatus, + ClientType, + ClientUpsertRequest, +} from "../../lib/devApi"; import { createClient, deleteClient, @@ -36,11 +41,6 @@ import { updateClient, updateClientStatus, } from "../../lib/devApi"; -import type { - ClientStatus, - ClientType, - ClientUpsertRequest, -} from "../../lib/devApi"; import { t } from "../../lib/i18n"; import { cn } from "../../lib/utils"; @@ -1118,7 +1118,7 @@ function ClientGeneralPage() { {securityProfile === "pkce" && ( -
e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} @@ -1145,7 +1145,7 @@ function ClientGeneralPage() { checked={headlessLoginEnabled} onCheckedChange={handleHeadlessToggle} /> -
+ )}
@@ -1454,104 +1454,102 @@ function ClientGeneralPage() {
{currentHeadlessJwksCache.parsedKeys?.length ? (
- {currentHeadlessJwksCache.parsedKeys.map( - (key, index) => { - const normalizedAlgorithm = key.alg?.trim() ?? ""; - const isMissingAlgorithm = - normalizedAlgorithm === ""; - const isUnsupportedAlgorithm = - !isMissingAlgorithm && - !HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET.has( - normalizedAlgorithm, - ); + {currentHeadlessJwksCache.parsedKeys.map((key) => { + const normalizedAlgorithm = key.alg?.trim() ?? ""; + const isMissingAlgorithm = + normalizedAlgorithm === ""; + const isUnsupportedAlgorithm = + !isMissingAlgorithm && + !HEADLESS_LOGIN_ALLOWED_ALGORITHM_SET.has( + normalizedAlgorithm, + ); - return ( -
-
-
-

- KID -

-

- {key.kid || "-"} -

-
-
-

- KTY -

-

- {key.kty || "-"} -

-
-
-

- USE -

-

- {key.use || "-"} -

-
-
-

- ALG -

-

- {key.alg || - t( - "msg.dev.clients.general.public_key.cache.missing_algorithm_badge", - "알고리즘 미선언", - )} -

- {isMissingAlgorithm && ( -

- {t( - "msg.dev.clients.general.public_key.cache.missing_algorithm_reason", - "이 키는 `alg`가 비어 있어서 저장할 수 없습니다.", - )} -

- )} - {isUnsupportedAlgorithm && ( -

- {t( - "msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", - "이 알고리즘은 Headless Login에서 지원되지 않습니다.", - )} -

- )} -
-
-
+ return ( +
+
+

- {t( - "ui.dev.clients.general.public_key.cache.parsed_key_n", - "N", + KID +

+

+ {key.kid || "-"} +

+
+
+

+ KTY +

+

+ {key.kty || "-"} +

+
+
+

+ USE +

+

+ {key.use || "-"} +

+
+
+

+ ALG +

+

+ {key.alg || + t( + "msg.dev.clients.general.public_key.cache.missing_algorithm_badge", + "알고리즘 미선언", + )}

-

- {key.n || "-"} -

+ {isMissingAlgorithm && ( +

+ {t( + "msg.dev.clients.general.public_key.cache.missing_algorithm_reason", + "이 키는 `alg`가 비어 있어서 저장할 수 없습니다.", + )} +

+ )} + {isUnsupportedAlgorithm && ( +

+ {t( + "msg.dev.clients.general.public_key.cache.unsupported_algorithm_reason", + "이 알고리즘은 Headless Login에서 지원되지 않습니다.", + )} +

+ )}
- ); - }, - )} +
+

+ {t( + "ui.dev.clients.general.public_key.cache.parsed_key_n", + "N", + )} +

+

+ {key.n || "-"} +

+
+
+ ); + })}
) : (
diff --git a/orgfront/src/features/clients/routes/ClientFederationPage.tsx b/orgfront/src/features/clients/routes/ClientFederationPage.tsx index 6700e7fe..9cfa3086 100644 --- a/orgfront/src/features/clients/routes/ClientFederationPage.tsx +++ b/orgfront/src/features/clients/routes/ClientFederationPage.tsx @@ -19,11 +19,11 @@ import { TableHeader, TableRow, } from "../../../components/ui/table"; +import type { IdpConfig, IdpConfigCreateRequest } from "../../../lib/devApi"; import { createIdpConfigForClient, listIdpConfigsForClient, } from "../../../lib/devApi"; -import type { IdpConfig, IdpConfigCreateRequest } from "../../../lib/devApi"; import { t } from "../../../lib/i18n"; // Proper Modal Component with Form @@ -177,9 +177,16 @@ const CreateIdpModal = ({ }; export function ClientFederationPage() { - const { id: clientId } = useParams<{ id: string }>(); + const { id: clientIdParam } = useParams<{ id: string }>(); + const clientId = clientIdParam ?? ""; const [isCreateModalOpen, setCreateModalOpen] = useState(false); + const { data, isLoading, error } = useQuery({ + queryKey: ["idpConfigs", clientId], + queryFn: () => listIdpConfigsForClient(clientId), + enabled: clientId.length > 0, + }); + if (!clientId) { return (
@@ -188,11 +195,6 @@ export function ClientFederationPage() { ); } - const { data, isLoading, error } = useQuery({ - queryKey: ["idpConfigs", clientId], - queryFn: () => listIdpConfigsForClient(clientId), - }); - return (
diff --git a/orgfront/src/features/orgchart/pickerTree.ts b/orgfront/src/features/orgchart/pickerTree.ts index 883e1224..5ddd23f0 100644 --- a/orgfront/src/features/orgchart/pickerTree.ts +++ b/orgfront/src/features/orgchart/pickerTree.ts @@ -1,5 +1,5 @@ import type { TenantSummary, UserSummary } from "../../lib/adminApi"; -import { type TenantNode, buildTenantFullTree } from "../../lib/tenantTree"; +import { buildTenantFullTree, type TenantNode } from "../../lib/tenantTree"; import { orderHanmacFamilyChildren } from "./hanmacFamilyOrder"; import type { OrgPickerTreeNode } from "./pickerTypes"; import { filterTenantsByVisibility } from "./tenantVisibility"; diff --git a/orgfront/src/features/orgchart/rankPriority.test.ts b/orgfront/src/features/orgchart/rankPriority.test.ts index 8247fedd..bf9ff999 100644 --- a/orgfront/src/features/orgchart/rankPriority.test.ts +++ b/orgfront/src/features/orgchart/rankPriority.test.ts @@ -16,9 +16,7 @@ describe("org chart rank priority", () => { it("orders executive and research ranks with shared priority weights", () => { expect(getOrgRankWeight("사장")).toBeLessThan(getOrgRankWeight("부사장")); - expect(getOrgRankWeight("전무이사")).toBeLessThan( - getOrgRankWeight("상무"), - ); + expect(getOrgRankWeight("전무이사")).toBeLessThan(getOrgRankWeight("상무")); expect(getOrgRankWeight("수석연구원")).toBeLessThan( getOrgRankWeight("책임"), ); diff --git a/orgfront/src/features/orgchart/routes/OrgChartPage.test.tsx b/orgfront/src/features/orgchart/routes/OrgChartPage.test.tsx index f38a106e..e0623749 100644 --- a/orgfront/src/features/orgchart/routes/OrgChartPage.test.tsx +++ b/orgfront/src/features/orgchart/routes/OrgChartPage.test.tsx @@ -1,6 +1,5 @@ import { describe, expect, it } from "vitest"; import { - type OrgNode, buildOrgSelectionOptions, buildUsersMap, clampScale, @@ -9,6 +8,7 @@ import { getOrgNodeHeaderFill, getSemanticZoomMode, layoutForest, + type OrgNode, } from "./OrgChartPage"; function orgNode(id: string, children: OrgNode[] = [], level = 0): OrgNode { @@ -137,7 +137,9 @@ describe("org chart layout", () => { ], new Set(), ); - const shortNode = shortLayout.nodes.find((item) => item.node.id === "short"); + const shortNode = shortLayout.nodes.find( + (item) => item.node.id === "short", + ); const longNode = longLayout.nodes.find((item) => item.node.id === "long"); expect(shortNode?.width).toBeLessThan(320); @@ -472,16 +474,31 @@ describe("org chart layout", () => { "visible-parent", ); const internalOrg = { - ...tenantNode("internal-org", "ORGANIZATION", "내부 조직", "internal-org"), + ...tenantNode( + "internal-org", + "ORGANIZATION", + "내부 조직", + "internal-org", + ), parentId: "visible-parent", config: { visibility: "internal" }, }; const internalChild = { - ...tenantNode("internal-child", "ORGANIZATION", "내부 하위", "internal-child"), + ...tenantNode( + "internal-child", + "ORGANIZATION", + "내부 하위", + "internal-child", + ), parentId: "internal-org", }; const privateOrg = { - ...tenantNode("private-org", "ORGANIZATION", "비공개 조직", "private-org"), + ...tenantNode( + "private-org", + "ORGANIZATION", + "비공개 조직", + "private-org", + ), parentId: "visible-parent", config: { visibility: "private" }, }; diff --git a/orgfront/src/features/orgchart/routes/OrgChartPage.tsx b/orgfront/src/features/orgchart/routes/OrgChartPage.tsx index 9872a44f..0bb51578 100644 --- a/orgfront/src/features/orgchart/routes/OrgChartPage.tsx +++ b/orgfront/src/features/orgchart/routes/OrgChartPage.tsx @@ -3,19 +3,19 @@ import type { Node as ReactFlowNode } from "@xyflow/react"; import * as React from "react"; import { useLocation, useParams } from "react-router-dom"; import { - type TenantSummary, - type UserSummary, fetchAllTenants, fetchPublicOrgChart, fetchUsers, + type TenantSummary, + type UserSummary, } from "../../../lib/adminApi"; -import { type TenantNode, buildTenantFullTree } from "../../../lib/tenantTree"; +import { buildTenantFullTree, type TenantNode } from "../../../lib/tenantTree"; import { orderHanmacFamilyChildren, orderHanmacFamilyTenants, } from "../hanmacFamilyOrder"; -import { filterTenantsByVisibility, getOrgUnitType } from "../tenantVisibility"; import { getOrgRankWeight } from "../rankPriority"; +import { filterTenantsByVisibility, getOrgUnitType } from "../tenantVisibility"; import { getOrgChartUserDisplayName, getUserOrgProfile } from "../userDisplay"; export type OrgNode = { @@ -160,7 +160,7 @@ function getComplementaryColor(hexColor: string) { function getDisplayTextWidthUnit(value: string) { return Array.from(value).reduce((sum, char) => { if (char === " ") return sum + 0.4; - if (/^[\x00-\x7f]$/.test(char)) return sum + 0.55; + if (char.charCodeAt(0) <= 0x7f) return sum + 0.55; return sum + 1; }, 0); } @@ -206,7 +206,7 @@ export function getMemberGridMetrics( ? NODE_WIDTH : Math.max( NODE_WIDTH, - NODE_PADDING_Y * 2 + + NODE_PADDING_Y * 2 + columnCount * memberColumnWidth + (columnCount - 1) * MEMBER_COLUMN_GAP, ); diff --git a/orgfront/src/features/orgchart/routes/OrgPickerEmbedPreviewPage.tsx b/orgfront/src/features/orgchart/routes/OrgPickerEmbedPreviewPage.tsx index 55ca1182..a0ef2376 100644 --- a/orgfront/src/features/orgchart/routes/OrgPickerEmbedPreviewPage.tsx +++ b/orgfront/src/features/orgchart/routes/OrgPickerEmbedPreviewPage.tsx @@ -1,10 +1,10 @@ import * as React from "react"; import { useLocation } from "react-router-dom"; import { + buildOrgPickerEmbedSrc, type OrgPickerEmbedOptions, type OrgPickerMode, type OrgPickerSelectableType, - buildOrgPickerEmbedSrc, parseOrgPickerEmbedOptions, } from "../pickerTypes"; diff --git a/orgfront/src/features/orgchart/routes/OrgPickerPage.tsx b/orgfront/src/features/orgchart/routes/OrgPickerPage.tsx index 101bd86b..4199d5a3 100644 --- a/orgfront/src/features/orgchart/routes/OrgPickerPage.tsx +++ b/orgfront/src/features/orgchart/routes/OrgPickerPage.tsx @@ -6,14 +6,14 @@ import { Button } from "../../../components/ui/button"; import { fetchAllTenants, fetchUsers } from "../../../lib/adminApi"; import { buildOrgPickerTree, flattenDescendants } from "../pickerTree"; import { + buildOrgPickerEmbedSrc, + nodeKey, type OrgPickerEmbedOptions, type OrgPickerMode, type OrgPickerResult, type OrgPickerSelectableType, type OrgPickerSelection, type OrgPickerTreeNode, - buildOrgPickerEmbedSrc, - nodeKey, parseOrgPickerEmbedOptions, parseOrgPickerMode, parseOrgPickerSelectableType, diff --git a/orgfront/src/features/orgchart/userDisplay.test.ts b/orgfront/src/features/orgchart/userDisplay.test.ts index db3dd704..cd67cde8 100644 --- a/orgfront/src/features/orgchart/userDisplay.test.ts +++ b/orgfront/src/features/orgchart/userDisplay.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from "vitest"; import type { UserSummary } from "../../lib/adminApi"; -import { - getOrgChartUserDisplayName, - getUserOrgProfile, -} from "./userDisplay"; +import { getOrgChartUserDisplayName, getUserOrgProfile } from "./userDisplay"; function user(overrides: Partial): UserSummary { return { diff --git a/orgfront/src/lib/apiClient.ts b/orgfront/src/lib/apiClient.ts index e8f07ecd..d355adc4 100644 --- a/orgfront/src/lib/apiClient.ts +++ b/orgfront/src/lib/apiClient.ts @@ -1,8 +1,6 @@ import axios from "axios"; import { shouldStartLoginRedirect } from "../../../common/core/auth"; -import { - shouldSuppressDevelopmentSessionRedirect, -} from "../../../common/core/session"; +import { shouldSuppressDevelopmentSessionRedirect } from "../../../common/core/session"; import { userManager } from "./auth"; let isRedirectingToLogin = false; diff --git a/orgfront/src/lib/authConfig.test.ts b/orgfront/src/lib/authConfig.test.ts index 1fb8a08d..1cd7359c 100644 --- a/orgfront/src/lib/authConfig.test.ts +++ b/orgfront/src/lib/authConfig.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { - ORGFRONT_AUTH_CALLBACK_PATH, buildOrgFrontAuthRedirectUris, + ORGFRONT_AUTH_CALLBACK_PATH, resolveOrgFrontPublicOrigin, } from "./authConfig"; diff --git a/orgfront/src/sdk/org-context-chart/orgContextChart.test.ts b/orgfront/src/sdk/org-context-chart/orgContextChart.test.ts index 22d8b419..8fb6d18f 100644 --- a/orgfront/src/sdk/org-context-chart/orgContextChart.test.ts +++ b/orgfront/src/sdk/org-context-chart/orgContextChart.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from "vitest"; import { - type OrgContextResponse, buildOrgChartModel, createOrgContextClient, + type OrgContextResponse, renderOrgChart, renderOrgPicker, } from "./index"; diff --git a/orgfront/tests/orgchart-vector-render.spec.ts b/orgfront/tests/orgchart-vector-render.spec.ts index 6d774156..ffa319f5 100644 --- a/orgfront/tests/orgchart-vector-render.spec.ts +++ b/orgfront/tests/orgchart-vector-render.spec.ts @@ -164,9 +164,16 @@ test("org chart hides internal and private organizations in the status chart", a tenants: [ tenant("group", "HMAC Group", "hmac"), tenant("visible", "Visible Org", "visible", "group", "ORGANIZATION"), - tenant("internal", "Internal Org", "internal", "group", "ORGANIZATION", { - visibility: "internal", - }), + tenant( + "internal", + "Internal Org", + "internal", + "group", + "ORGANIZATION", + { + visibility: "internal", + }, + ), tenant( "internal-child", "Internal Child", @@ -192,9 +199,9 @@ test("org chart hides internal and private organizations in the status chart", a const svg = page.locator('[data-testid="orgchart-vector-svg"]'); await expect(svg.getByText("Visible Org")).toBeVisible(); await expect(svg.getByText("Visible User 사원")).toBeVisible(); - await expect(svg.getByText(/Internal Org|Internal Child|Private Org/)).toHaveCount( - 0, - ); + await expect( + svg.getByText(/Internal Org|Internal Child|Private Org/), + ).toHaveCount(0); await expect(svg.getByText(/Internal User|Private User/)).toHaveCount(0); }); diff --git a/orgfront/tests/orgfront-auto-login.spec.ts b/orgfront/tests/orgfront-auto-login.spec.ts index e15b4eb5..1257bd09 100644 --- a/orgfront/tests/orgfront-auto-login.spec.ts +++ b/orgfront/tests/orgfront-auto-login.spec.ts @@ -1,4 +1,4 @@ -import { type Page, expect, test } from "@playwright/test"; +import { expect, type Page, test } from "@playwright/test"; async function stubOidcAuthorization(page: Page) { let authorizationURL = ""; diff --git a/orgfront/vitest.config.ts b/orgfront/vitest.config.ts index f2474f57..b900b930 100644 --- a/orgfront/vitest.config.ts +++ b/orgfront/vitest.config.ts @@ -2,6 +2,14 @@ import { fileURLToPath } from "node:url"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vitest/config"; +const commonRoot = fileURLToPath(new URL("../common", import.meta.url)).replace( + /\\/g, + "/", +); +const commonCoverageIncludes = ["core", "shell", "theme", "ui"].map( + (directory) => `${commonRoot}/${directory}/**/*.{ts,tsx}`, +); + export default defineConfig({ plugins: [react()], server: { @@ -16,5 +24,27 @@ export default defineConfig({ globals: true, environment: "jsdom", include: ["src/**/*.{test,spec}.{ts,tsx}"], + coverage: { + provider: "v8", + reporter: ["text", "html", "lcov", "json-summary"], + reportsDirectory: "coverage", + all: true, + allowExternal: true, + include: ["src/**/*.{ts,tsx}", ...commonCoverageIncludes], + exclude: [ + "**/*.{test,spec}.{ts,tsx}", + "**/*.d.ts", + "**/node_modules/**", + "**/dist/**", + "**/coverage/**", + "src/main.tsx", + "src/vite-env.d.ts", + "../common/**/node_modules/**", + "../common/.pnpm-store/**", + `${commonRoot}/theme/**`, + `${commonRoot}/core/pagination/*.worker.ts`, + `${commonRoot}/core/query/queryClient.ts`, + ], + }, }, }); diff --git a/scripts/summarize_vitest_coverage.mjs b/scripts/summarize_vitest_coverage.mjs new file mode 100644 index 00000000..78596fbd --- /dev/null +++ b/scripts/summarize_vitest_coverage.mjs @@ -0,0 +1,81 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; + +const repoRoot = process.cwd(); +const packages = ["adminfront", "devfront", "orgfront"]; + +function formatPct(value) { + return typeof value === "number" ? `${value.toFixed(2)}%` : "n/a"; +} + +async function readCoverageSummary(packageName) { + const summaryPath = path.join( + repoRoot, + packageName, + "coverage", + "coverage-summary.json", + ); + const raw = await readFile(summaryPath, "utf8"); + const summary = JSON.parse(raw); + const total = summary.total; + + return { + package: packageName, + statements: total.statements.pct, + branches: total.branches.pct, + functions: total.functions.pct, + lines: total.lines.pct, + summaryPath: path.relative(repoRoot, summaryPath), + htmlPath: `${packageName}/coverage/index.html`, + lcovPath: `${packageName}/coverage/lcov.info`, + }; +} + +function renderMarkdown(rows) { + const lines = [ + "# Vitest Coverage Summary", + "", + "| Package | Statements | Branches | Functions | Lines | HTML Report | LCOV |", + "| --- | ---: | ---: | ---: | ---: | --- | --- |", + ]; + + for (const row of rows) { + lines.push( + [ + `| ${row.package}`, + formatPct(row.statements), + formatPct(row.branches), + formatPct(row.functions), + formatPct(row.lines), + row.htmlPath, + `${row.package}/coverage/lcov.info |`, + ].join(" | "), + ); + } + + lines.push( + "", + "Coverage includes each app's `src` tree and imported/covered files under `common`.", + "", + ); + + return lines.join("\n"); +} + +const rows = []; +for (const packageName of packages) { + rows.push(await readCoverageSummary(packageName)); +} + +const reportsDir = path.join(repoRoot, "reports"); +await mkdir(reportsDir, { recursive: true }); +await writeFile( + path.join(reportsDir, "vitest-coverage-summary.json"), + `${JSON.stringify({ packages: rows }, null, 2)}\n`, +); +await writeFile( + path.join(reportsDir, "vitest-coverage-summary.md"), + renderMarkdown(rows), +); + +console.log(renderMarkdown(rows)); diff --git a/scripts/update_code_check_badges.mjs b/scripts/update_code_check_badges.mjs new file mode 100644 index 00000000..cc4e069e --- /dev/null +++ b/scripts/update_code_check_badges.mjs @@ -0,0 +1,230 @@ +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; +import path from "node:path"; + +const repoRoot = process.cwd(); +const badgeDir = path.join(repoRoot, "docs", "badges"); +const manifestPath = path.join(badgeDir, "badges.json"); + +const resultStyles = { + success: { message: "passing", color: "#2ea043" }, + failure: { message: "failing", color: "#cf222e" }, + cancelled: { message: "cancelled", color: "#bf8700" }, + skipped: { message: "skipped", color: "#6e7781" }, + unknown: { message: "unknown", color: "#6e7781" }, +}; + +const badgeDefinitions = { + "code-check": { label: "code check", message: "unknown", color: "#6e7781" }, + biome: { label: "biome", message: "unknown", color: "#6e7781" }, + "userfront-e2e-fast": { + label: "userfront e2e fast", + message: "unknown", + color: "#6e7781", + }, + "userfront-e2e-full": { + label: "userfront e2e full", + message: "unknown", + color: "#6e7781", + }, + "adminfront-coverage": { + label: "adminfront coverage", + message: "38.89%", + color: "#bf8700", + }, + "devfront-coverage": { + label: "devfront coverage", + message: "8.87%", + color: "#cf222e", + }, + "orgfront-coverage": { + label: "orgfront coverage", + message: "37.50%", + color: "#bf8700", + }, +}; + +function normalizeResult(result) { + return resultStyles[result] ? result : "unknown"; +} + +function styleForResult(result) { + return resultStyles[normalizeResult(result)]; +} + +function colorForCoverage(percent) { + if (percent >= 80) return "#2ea043"; + if (percent >= 35) return "#bf8700"; + return "#cf222e"; +} + +function escapeXml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function textWidth(text) { + return Math.max(38, Math.ceil(String(text).length * 6.8 + 10)); +} + +function renderBadge({ label, message, color }) { + const labelWidth = textWidth(label); + const messageWidth = textWidth(message); + const width = labelWidth + messageWidth; + const labelCenter = labelWidth / 2; + const messageCenter = labelWidth + messageWidth / 2; + + return ` + ${escapeXml(label)}: ${escapeXml(message)} + + + + + + + + + + + + ${escapeXml(label)} + ${escapeXml(label)} + ${escapeXml(message)} + ${escapeXml(message)} + + +`; +} + +async function readJsonIfExists(filePath) { + try { + return JSON.parse(await readFile(filePath, "utf8")); + } catch { + return null; + } +} + +async function findCoverageSummary(directory) { + const entries = await readdir(directory, { withFileTypes: true }).catch( + () => [], + ); + + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); + if (entry.isFile() && entry.name === "vitest-coverage-summary.json") { + return entryPath; + } + if (entry.isDirectory()) { + const found = await findCoverageSummary(entryPath); + if (found) return found; + } + } + + return null; +} + +function updateResultBadge(manifest, key, result) { + const style = styleForResult(result); + manifest.badges[key] = { + ...(manifest.badges[key] ?? badgeDefinitions[key]), + message: style.message, + color: style.color, + }; +} + +function updateCoverageBadges(manifest, coverageSummary) { + for (const row of coverageSummary.packages ?? []) { + const key = `${row.package}-coverage`; + if (!badgeDefinitions[key]) continue; + const statements = Number(row.statements); + manifest.badges[key] = { + ...(manifest.badges[key] ?? badgeDefinitions[key]), + message: `${statements.toFixed(2)}%`, + color: colorForCoverage(statements), + }; + } +} + +const existingManifest = process.env.RESET_BADGES === "true" + ? null + : await readJsonIfExists(manifestPath); +const manifest = { + schemaVersion: 1, + generatedBy: "scripts/update_code_check_badges.mjs", + updatedAt: new Date().toISOString(), + badges: { + ...badgeDefinitions, + ...(existingManifest?.badges ?? {}), + }, +}; + +const jobResults = { + lint: process.env.LINT_RESULT, + biome: process.env.BIOME_RESULT, + backend: process.env.BACKEND_RESULT, + userfront: process.env.USERFRONT_RESULT, + userfrontE2e: process.env.USERFRONT_E2E_RESULT, + coverage: process.env.COVERAGE_RESULT, + adminfront: process.env.ADMINFRONT_RESULT, + devfront: process.env.DEVFRONT_RESULT, + orgfront: process.env.ORGFRONT_RESULT, +}; + +const overallResults = Object.values(jobResults).filter(Boolean); +const hasFailure = overallResults.some((result) => + ["failure", "cancelled"].includes(result), +); +const allSkipped = overallResults.length > 0 && + overallResults.every((result) => result === "skipped"); +updateResultBadge( + manifest, + "code-check", + overallResults.length === 0 + ? "unknown" + : hasFailure + ? "failure" + : allSkipped + ? "skipped" + : "success", +); + +updateResultBadge(manifest, "biome", jobResults.biome); + +const e2eWasFull = process.env.USERFRONT_E2E_FULL === "true"; +if (jobResults.userfrontE2e && jobResults.userfrontE2e !== "skipped") { + updateResultBadge( + manifest, + e2eWasFull ? "userfront-e2e-full" : "userfront-e2e-fast", + jobResults.userfrontE2e, + ); +} + +if (jobResults.coverage === "failure" || jobResults.coverage === "cancelled") { + for (const key of [ + "adminfront-coverage", + "devfront-coverage", + "orgfront-coverage", + ]) { + updateResultBadge(manifest, key, "failure"); + } +} else { + const coverageSummaryPath = process.env.COVERAGE_SUMMARY_PATH || + (await findCoverageSummary(path.join(repoRoot, "badge-artifacts"))); + const coverageSummary = coverageSummaryPath + ? await readJsonIfExists(coverageSummaryPath) + : null; + if (coverageSummary) { + updateCoverageBadges(manifest, coverageSummary); + } +} + +await mkdir(badgeDir, { recursive: true }); +await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + +for (const [key, badge] of Object.entries(manifest.badges)) { + await writeFile(path.join(badgeDir, `${key}.svg`), renderBadge(badge)); +} + +console.log(`Updated ${Object.keys(manifest.badges).length} badge files.`); diff --git a/userfront-e2e/biome.json b/userfront-e2e/biome.json new file mode 100644 index 00000000..66e0edd1 --- /dev/null +++ b/userfront-e2e/biome.json @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": ["../common/config/biome.base.json"] +} diff --git a/userfront-e2e/package-lock.json b/userfront-e2e/package-lock.json index 829a0589..e32aedee 100644 --- a/userfront-e2e/package-lock.json +++ b/userfront-e2e/package-lock.json @@ -8,9 +8,188 @@ "name": "userfront-e2e", "version": "0.1.0", "devDependencies": { + "@biomejs/biome": "2.4.16", "@playwright/test": "^1.58.2", "@types/node": "^24.3.0", "typescript": "^5.9.2" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz", + "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.16", + "@biomejs/cli-darwin-x64": "2.4.16", + "@biomejs/cli-linux-arm64": "2.4.16", + "@biomejs/cli-linux-arm64-musl": "2.4.16", + "@biomejs/cli-linux-x64": "2.4.16", + "@biomejs/cli-linux-x64-musl": "2.4.16", + "@biomejs/cli-win32-arm64": "2.4.16", + "@biomejs/cli-win32-x64": "2.4.16" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz", + "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz", + "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz", + "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz", + "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz", + "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz", + "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz", + "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz", + "integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" } }, "node_modules/@playwright/test": { diff --git a/userfront-e2e/package.json b/userfront-e2e/package.json index 92cc79a6..a3d2e9f3 100644 --- a/userfront-e2e/package.json +++ b/userfront-e2e/package.json @@ -11,9 +11,12 @@ "test:ui": "playwright test --ui", "serve:build": "node ./scripts/serve-userfront-build.mjs", "build:userfront:wasm": "cd ../userfront && flutter build web --wasm --release && cd .. && node userfront/scripts/optimize-web-build.mjs userfront/build/web", + "lint": "biome check .", + "lint:fix": "biome check . --write", "test:wasm": "npm run build:userfront:wasm && npm test" }, "devDependencies": { + "@biomejs/biome": "2.4.16", "@playwright/test": "^1.58.2", "@types/node": "^24.3.0", "typescript": "^5.9.2" diff --git a/userfront-e2e/playwright.config.ts b/userfront-e2e/playwright.config.ts index a6b19492..db2b22d7 100644 --- a/userfront-e2e/playwright.config.ts +++ b/userfront-e2e/playwright.config.ts @@ -1,6 +1,6 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; -const port = Number.parseInt(process.env.PORT ?? '4173', 10); +const port = Number.parseInt(process.env.PORT ?? "4173", 10); const defaultBaseUrl = `http://127.0.0.1:${port}`; const baseURL = process.env.BASE_URL ?? defaultBaseUrl; const reuseExistingServer = !process.env.CI; @@ -9,60 +9,60 @@ const configuredWorkers = process.env.PLAYWRIGHT_WORKERS : undefined; export default defineConfig({ - testDir: './tests', + testDir: "./tests", fullyParallel: false, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: configuredWorkers ?? 1, - reporter: process.env.CI ? [['html', { open: 'never' }], ['list']] : 'html', + reporter: process.env.CI ? [["html", { open: "never" }], ["list"]] : "html", use: { baseURL, - trace: 'on-first-retry', - screenshot: 'only-on-failure', - video: 'retain-on-failure', - locale: process.env.LOCALE ?? 'ko-KR', + trace: "on-first-retry", + screenshot: "only-on-failure", + video: "retain-on-failure", + locale: process.env.LOCALE ?? "ko-KR", }, projects: [ { - name: 'webkit-desktop', + name: "webkit-desktop", use: { - ...devices['Desktop Safari'], - serviceWorkers: 'block', + ...devices["Desktop Safari"], + serviceWorkers: "block", }, }, { - name: 'webkit-mobile-webapp', + name: "webkit-mobile-webapp", use: { - ...devices['iPhone 13'], - serviceWorkers: 'block', + ...devices["iPhone 13"], + serviceWorkers: "block", }, }, { - name: 'chromium-desktop', + name: "chromium-desktop", use: { - ...devices['Desktop Chrome'], - serviceWorkers: 'block', + ...devices["Desktop Chrome"], + serviceWorkers: "block", }, }, { - name: 'firefox-desktop', + name: "firefox-desktop", use: { - ...devices['Desktop Firefox'], - serviceWorkers: 'block', + ...devices["Desktop Firefox"], + serviceWorkers: "block", }, }, { - name: 'chromium-mobile-webapp', + name: "chromium-mobile-webapp", use: { - ...devices['Pixel 7'], - serviceWorkers: 'block', + ...devices["Pixel 7"], + serviceWorkers: "block", }, }, ], webServer: process.env.BASE_URL ? undefined : { - command: 'node ./scripts/serve-userfront-build.mjs', + command: "node ./scripts/serve-userfront-build.mjs", url: defaultBaseUrl, reuseExistingServer, timeout: 120_000, diff --git a/userfront-e2e/scripts/serve-userfront-build.mjs b/userfront-e2e/scripts/serve-userfront-build.mjs index 2da87c06..69cd80ae 100644 --- a/userfront-e2e/scripts/serve-userfront-build.mjs +++ b/userfront-e2e/scripts/serve-userfront-build.mjs @@ -1,53 +1,53 @@ -import { createReadStream, existsSync, statSync } from 'node:fs'; -import { dirname, extname, join, normalize } from 'node:path'; -import { createServer } from 'node:http'; -import { fileURLToPath } from 'node:url'; +import { createReadStream, existsSync, statSync } from "node:fs"; +import { createServer } from "node:http"; +import { dirname, extname, join, normalize } from "node:path"; +import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const root = normalize(join(__dirname, '../../userfront/build/web')); +const root = normalize(join(__dirname, "../../userfront/build/web")); if (!existsSync(root) || !statSync(root).isDirectory()) { console.error( - '[userfront-e2e] userfront/build/web not found. Run: cd userfront && flutter build web --wasm --release', + "[userfront-e2e] userfront/build/web not found. Run: cd userfront && flutter build web --wasm --release", ); process.exit(1); } -const port = Number.parseInt(process.env.PORT ?? '4173', 10); +const port = Number.parseInt(process.env.PORT ?? "4173", 10); const contentTypes = { - '.css': 'text/css; charset=utf-8', - '.html': 'text/html; charset=utf-8', - '.ico': 'image/x-icon', - '.js': 'application/javascript; charset=utf-8', - '.json': 'application/json; charset=utf-8', - '.mjs': 'application/javascript; charset=utf-8', - '.png': 'image/png', - '.svg': 'image/svg+xml; charset=utf-8', - '.txt': 'text/plain; charset=utf-8', - '.wasm': 'application/wasm', - '.webmanifest': 'application/manifest+json; charset=utf-8', - '.woff': 'font/woff', - '.woff2': 'font/woff2', + ".css": "text/css; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".ico": "image/x-icon", + ".js": "application/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".mjs": "application/javascript; charset=utf-8", + ".png": "image/png", + ".svg": "image/svg+xml; charset=utf-8", + ".txt": "text/plain; charset=utf-8", + ".wasm": "application/wasm", + ".webmanifest": "application/manifest+json; charset=utf-8", + ".woff": "font/woff", + ".woff2": "font/woff2", }; const server = createServer((req, res) => { - const url = new URL(req.url ?? '/', 'http://localhost'); + const url = new URL(req.url ?? "/", "http://localhost"); const pathname = decodeURIComponent(url.pathname); - if (pathname === '/' && url.search === '') { + if (pathname === "/" && url.search === "") { res.statusCode = 302; - res.setHeader('Location', '/ko/signin'); - res.setHeader('Cache-Control', 'no-cache, max-age=0, must-revalidate'); + res.setHeader("Location", "/ko/signin"); + res.setHeader("Cache-Control", "no-cache, max-age=0, must-revalidate"); res.end(); return; } - const relative = pathname === '/' ? '/index.html' : pathname; + const relative = pathname === "/" ? "/index.html" : pathname; const candidate = normalize(join(root, relative)); if (!candidate.startsWith(root)) { res.statusCode = 403; - res.end('Forbidden'); + res.end("Forbidden"); return; } @@ -57,87 +57,92 @@ const server = createServer((req, res) => { if (!existsSync(filePath) || statSync(filePath).isDirectory()) { if (extname(pathname)) { res.statusCode = 404; - res.setHeader('Cache-Control', 'no-cache, max-age=0, must-revalidate'); - res.end('Not Found'); + res.setHeader("Cache-Control", "no-cache, max-age=0, must-revalidate"); + res.end("Not Found"); return; } // Flutter web 라우팅 경로(`/ko`, `/ko/signin`)도 index.html로 fallback 처리 - filePath = join(root, 'index.html'); + filePath = join(root, "index.html"); servesAppShellFallback = true; } - const acceptsBrotli = /\bbr\b/.test(req.headers['accept-encoding'] ?? ''); + const acceptsBrotli = /\bbr\b/.test(req.headers["accept-encoding"] ?? ""); const brotliPath = `${filePath}.br`; - const servedPath = acceptsBrotli && existsSync(brotliPath) ? brotliPath : filePath; + const servedPath = + acceptsBrotli && existsSync(brotliPath) ? brotliPath : filePath; const ext = extname(filePath); - const contentType = contentTypes[ext] ?? 'application/octet-stream'; + const contentType = contentTypes[ext] ?? "application/octet-stream"; const stats = statSync(servedPath); const etag = `"${stats.size.toString(16)}-${Math.trunc(stats.mtimeMs).toString(16)}"`; - const cacheControl = cacheControlFor(pathname, filePath, servesAppShellFallback); + const cacheControl = cacheControlFor( + pathname, + filePath, + servesAppShellFallback, + ); - res.setHeader('Content-Type', contentType); - res.setHeader('ETag', etag); - res.setHeader('Last-Modified', stats.mtime.toUTCString()); - res.setHeader('Cache-Control', cacheControl); - res.setHeader('Vary', 'Accept-Encoding'); + res.setHeader("Content-Type", contentType); + res.setHeader("ETag", etag); + res.setHeader("Last-Modified", stats.mtime.toUTCString()); + res.setHeader("Cache-Control", cacheControl); + res.setHeader("Vary", "Accept-Encoding"); // Flutter WASM requires SharedArrayBuffer which needs these COOP/COEP headers // to be cross-origin isolated in most modern browsers (WebKit, Firefox, etc.) - res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); - res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); + res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); if (servedPath === brotliPath) { - res.setHeader('Content-Encoding', 'br'); + res.setHeader("Content-Encoding", "br"); } - if (req.headers['if-none-match'] === etag) { + if (req.headers["if-none-match"] === etag) { res.statusCode = 304; res.end(); return; } createReadStream(servedPath) - .on('error', () => { + .on("error", () => { res.statusCode = 500; - res.end('Internal Server Error'); + res.end("Internal Server Error"); }) .pipe(res); }); function cacheControlFor(pathname, filePath, servesAppShellFallback) { - const basename = filePath.split('/').pop() ?? ''; + const basename = filePath.split("/").pop() ?? ""; if ( servesAppShellFallback || - basename === 'index.html' || - basename === 'flutter_bootstrap.js' || - basename === 'flutter_service_worker.js' || - basename === 'version.json' || - basename === 'manifest.json' + basename === "index.html" || + basename === "flutter_bootstrap.js" || + basename === "flutter_service_worker.js" || + basename === "version.json" || + basename === "manifest.json" ) { - return 'no-cache, max-age=0, must-revalidate'; + return "no-cache, max-age=0, must-revalidate"; } if (/^\/canvaskit\/.*\.(?:js|wasm)$/i.test(pathname)) { - return 'public, max-age=31536000, immutable'; + return "public, max-age=31536000, immutable"; } if (/^\/main\.dart\.[0-9a-f]{12}\.(?:js|mjs|wasm)$/i.test(pathname)) { - return 'public, max-age=31536000, immutable'; + return "public, max-age=31536000, immutable"; } if (/\.(?:png|ico|svg|webp|woff|woff2)$/i.test(pathname)) { - return 'public, max-age=31536000, immutable'; + return "public, max-age=31536000, immutable"; } if (/\.(?:js|css|json|mjs|wasm)$/i.test(pathname)) { - return 'no-cache, max-age=0, must-revalidate'; + return "no-cache, max-age=0, must-revalidate"; } - return 'no-cache, max-age=0, must-revalidate'; + return "no-cache, max-age=0, must-revalidate"; } -server.listen(port, '127.0.0.1', () => { +server.listen(port, "127.0.0.1", () => { console.log(`[userfront-e2e] serving ${root} at http://127.0.0.1:${port}`); }); diff --git a/userfront-e2e/tests/auth-routing.spec.ts b/userfront-e2e/tests/auth-routing.spec.ts index 4e81021c..eb58e277 100644 --- a/userfront-e2e/tests/auth-routing.spec.ts +++ b/userfront-e2e/tests/auth-routing.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page, type Route } from '@playwright/test'; +import { expect, type Page, type Route, test } from "@playwright/test"; type MockOptions = { sessionStatus?: number; @@ -9,23 +9,23 @@ type MockOptions = { async function seedTokenLogin(page: Page): Promise { await page.addInitScript(() => { - window.localStorage.setItem('baron_auth_token', 'e30.e30.e30'); - window.localStorage.setItem('baron_auth_provider', 'ory'); - window.localStorage.removeItem('baron_auth_cookie_mode'); - window.localStorage.removeItem('baron_auth_pending_provider'); + window.localStorage.setItem("baron_auth_token", "e30.e30.e30"); + window.localStorage.setItem("baron_auth_provider", "ory"); + window.localStorage.removeItem("baron_auth_cookie_mode"); + window.localStorage.removeItem("baron_auth_pending_provider"); }); } async function seedSessionTokenLogin(page: Page): Promise { await page.addInitScript(() => { - window.sessionStorage.setItem('baron_auth_token', 'e30.e30.e30'); - window.sessionStorage.setItem('baron_auth_provider', 'ory'); - window.sessionStorage.removeItem('baron_auth_cookie_mode'); - window.sessionStorage.removeItem('baron_auth_pending_provider'); - window.localStorage.removeItem('baron_auth_token'); - window.localStorage.removeItem('baron_auth_provider'); - window.localStorage.removeItem('baron_auth_cookie_mode'); - window.localStorage.removeItem('baron_auth_pending_provider'); + window.sessionStorage.setItem("baron_auth_token", "e30.e30.e30"); + window.sessionStorage.setItem("baron_auth_provider", "ory"); + window.sessionStorage.removeItem("baron_auth_cookie_mode"); + window.sessionStorage.removeItem("baron_auth_pending_provider"); + window.localStorage.removeItem("baron_auth_token"); + window.localStorage.removeItem("baron_auth_provider"); + window.localStorage.removeItem("baron_auth_cookie_mode"); + window.localStorage.removeItem("baron_auth_pending_provider"); }); } @@ -35,29 +35,29 @@ async function mockUserfrontApis( ): Promise { const sessionStatus = options.sessionStatus ?? 200; - await page.context().route('**/api/v1/**', async (route: Route) => { + await page.context().route("**/api/v1/**", async (route: Route) => { const requestUrl = new URL(route.request().url()); const path = requestUrl.pathname; - if (path.endsWith('/api/v1/user/me')) { + if (path.endsWith("/api/v1/user/me")) { options.captureUserMe?.(); if (sessionStatus === 200) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ - id: 'e2e-user', - email: 'e2e@example.com', - name: 'E2E User', - phone: '+821012341234', - department: 'QA', - affiliationType: 'employee', - companyCode: 'BARON', + id: "e2e-user", + email: "e2e@example.com", + name: "E2E User", + phone: "+821012341234", + department: "QA", + affiliationType: "employee", + companyCode: "BARON", tenant: { - id: 'tenant-1', - name: 'Baron', - slug: 'baron', - description: 'E2E tenant', + id: "tenant-1", + name: "Baron", + slug: "baron", + description: "E2E tenant", }, }), }); @@ -66,32 +66,32 @@ async function mockUserfrontApis( await route.fulfill({ status: sessionStatus, - contentType: 'application/json', - body: JSON.stringify({ error: 'unauthorized' }), + contentType: "application/json", + body: JSON.stringify({ error: "unauthorized" }), }); return; } - if (path.endsWith('/api/v1/user/rp/linked')) { + if (path.endsWith("/api/v1/user/rp/linked")) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ items: [] }), }); return; } - if (path.endsWith('/api/v1/audit/auth/timeline')) { + if (path.endsWith("/api/v1/audit/auth/timeline")) { await route.fulfill({ status: 200, - contentType: 'application/json', - body: JSON.stringify({ items: [], next_cursor: '' }), + contentType: "application/json", + body: JSON.stringify({ items: [], next_cursor: "" }), }); return; } - if (path.endsWith('/api/v1/auth/qr/approve')) { - if (route.request().method() == 'POST') { + if (path.endsWith("/api/v1/auth/qr/approve")) { + if (route.request().method() === "POST") { let pendingRef: string | null = null; try { const body = (route.request().postDataJSON() ?? {}) as { @@ -100,44 +100,55 @@ async function mockUserfrontApis( pendingRef = body.pendingRef ?? null; console.log(`[E2E-MOCK] /api/v1/auth/qr/approve POST body:`, body); } catch (e) { - console.log(`[E2E-MOCK] /api/v1/auth/qr/approve POST body parse error:`, e); + console.log( + `[E2E-MOCK] /api/v1/auth/qr/approve POST body parse error:`, + e, + ); pendingRef = null; } options.captureApprove?.(pendingRef); } else { - console.log(`[E2E-MOCK] /api/v1/auth/qr/approve ${route.request().method()} request`); + console.log( + `[E2E-MOCK] /api/v1/auth/qr/approve ${route.request().method()} request`, + ); } await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ ok: true }), }); return; } if ( - path.endsWith('/api/v1/auth/magic-link/verify') || - path.endsWith('/api/v1/auth/login/code/verify') || - path.endsWith('/api/v1/auth/login/code/verify-short') + path.endsWith("/api/v1/auth/magic-link/verify") || + path.endsWith("/api/v1/auth/login/code/verify") || + path.endsWith("/api/v1/auth/login/code/verify-short") ) { let body: Record = {}; try { - body = (route.request().postDataJSON() ?? {}) as Record; + body = (route.request().postDataJSON() ?? {}) as Record< + string, + unknown + >; } catch { body = {}; } options.captureVerify?.(path, body); await route.fulfill({ status: 200, - contentType: 'application/json', - body: JSON.stringify({ status: 'approved', pendingRef: 'e2e-approved' }), + contentType: "application/json", + body: JSON.stringify({ + status: "approved", + pendingRef: "e2e-approved", + }), }); return; } await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({}), }); }); @@ -145,15 +156,15 @@ async function mockUserfrontApis( function collectClientFailures(page: Page): string[] { const failures: string[] = []; - page.on('pageerror', (error) => { + page.on("pageerror", (error) => { failures.push(error.message); }); - page.on('console', (message) => { + page.on("console", (message) => { const text = message.text(); if ( - message.type() === 'error' || + message.type() === "error" || (/exception|verify_failed|verification failed|인증 실패/i.test(text) && - !text.includes('Exception while loading service worker')) + !text.includes("Exception while loading service worker")) ) { failures.push(text); } @@ -164,14 +175,14 @@ function collectClientFailures(page: Page): string[] { async function makeWindowCloseNavigateToRoot(page: Page): Promise { await page.addInitScript(() => { window.close = () => { - window.location.href = '/'; + window.location.href = "/"; }; }); } async function enableFlutterAccessibility(page: Page): Promise { await page.waitForTimeout(300); - const button = page.getByRole('button', { name: 'Enable accessibility' }); + const button = page.getByRole("button", { name: "Enable accessibility" }); if (await button.count()) { await button.first().evaluate((node) => { (node as HTMLElement).click(); @@ -179,7 +190,7 @@ async function enableFlutterAccessibility(page: Page): Promise { await page.waitForTimeout(200); return; } - const placeholder = page.locator('flt-semantics-placeholder').first(); + const placeholder = page.locator("flt-semantics-placeholder").first(); if (await placeholder.count()) { await placeholder.evaluate((node) => { (node as HTMLElement).click(); @@ -188,47 +199,51 @@ async function enableFlutterAccessibility(page: Page): Promise { } } -test.describe('UserFront WASM auth routing', () => { - test('비로그인 /ko 진입 시 /ko/signin 으로 리다이렉트된다', async ({ page }) => { +test.describe("UserFront WASM auth routing", () => { + test("비로그인 /ko 진입 시 /ko/signin 으로 리다이렉트된다", async ({ + page, + }) => { await mockUserfrontApis(page, { sessionStatus: 401 }); - await page.goto('/ko'); + await page.goto("/ko"); await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/); }); - test('로그인 상태 /ko 진입 후 새로고침해도 /ko/dashboard 를 유지한다', async ({ + test("로그인 상태 /ko 진입 후 새로고침해도 /ko/dashboard 를 유지한다", async ({ page, }) => { await seedTokenLogin(page); await mockUserfrontApis(page); - await page.goto('/ko'); + await page.goto("/ko"); await expect(page).toHaveURL(/\/ko\/dashboard$/); await page.reload(); await expect(page).toHaveURL(/\/ko\/dashboard$/); }); - test('sessionStorage 기반 로그인 상태에서도 /ko/dashboard 를 유지한다', async ({ + test("sessionStorage 기반 로그인 상태에서도 /ko/dashboard 를 유지한다", async ({ page, }) => { await seedSessionTokenLogin(page); await mockUserfrontApis(page); - await page.goto('/ko'); + await page.goto("/ko"); await expect(page).toHaveURL(/\/ko\/dashboard$/); }); - test('비로그인 /ko/approve 는 signin(+notice)으로 이동한다', async ({ page }) => { + test("비로그인 /ko/approve 는 signin(+notice)으로 이동한다", async ({ + page, + }) => { await mockUserfrontApis(page, { sessionStatus: 401 }); - await page.goto('/ko/approve?ref=e2e-ref'); + await page.goto("/ko/approve?ref=e2e-ref"); await expect(page).toHaveURL(/\/ko\/signin\?notice=qr_login_required$/); }); - test('로그인 상태 /ko/approve 는 승인 API 호출 후 dashboard로 이동한다', async ({ + test("로그인 상태 /ko/approve 는 승인 API 호출 후 dashboard로 이동한다", async ({ page, }) => { let approvedRef: string | null = null; @@ -240,15 +255,15 @@ test.describe('UserFront WASM auth routing', () => { }, }); - await page.goto('/ko/approve?ref=e2e-approve-ref'); + await page.goto("/ko/approve?ref=e2e-approve-ref"); await expect(page).toHaveURL(/\/ko\/dashboard(?:\?.*)?$/, { timeout: 10_000, }); - expect(approvedRef).toBe('e2e-approve-ref'); + expect(approvedRef).toBe("e2e-approve-ref"); }); - test('verifyOnly 승인 완료 화면의 상단 액션은 signin으로 이동시키지 않는다', async ({ + test("verifyOnly 승인 완료 화면의 상단 액션은 signin으로 이동시키지 않는다", async ({ page, }) => { let userMeCalls = 0; @@ -269,20 +284,20 @@ test.describe('UserFront WASM auth routing', () => { }); await makeWindowCloseNavigateToRoot(page); - await page.goto('/ko/l/AB123456'); + await page.goto("/ko/l/AB123456"); await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1); await expect(page).toHaveURL(/\/ko\/verify-complete$/); expect(userMeCalls).toBe(0); expect(verifyRequests[0].path).toContain( - '/api/v1/auth/login/code/verify-short', + "/api/v1/auth/login/code/verify-short", ); expect(verifyRequests[0].body).toMatchObject({ - shortCode: 'AB123456', + shortCode: "AB123456", verifyOnly: true, }); - await page.locator('flt-glass-pane').click({ + await page.locator("flt-glass-pane").click({ position: { x: 30, y: 28 }, force: true, }); @@ -293,7 +308,7 @@ test.describe('UserFront WASM auth routing', () => { expect(clientFailures).toEqual([]); }); - test('verifyOnly 승인 완료 버튼은 SMS 링크에서 로그인 창으로 이동하고 user/me 조회를 만들지 않는다', async ({ + test("verifyOnly 승인 완료 버튼은 SMS 링크에서 로그인 창으로 이동하고 user/me 조회를 만들지 않는다", async ({ page, }) => { let userMeCalls = 0; @@ -311,25 +326,25 @@ test.describe('UserFront WASM auth routing', () => { }); await makeWindowCloseNavigateToRoot(page); - await page.goto('/ko/l/AB123456'); + await page.goto("/ko/l/AB123456"); await expect.poll(() => verifyCalls, { timeout: 10_000 }).toBe(1); await expect(page).toHaveURL(/\/ko\/verify-complete$/); expect(userMeCalls).toBe(0); await enableFlutterAccessibility(page); - await page.getByRole('button', { name: '로그인 창으로 이동하기' }).click(); + await page.getByRole("button", { name: "로그인 창으로 이동하기" }).click(); expect(userMeCalls).toBe(0); await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/); expect( clientFailures.filter( - (failure) => !failure.includes('401 (Unauthorized)'), + (failure) => !failure.includes("401 (Unauthorized)"), ), ).toEqual([]); }); - test('verifyOnly 원격 승인 완료는 로그인 창 이동 모달 CTA를 표시한다', async ({ + test("verifyOnly 원격 승인 완료는 로그인 창 이동 모달 CTA를 표시한다", async ({ page, }) => { let verifyCalls = 0; @@ -343,26 +358,26 @@ test.describe('UserFront WASM auth routing', () => { }); await makeWindowCloseNavigateToRoot(page); - await page.goto('/ko/l/AB123456'); + await page.goto("/ko/l/AB123456"); await expect.poll(() => verifyCalls, { timeout: 10_000 }).toBe(1); await expect(page).toHaveURL(/\/ko\/verify-complete$/); await enableFlutterAccessibility(page); await expect( - page.getByText('요청하신 로그인이 완료되었습니다'), + page.getByText("요청하신 로그인이 완료되었습니다"), ).toBeVisible(); - await expect(page.getByRole('button', { name: '창 닫기' })).toHaveCount(0); + await expect(page.getByRole("button", { name: "창 닫기" })).toHaveCount(0); await expect( - page.getByRole('button', { name: '로그인 창으로 이동하기' }), + page.getByRole("button", { name: "로그인 창으로 이동하기" }), ).toBeVisible(); - await page.getByRole('button', { name: '로그인 창으로 이동하기' }).click(); + await page.getByRole("button", { name: "로그인 창으로 이동하기" }).click(); await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/); expect(clientFailures).toEqual([]); }); - test('루트/로그인에 붙은 인증 payload는 전용 verify 라우트에서만 소비하고 완료 URL을 정리한다', async ({ + test("루트/로그인에 붙은 인증 payload는 전용 verify 라우트에서만 소비하고 완료 URL을 정리한다", async ({ page, }) => { let userMeCalls = 0; @@ -383,26 +398,26 @@ test.describe('UserFront WASM auth routing', () => { }); await page.goto( - '/?loginId=e2e%40example.com&code=654321&pendingRef=pending-root&utm=drop', + "/?loginId=e2e%40example.com&code=654321&pendingRef=pending-root&utm=drop", ); await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1); await expect(page).toHaveURL(/\/ko\/verify-complete$/); expect(userMeCalls).toBe(0); - expect(verifyRequests[0].path).toContain('/api/v1/auth/login/code/verify'); + expect(verifyRequests[0].path).toContain("/api/v1/auth/login/code/verify"); expect(verifyRequests[0].body).toMatchObject({ - loginId: 'e2e@example.com', - code: '654321', - pendingRef: 'pending-root', + loginId: "e2e@example.com", + code: "654321", + pendingRef: "pending-root", verifyOnly: true, }); - expect(page.url()).not.toContain('loginId='); - expect(page.url()).not.toContain('code='); - expect(page.url()).not.toContain('pendingRef='); - expect(page.url()).not.toContain('utm='); + expect(page.url()).not.toContain("loginId="); + expect(page.url()).not.toContain("code="); + expect(page.url()).not.toContain("pendingRef="); + expect(page.url()).not.toContain("utm="); expect(clientFailures).toEqual([]); }); - test('로그인 페이지에 붙은 인증 payload도 전용 verify 라우트로 넘긴다', async ({ + test("로그인 페이지에 붙은 인증 payload도 전용 verify 라우트로 넘긴다", async ({ page, }) => { let userMeCalls = 0; @@ -422,26 +437,26 @@ test.describe('UserFront WASM auth routing', () => { }, }); - await page.goto('/ko/signin?loginId=e2e%40example.com&code=999999'); + await page.goto("/ko/signin?loginId=e2e%40example.com&code=999999"); await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1); await expect(page).toHaveURL(/\/ko\/verify-complete$/); expect(userMeCalls).toBe(0); expect(verifyRequests[0].body).toMatchObject({ - loginId: 'e2e@example.com', - code: '999999', + loginId: "e2e@example.com", + code: "999999", verifyOnly: true, }); - expect(page.url()).not.toContain('loginId='); - expect(page.url()).not.toContain('code='); + expect(page.url()).not.toContain("loginId="); + expect(page.url()).not.toContain("code="); expect(clientFailures).toEqual([]); }); - test('verifyOnly 승인 링크를 팝업에서 닫으면 창만 닫히고 부모는 이동하지 않는다', async ({ + test("verifyOnly 승인 링크를 팝업에서 닫으면 창만 닫히고 부모는 이동하지 않는다", async ({ page, }, testInfo) => { test.skip( - testInfo.project.name === 'webkit-mobile-webapp', - 'Mobile WebKit closes the opener page when this popup flow closes in headless mode.', + testInfo.project.name === "webkit-mobile-webapp", + "Mobile WebKit closes the opener page when this popup flow closes in headless mode.", ); let userMeCalls = 0; let verifyCalls = 0; @@ -458,16 +473,16 @@ test.describe('UserFront WASM auth routing', () => { }); const baseURL = testInfo.project.use.baseURL; - if (typeof baseURL !== 'string') throw new Error('baseURL is required'); - const popupURL = new URL('/ko/l/AB123456', baseURL).toString(); - const parentURL = new URL('/version.json', baseURL).toString(); + if (typeof baseURL !== "string") throw new Error("baseURL is required"); + const popupURL = new URL("/ko/l/AB123456", baseURL).toString(); + const parentURL = new URL("/version.json", baseURL).toString(); await page.goto(parentURL); await expect(page).toHaveURL(parentURL); - const popupPromise = page.waitForEvent('popup'); + const popupPromise = page.waitForEvent("popup"); await page.evaluate((url) => { - window.open(url, '_blank'); + window.open(url, "_blank"); }, popupURL); const popup = await popupPromise; @@ -477,10 +492,10 @@ test.describe('UserFront WASM auth routing', () => { if (!popup.isClosed()) { await enableFlutterAccessibility(popup); - const closePromise = popup.waitForEvent('close').catch(() => undefined); + const closePromise = popup.waitForEvent("close").catch(() => undefined); try { await popup - .getByRole('button', { name: '로그인 창으로 이동하기' }) + .getByRole("button", { name: "로그인 창으로 이동하기" }) .click(); } catch (error) { if (!popup.isClosed()) { @@ -495,7 +510,7 @@ test.describe('UserFront WASM auth routing', () => { expect(clientFailures).toEqual([]); }); - test('verifyOnly 승인 완료 버튼은 이메일 magic link에서도 로그인 창으로 이동하고 user/me 조회를 만들지 않는다', async ({ + test("verifyOnly 승인 완료 버튼은 이메일 magic link에서도 로그인 창으로 이동하고 user/me 조회를 만들지 않는다", async ({ page, }) => { let userMeCalls = 0; @@ -516,26 +531,26 @@ test.describe('UserFront WASM auth routing', () => { }); await makeWindowCloseNavigateToRoot(page); - await page.goto('/ko/verify/e2e-email-token'); + await page.goto("/ko/verify/e2e-email-token"); await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1); await expect(page).toHaveURL(/\/ko\/verify-complete$/); expect(userMeCalls).toBe(0); - expect(verifyRequests[0].path).toContain('/api/v1/auth/magic-link/verify'); + expect(verifyRequests[0].path).toContain("/api/v1/auth/magic-link/verify"); expect(verifyRequests[0].body).toMatchObject({ - token: 'e2e-email-token', + token: "e2e-email-token", verifyOnly: true, }); await enableFlutterAccessibility(page); - await page.getByRole('button', { name: '로그인 창으로 이동하기' }).click(); + await page.getByRole("button", { name: "로그인 창으로 이동하기" }).click(); expect(userMeCalls).toBe(0); await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/); expect(clientFailures).toEqual([]); }); - test('verifyOnly 승인 완료 버튼은 이메일 code link에서도 로그인 창으로 이동하고 user/me 조회를 만들지 않는다', async ({ + test("verifyOnly 승인 완료 버튼은 이메일 code link에서도 로그인 창으로 이동하고 user/me 조회를 만들지 않는다", async ({ page, }) => { let userMeCalls = 0; @@ -557,22 +572,22 @@ test.describe('UserFront WASM auth routing', () => { await makeWindowCloseNavigateToRoot(page); await page.goto( - '/ko/verify?loginId=e2e%40example.com&code=654321&pendingRef=pending-email', + "/ko/verify?loginId=e2e%40example.com&code=654321&pendingRef=pending-email", ); await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1); await expect(page).toHaveURL(/\/ko\/verify-complete$/); expect(userMeCalls).toBe(0); - expect(verifyRequests[0].path).toContain('/api/v1/auth/login/code/verify'); + expect(verifyRequests[0].path).toContain("/api/v1/auth/login/code/verify"); expect(verifyRequests[0].body).toMatchObject({ - loginId: 'e2e@example.com', - code: '654321', - pendingRef: 'pending-email', + loginId: "e2e@example.com", + code: "654321", + pendingRef: "pending-email", verifyOnly: true, }); await enableFlutterAccessibility(page); - await page.getByRole('button', { name: '로그인 창으로 이동하기' }).click(); + await page.getByRole("button", { name: "로그인 창으로 이동하기" }).click(); expect(userMeCalls).toBe(0); await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/); diff --git a/userfront-e2e/tests/login-performance-budget.spec.ts b/userfront-e2e/tests/login-performance-budget.spec.ts index 0d400fbc..a0423838 100644 --- a/userfront-e2e/tests/login-performance-budget.spec.ts +++ b/userfront-e2e/tests/login-performance-budget.spec.ts @@ -1,11 +1,11 @@ import { devices, expect, - test, type Page, type Request, type Response, -} from '@playwright/test'; + test, +} from "@playwright/test"; type LoadMetrics = { appOrigin: string; @@ -18,20 +18,20 @@ type LoadMetrics = { }; async function mockPublicApis(page: Page): Promise { - await page.route('**/api/v1/**', async (route) => { + await page.route("**/api/v1/**", async (route) => { const requestUrl = new URL(route.request().url()); - if (requestUrl.pathname.endsWith('/api/v1/user/me')) { + if (requestUrl.pathname.endsWith("/api/v1/user/me")) { await route.fulfill({ status: 401, - contentType: 'application/json', - body: JSON.stringify({ error: 'unauthorized' }), + contentType: "application/json", + body: JSON.stringify({ error: "unauthorized" }), }); return; } await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({}), }); }); @@ -39,7 +39,7 @@ async function mockPublicApis(page: Page): Promise { async function measureSigninLoad(page: Page): Promise { const appOrigin = new URL( - process.env.BASE_URL ?? `http://127.0.0.1:${process.env.PORT ?? '4173'}`, + process.env.BASE_URL ?? `http://127.0.0.1:${process.env.PORT ?? "4173"}`, ).origin; const requestedUrls: string[] = []; const requestedPathCounts = new Map(); @@ -50,7 +50,7 @@ async function measureSigninLoad(page: Page): Promise { const onRequest = (request: Request) => { const requestUrl = new URL(request.url()); requestedUrls.push(request.url()); - if (requestUrl.protocol === 'http:' || requestUrl.protocol === 'https:') { + if (requestUrl.protocol === "http:" || requestUrl.protocol === "https:") { const resourceKey = `${requestUrl.origin}${requestUrl.pathname}`; requestedPathCounts.set( resourceKey, @@ -61,28 +61,31 @@ async function measureSigninLoad(page: Page): Promise { const onResponse = async (response: Response) => { const url = new URL(response.url()); - const cacheControl = response.headers()['cache-control']; + const cacheControl = response.headers()["cache-control"]; if (cacheControl) { cacheControlByPath.set(url.pathname, cacheControl); } - const contentEncoding = response.headers()['content-encoding']; + const contentEncoding = response.headers()["content-encoding"]; if (contentEncoding) { contentEncodingByPath.set(url.pathname, contentEncoding); } const timing = response.request().timing(); if (timing.responseEnd >= 0) { - const sizes = await response.request().sizes().catch(() => null); + const sizes = await response + .request() + .sizes() + .catch(() => null); transferredBytes += sizes?.responseBodySize ?? 0; } }; - page.on('request', onRequest); - page.on('response', onResponse); + page.on("request", onRequest); + page.on("response", onResponse); try { const start = performance.now(); - await page.goto('/ko/signin', { waitUntil: 'networkidle' }); + await page.goto("/ko/signin", { waitUntil: "networkidle" }); await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/); const durationMs = Math.round(performance.now() - start); @@ -96,8 +99,8 @@ async function measureSigninLoad(page: Page): Promise { contentEncodingByPath, }; } finally { - page.off('request', onRequest); - page.off('response', onResponse); + page.off("request", onRequest); + page.off("response", onResponse); } } @@ -109,13 +112,13 @@ function expectNoDuplicateStaticRequests(metrics: LoadMetrics): void { return ( count > 1 && resourceUrl.origin === metrics.appOrigin && - !path.startsWith('/api/') && - !path.endsWith('/ko/signin') && - !path.endsWith('/') && - !path.endsWith('/main.dart.wasm') && - !path.endsWith('/main.dart.mjs') && - !path.endsWith('/skwasm.js') && - !path.endsWith('/skwasm.wasm') + !path.startsWith("/api/") && + !path.endsWith("/ko/signin") && + !path.endsWith("/") && + !path.endsWith("/main.dart.wasm") && + !path.endsWith("/main.dart.mjs") && + !path.endsWith("/skwasm.js") && + !path.endsWith("/skwasm.wasm") ); }, ); @@ -126,41 +129,41 @@ function resolvePerformanceBudget(projectName: string): { coldMs: number; warmMs: number; } { - if (projectName.includes('webkit')) { + if (projectName.includes("webkit")) { return { coldMs: 4000, warmMs: 4000 }; } - if (projectName.includes('firefox')) { + if (projectName.includes("firefox")) { return { coldMs: 2600, warmMs: 2800 }; } - if (projectName.includes('mobile')) { + if (projectName.includes("mobile")) { return { coldMs: 3000, warmMs: 2300 }; } return { coldMs: 2300, warmMs: 1500 }; } function resolveRootRedirectBudget(projectName: string): number { - if (projectName.includes('webkit')) { + if (projectName.includes("webkit")) { return 700; } - if (projectName.includes('firefox')) { + if (projectName.includes("firefox")) { return 600; } return 300; } -test.describe('UserFront login performance budget', () => { - test('mobile Chrome service worker install does not fetch unused CanvasKit variants', async ({ +test.describe("UserFront login performance budget", () => { + test("mobile Chrome service worker install does not fetch unused CanvasKit variants", async ({ browser, }, testInfo) => { test.skip( - testInfo.project.name !== 'chromium-mobile-webapp', - 'service worker install race is covered once in the mobile Chromium project', + testInfo.project.name !== "chromium-mobile-webapp", + "service worker install race is covered once in the mobile Chromium project", ); const context = await browser.newContext({ - ...devices['Pixel 7'], - locale: 'ko-KR', - serviceWorkers: 'allow', + ...devices["Pixel 7"], + locale: "ko-KR", + serviceWorkers: "allow", }); const page = await context.newPage(); await mockPublicApis(page); @@ -168,22 +171,23 @@ test.describe('UserFront login performance budget', () => { try { const serviceWorkerResponse = await context.request.get( new URL( - '/flutter_service_worker.js', - process.env.BASE_URL ?? `http://127.0.0.1:${process.env.PORT ?? '4173'}`, + "/flutter_service_worker.js", + process.env.BASE_URL ?? + `http://127.0.0.1:${process.env.PORT ?? "4173"}`, ).toString(), ); const serviceWorkerBody = await serviceWorkerResponse.text(); expect(serviceWorkerBody).not.toContain('"/canvaskit/'); expect(serviceWorkerBody).not.toContain('"/main.dart.'); - await page.goto('/ko/signin', { waitUntil: 'domcontentloaded' }); + await page.goto("/ko/signin", { waitUntil: "domcontentloaded" }); await page.waitForTimeout(3_000); } finally { await context.close(); } }); - test('warm login page load stays within the platform budget and reuses cached assets', async ({ + test("warm login page load stays within the platform budget and reuses cached assets", async ({ page, }, testInfo) => { await mockPublicApis(page); @@ -209,14 +213,14 @@ test.describe('UserFront login performance budget', () => { ...warm.contentEncodingByPath, ]); - const appShellCache = cacheControlByPath.get('/ko/signin') ?? ''; - expect(appShellCache).toContain('no-cache'); + const appShellCache = cacheControlByPath.get("/ko/signin") ?? ""; + expect(appShellCache).toContain("no-cache"); const serviceWorkerState = await page.evaluate(async () => { - if (!('serviceWorker' in navigator)) { + if (!("serviceWorker" in navigator)) { return { available: false, secure: window.isSecureContext, - scriptUrl: '', + scriptUrl: "", }; } const registrations = await navigator.serviceWorker.getRegistrations(); @@ -225,43 +229,48 @@ test.describe('UserFront login performance budget', () => { available: true, secure: window.isSecureContext, count: registrations.length, - controller: navigator.serviceWorker.controller?.scriptURL ?? '', + controller: navigator.serviceWorker.controller?.scriptURL ?? "", scriptUrl: registration?.active?.scriptURL ?? registration?.waiting?.scriptURL ?? registration?.installing?.scriptURL ?? - '', + "", }; }); - if (testInfo.project.name.includes('mobile') && serviceWorkerState.scriptUrl) { + if ( + testInfo.project.name.includes("mobile") && + serviceWorkerState.scriptUrl + ) { expect(new URL(serviceWorkerState.scriptUrl).pathname).toBe( - '/flutter_service_worker.js', + "/flutter_service_worker.js", ); - const serviceWorkerResponse = await page.context().request.get( - new URL('/flutter_service_worker.js', page.url()).toString(), - ); - expect(serviceWorkerResponse.headers()['cache-control'] ?? '').toContain( - 'no-cache', + const serviceWorkerResponse = await page + .context() + .request.get( + new URL("/flutter_service_worker.js", page.url()).toString(), + ); + expect(serviceWorkerResponse.headers()["cache-control"] ?? "").toContain( + "no-cache", ); } else { - expect(serviceWorkerState.scriptUrl).toBe(''); + expect(serviceWorkerState.scriptUrl).toBe(""); } expect(cold.durationMs).toBeGreaterThanOrEqual(0); }); - test('root redirects to localized signin before Flutter boots', async ({ + test("root redirects to localized signin before Flutter boots", async ({ page, }, testInfo) => { await mockPublicApis(page); const requestedUrls: string[] = []; - page.on('request', (request) => { + page.on("request", (request) => { requestedUrls.push(request.url()); }); const start = performance.now(); - await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.goto("/", { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/); const durationMs = Math.round(performance.now() - start); @@ -269,10 +278,10 @@ test.describe('UserFront login performance budget', () => { resolveRootRedirectBudget(testInfo.project.name), ); const rootIndex = requestedUrls.findIndex( - (url) => new URL(url).pathname === '/', + (url) => new URL(url).pathname === "/", ); const bootstrapIndex = requestedUrls.findIndex((url) => - new URL(url).pathname.endsWith('/flutter_bootstrap.js'), + new URL(url).pathname.endsWith("/flutter_bootstrap.js"), ); expect(rootIndex).toBeGreaterThanOrEqual(0); expect(bootstrapIndex).toBeGreaterThan(rootIndex); diff --git a/userfront-e2e/tests/oidc-login-challenge.spec.ts b/userfront-e2e/tests/oidc-login-challenge.spec.ts index 920a69b4..7184963f 100644 --- a/userfront-e2e/tests/oidc-login-challenge.spec.ts +++ b/userfront-e2e/tests/oidc-login-challenge.spec.ts @@ -1,26 +1,26 @@ -import { expect, test, type Page, type Route } from '@playwright/test'; +import { expect, type Page, type Route, test } from "@playwright/test"; async function mockUserfrontApisForRepro( page: Page, options: { sessionStatus: number } = { sessionStatus: 401 }, ): Promise { - await page.route('**/api/v1/**', async (route: Route) => { + await page.route("**/api/v1/**", async (route: Route) => { const requestUrl = new URL(route.request().url()); const path = requestUrl.pathname; - if (path.endsWith('/api/v1/user/me')) { + if (path.endsWith("/api/v1/user/me")) { await route.fulfill({ status: options.sessionStatus, - contentType: 'application/json', - body: JSON.stringify({ error: 'unauthorized' }), + contentType: "application/json", + body: JSON.stringify({ error: "unauthorized" }), }); return; } - if (path.endsWith('/api/v1/client-log')) { + if (path.endsWith("/api/v1/client-log")) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ ok: true }), }); return; @@ -29,23 +29,25 @@ async function mockUserfrontApisForRepro( // Default mock for other APIs await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({}), }); }); } -test.describe('Issue #345 Reproduction (Log-based Validation)', () => { - test('비로그인 상태에서 login_challenge와 함께 signin 진입 시 루프 없이 로그가 정상 출력되어야 한다', async ({ page }) => { +test.describe("Issue #345 Reproduction (Log-based Validation)", () => { + test("비로그인 상태에서 login_challenge와 함께 signin 진입 시 루프 없이 로그가 정상 출력되어야 한다", async ({ + page, + }) => { const logs: string[] = []; - page.on('console', msg => { + page.on("console", (msg) => { const text = msg.text(); logs.push(text); console.log(`[Browser] ${text}`); }); const requests: string[] = []; - page.on('request', request => { + page.on("request", (request) => { if (request.isNavigationRequest()) { requests.push(request.url()); } @@ -53,29 +55,31 @@ test.describe('Issue #345 Reproduction (Log-based Validation)', () => { await mockUserfrontApisForRepro(page, { sessionStatus: 401 }); - const targetUrl = '/ko/signin?login_challenge=repro_challenge_12345'; + const targetUrl = "/ko/signin?login_challenge=repro_challenge_12345"; await page.goto(targetUrl); - + // WASM 앱 로딩 및 로직 실행 대기 await page.waitForTimeout(7000); const currentUrl = page.url(); - const signinNavigations = requests.filter(url => url.includes('/signin')); + const signinNavigations = requests.filter((url) => url.includes("/signin")); // [검증 1] URL 유지 확인 - expect(currentUrl).toContain('login_challenge=repro_challenge_12345'); - + expect(currentUrl).toContain("login_challenge=repro_challenge_12345"); + // [검증 2] 리다이렉트 루프 발생 여부 확인 (최초 진입 1회만 있어야 함) expect(signinNavigations.length).toBeLessThanOrEqual(1); // [검증 3] 핵심 로직 로그 확인 (성공의 결정적 증거) // 이전에는 여기서 Exception이 발생했으나, 이제는 아래 로그가 찍혀야 함 - const hasSuccessLog = logs.some(log => - log.includes('[Auth] OIDC auto-accept: No active session (status: 401)') + const hasSuccessLog = logs.some((log) => + log.includes("[Auth] OIDC auto-accept: No active session (status: 401)"), ); - + expect(hasSuccessLog).toBe(true); - - console.log('✅ 루프가 해결되었으며, 로그 검증을 통해 정상 동작을 확인했습니다.'); + + console.log( + "✅ 루프가 해결되었으며, 로그 검증을 통해 정상 동작을 확인했습니다.", + ); }); }); diff --git a/userfront-e2e/tests/password-and-reset.spec.ts b/userfront-e2e/tests/password-and-reset.spec.ts index e722ef6d..21ad4a93 100644 --- a/userfront-e2e/tests/password-and-reset.spec.ts +++ b/userfront-e2e/tests/password-and-reset.spec.ts @@ -1,4 +1,10 @@ -import { expect, test, type Locator, type Page, type Route } from '@playwright/test'; +import { + expect, + type Locator, + type Page, + type Route, + test, +} from "@playwright/test"; type RequestCapture = { loginBody?: Record; @@ -7,13 +13,14 @@ type RequestCapture = { clientLogs: string[]; }; -const resetNewPasswordName = /^(새 비밀번호|ui\.userfront\.reset\.new_password)$/; +const resetNewPasswordName = + /^(새 비밀번호|ui\.userfront\.reset\.new_password)$/; const resetConfirmPasswordName = /^(새 비밀번호 확인|ui\.userfront\.reset\.confirm_password)$/; const resetSubmitButtonName = /^(비밀번호 변경|ui\.userfront\.reset\.submit)$/; async function enableFlutterAccessibility(page: Page): Promise { - const button = page.getByRole('button', { name: 'Enable accessibility' }); + const button = page.getByRole("button", { name: "Enable accessibility" }); if (await button.count()) { await button.first().evaluate((node) => { (node as HTMLElement).click(); @@ -22,7 +29,7 @@ async function enableFlutterAccessibility(page: Page): Promise { return; } await page.waitForTimeout(300); - const placeholder = page.locator('flt-semantics-placeholder').first(); + const placeholder = page.locator("flt-semantics-placeholder").first(); if (await placeholder.count()) { await placeholder.evaluate((node) => { (node as HTMLElement).click(); @@ -98,7 +105,7 @@ async function clickPasswordTab(page: Page): Promise { } const coords = coordsFor(page); await page.waitForTimeout(900); - const pane = page.locator('flt-glass-pane'); + const pane = page.locator("flt-glass-pane"); await pane.click({ position: { x: coords.signinPasswordTabX, y: coords.signinTabY }, force: true, @@ -111,12 +118,17 @@ async function clickPasswordTab(page: Page): Promise { await page.waitForTimeout(200); } -async function fillAt(page: Page, x: number, y: number, value: string): Promise { - const pane = page.locator('flt-glass-pane'); +async function fillAt( + page: Page, + x: number, + y: number, + value: string, +): Promise { + const pane = page.locator("flt-glass-pane"); await pane.click({ position: { x, y }, force: true }); await page.waitForTimeout(100); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Backspace'); + await page.keyboard.press("Control+A"); + await page.keyboard.press("Backspace"); await page.keyboard.type(value); } @@ -127,8 +139,8 @@ async function typeIntoAccessibleField( ): Promise { await field.click({ force: true }); await page.waitForTimeout(100); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Backspace'); + await page.keyboard.press("Control+A"); + await page.keyboard.press("Backspace"); await page.keyboard.type(value); } @@ -139,7 +151,7 @@ async function fillPasswordLoginForm( ): Promise { if (isMobileProject(page)) { await enableFlutterAccessibility(page); - const inputs = page.getByRole('textbox'); + const inputs = page.getByRole("textbox"); await inputs.nth(0).fill(loginId); await inputs.nth(1).fill(password); return; @@ -152,32 +164,47 @@ async function fillPasswordLoginForm( async function submitPasswordLogin(page: Page): Promise { if (isMobileProject(page)) { await enableFlutterAccessibility(page); - await page.getByRole('button', { name: '로그인' }).click({ force: true }); + await page.getByRole("button", { name: "로그인" }).click({ force: true }); return; } - await page.keyboard.press('Enter'); + await page.keyboard.press("Enter"); } -async function fillResetPasswordForm(page: Page, password: string): Promise { +async function fillResetPasswordForm( + page: Page, + password: string, +): Promise { await enableFlutterAccessibility(page); - const newPasswordInput = page.getByRole('textbox', { + const newPasswordInput = page.getByRole("textbox", { name: resetNewPasswordName, }); - const confirmPasswordInput = page.getByRole('textbox', { + const confirmPasswordInput = page.getByRole("textbox", { name: resetConfirmPasswordName, }); - if ((await newPasswordInput.count()) > 0 && (await confirmPasswordInput.count()) > 0) { + if ( + (await newPasswordInput.count()) > 0 && + (await confirmPasswordInput.count()) > 0 + ) { await typeIntoAccessibleField(page, newPasswordInput, password); await typeIntoAccessibleField(page, confirmPasswordInput, password); return; } if (isMobileProject(page)) { - await page.getByRole('textbox', { name: resetNewPasswordName }).fill(password); - await page.getByRole('textbox', { name: resetConfirmPasswordName }).fill(password); + await page + .getByRole("textbox", { name: resetNewPasswordName }) + .fill(password); + await page + .getByRole("textbox", { name: resetConfirmPasswordName }) + .fill(password); return; } const coords = coordsFor(page); - await fillAt(page, coords.resetNewPasswordX, coords.resetNewPasswordY, password); + await fillAt( + page, + coords.resetNewPasswordX, + coords.resetNewPasswordY, + password, + ); await fillAt( page, coords.resetConfirmPasswordX, @@ -188,7 +215,9 @@ async function fillResetPasswordForm(page: Page, password: string): Promise { await enableFlutterAccessibility(page); - const submitButton = page.getByRole('button', { name: resetSubmitButtonName }); + const submitButton = page.getByRole("button", { + name: resetSubmitButtonName, + }); if ((await submitButton.count()) > 0) { await submitButton.click({ force: true }); return; @@ -197,32 +226,35 @@ async function submitResetPassword(page: Page): Promise { return; } const coords = coordsFor(page); - await page.locator('flt-glass-pane').click({ + await page.locator("flt-glass-pane").click({ position: { x: coords.resetSubmitX, y: coords.resetSubmitY }, force: true, }); } -async function mockAuthApis(page: Page, capture: RequestCapture): Promise { - await page.route('**/api/v1/**', async (route: Route) => { +async function mockAuthApis( + page: Page, + capture: RequestCapture, +): Promise { + await page.route("**/api/v1/**", async (route: Route) => { const requestUrl = new URL(route.request().url()); const path = requestUrl.pathname; - if (path.endsWith('/api/v1/auth/password/login')) { + if (path.endsWith("/api/v1/auth/password/login")) { capture.loginBody = (route.request().postDataJSON() ?? {}) as Record< string, unknown >; - const loginId = String(capture.loginBody.loginId ?? ''); - const password = String(capture.loginBody.password ?? ''); - if (loginId === 'e2e@example.com' && password === 'ValidPass1!') { + const loginId = String(capture.loginBody.loginId ?? ""); + const password = String(capture.loginBody.password ?? ""); + if (loginId === "e2e@example.com" && password === "ValidPass1!") { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ - sessionJwt: 'e30.e30.e30', - provider: 'ory', + sessionJwt: "e30.e30.e30", + provider: "ory", }), }); return; @@ -230,16 +262,16 @@ async function mockAuthApis(page: Page, capture: RequestCapture): Promise await route.fulfill({ status: 401, - contentType: 'application/json', - body: JSON.stringify({ error: 'password_or_email_mismatch' }), + contentType: "application/json", + body: JSON.stringify({ error: "password_or_email_mismatch" }), }); return; } - if (path.endsWith('/api/v1/auth/password/policy')) { + if (path.endsWith("/api/v1/auth/password/policy")) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ minLength: 12, minCharacterTypes: 3, @@ -252,21 +284,21 @@ async function mockAuthApis(page: Page, capture: RequestCapture): Promise return; } - if (path.endsWith('/api/v1/auth/password/reset/complete')) { + if (path.endsWith("/api/v1/auth/password/reset/complete")) { capture.resetBody = (route.request().postDataJSON() ?? {}) as Record< string, unknown >; - capture.resetToken = requestUrl.searchParams.get('token'); + capture.resetToken = requestUrl.searchParams.get("token"); await route.fulfill({ status: 200, - contentType: 'application/json', - body: JSON.stringify({ status: 'ok' }), + contentType: "application/json", + body: JSON.stringify({ status: "ok" }), }); return; } - if (path.endsWith('/api/v1/client-log')) { + if (path.endsWith("/api/v1/client-log")) { const payload = (route.request().postDataJSON() ?? {}) as { message?: string; }; @@ -275,108 +307,112 @@ async function mockAuthApis(page: Page, capture: RequestCapture): Promise } await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ ok: true }), }); return; } - if (path.endsWith('/api/v1/user/me')) { - const authHeader = route.request().headers()['authorization'] ?? ''; - if (!authHeader.startsWith('Bearer ')) { + if (path.endsWith("/api/v1/user/me")) { + const authHeader = route.request().headers()["authorization"] ?? ""; + if (!authHeader.startsWith("Bearer ")) { await route.fulfill({ status: 401, - contentType: 'application/json', - body: JSON.stringify({ error: 'unauthorized' }), + contentType: "application/json", + body: JSON.stringify({ error: "unauthorized" }), }); return; } await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ - id: 'e2e-user', - email: 'e2e@example.com', - name: 'E2E User', - phone: '+821012341234', - department: 'QA', - affiliationType: 'employee', - companyCode: 'BARON', + id: "e2e-user", + email: "e2e@example.com", + name: "E2E User", + phone: "+821012341234", + department: "QA", + affiliationType: "employee", + companyCode: "BARON", tenant: { - id: 'tenant-1', - name: 'Baron', - slug: 'baron', - description: 'E2E tenant', + id: "tenant-1", + name: "Baron", + slug: "baron", + description: "E2E tenant", }, }), }); return; } - if (path.endsWith('/api/v1/user/rp/linked')) { + if (path.endsWith("/api/v1/user/rp/linked")) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ items: [] }), }); return; } - if (path.endsWith('/api/v1/audit/auth/timeline')) { + if (path.endsWith("/api/v1/audit/auth/timeline")) { await route.fulfill({ status: 200, - contentType: 'application/json', - body: JSON.stringify({ items: [], next_cursor: '' }), + contentType: "application/json", + body: JSON.stringify({ items: [], next_cursor: "" }), }); return; } await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({}), }); }); } -test.describe('UserFront WASM password login and reset', () => { - test.skip(({ isMobile }) => isMobile, 'Desktop only (hardcoded coordinates)'); - test('비밀번호 로그인 성공 시 dashboard로 이동하고 토큰을 저장한다', async ({ page }) => { +test.describe("UserFront WASM password login and reset", () => { + test.skip(({ isMobile }) => isMobile, "Desktop only (hardcoded coordinates)"); + test("비밀번호 로그인 성공 시 dashboard로 이동하고 토큰을 저장한다", async ({ + page, + }) => { test.skip( isMobileProject(page), - 'Mobile webapp keeps dedicated auth-routing coverage; password form canvas automation is covered on desktop.', + "Mobile webapp keeps dedicated auth-routing coverage; password form canvas automation is covered on desktop.", ); const capture: RequestCapture = { clientLogs: [] }; await mockAuthApis(page, capture); - await page.goto('/ko/signin'); + await page.goto("/ko/signin"); await clickPasswordTab(page); - await fillPasswordLoginForm(page, 'e2e@example.com', 'ValidPass1!'); + await fillPasswordLoginForm(page, "e2e@example.com", "ValidPass1!"); await submitPasswordLogin(page); await expect(page).toHaveURL(/\/ko\/dashboard$/); - expect(capture.loginBody?.loginId).toBe('e2e@example.com'); - expect(capture.loginBody?.password).toBe('ValidPass1!'); + expect(capture.loginBody?.loginId).toBe("e2e@example.com"); + expect(capture.loginBody?.password).toBe("ValidPass1!"); const storedToken = await page.evaluate(() => - window.localStorage.getItem('baron_auth_token'), + window.localStorage.getItem("baron_auth_token"), ); - expect(storedToken).toBe('e30.e30.e30'); + expect(storedToken).toBe("e30.e30.e30"); }); - test('비밀번호 로그인 실패 시 에러 코드를 사용자에게 표시한다', async ({ page }) => { + test("비밀번호 로그인 실패 시 에러 코드를 사용자에게 표시한다", async ({ + page, + }) => { test.skip( isMobileProject(page), - 'Mobile webapp keeps dedicated auth-routing coverage; password form canvas automation is covered on desktop.', + "Mobile webapp keeps dedicated auth-routing coverage; password form canvas automation is covered on desktop.", ); const capture: RequestCapture = { clientLogs: [] }; await mockAuthApis(page, capture); - await page.goto('/ko/signin'); + await page.goto("/ko/signin"); await clickPasswordTab(page); - await fillPasswordLoginForm(page, 'e2e@example.com', 'WrongPass1!'); + await fillPasswordLoginForm(page, "e2e@example.com", "WrongPass1!"); await submitPasswordLogin(page); await expect(page).toHaveURL(/\/ko\/signin$/); @@ -384,36 +420,37 @@ test.describe('UserFront WASM password login and reset', () => { .poll( () => capture.clientLogs.some((message) => - message.includes('password_or_email_mismatch'), + message.includes("password_or_email_mismatch"), ), { timeout: 10000 }, ) .toBe(true); }); - test('reset-password에서 변경 성공 시 signin으로 이동한다', async ({ page }) => { + test("reset-password에서 변경 성공 시 signin으로 이동한다", async ({ + page, + }) => { const capture: RequestCapture = { clientLogs: [] }; await mockAuthApis(page, capture); const policyLoaded = page.waitForResponse( (response) => - response.url().includes('/api/v1/auth/password/policy') && + response.url().includes("/api/v1/auth/password/policy") && response.status() === 200, ); - await page.goto('/ko/reset-password?token=reset-token-e2e'); + await page.goto("/ko/reset-password?token=reset-token-e2e"); await policyLoaded; await page.waitForTimeout(900); - await fillResetPasswordForm(page, 'ValidPass1!A'); + await fillResetPasswordForm(page, "ValidPass1!A"); await submitResetPassword(page); await expect - .poll( - () => capture.resetBody?.newPassword as string | undefined, - { timeout: 10000 }, - ) - .toBe('ValidPass1!A'); + .poll(() => capture.resetBody?.newPassword as string | undefined, { + timeout: 10000, + }) + .toBe("ValidPass1!A"); await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/, { timeout: 10_000 }); - expect(capture.resetToken).toBe('reset-token-e2e'); - expect(capture.resetBody?.newPassword).toBe('ValidPass1!A'); + expect(capture.resetToken).toBe("reset-token-e2e"); + expect(capture.resetBody?.newPassword).toBe("ValidPass1!A"); }); }); diff --git a/userfront-e2e/tests/profile-department.spec.ts b/userfront-e2e/tests/profile-department.spec.ts index f3a7bda0..615171c6 100644 --- a/userfront-e2e/tests/profile-department.spec.ts +++ b/userfront-e2e/tests/profile-department.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page, type Route } from '@playwright/test'; +import { expect, type Page, type Route, test } from "@playwright/test"; type ProfileState = { department: string; @@ -7,7 +7,7 @@ type ProfileState = { }; async function enableFlutterAccessibility(page: Page): Promise { - const button = page.getByRole('button', { name: 'Enable accessibility' }); + const button = page.getByRole("button", { name: "Enable accessibility" }); if (await button.count()) { await button.click({ force: true }).catch(async () => { await page @@ -59,26 +59,31 @@ function isMobileProject(page: Page): boolean { async function seedTokenLogin(page: Page): Promise { await page.addInitScript(() => { - window.localStorage.setItem('baron_auth_token', 'e30.e30.e30'); - window.localStorage.setItem('baron_auth_provider', 'ory'); - window.localStorage.removeItem('baron_auth_cookie_mode'); - window.localStorage.removeItem('baron_auth_pending_provider'); + window.localStorage.setItem("baron_auth_token", "e30.e30.e30"); + window.localStorage.setItem("baron_auth_provider", "ory"); + window.localStorage.removeItem("baron_auth_cookie_mode"); + window.localStorage.removeItem("baron_auth_pending_provider"); }); } -async function fillAt(page: Page, x: number, y: number, value: string): Promise { - const pane = page.locator('flt-glass-pane'); +async function fillAt( + page: Page, + x: number, + y: number, + value: string, +): Promise { + const pane = page.locator("flt-glass-pane"); await pane.click({ position: { x, y }, force: true }); await page.waitForTimeout(100); await replaceFocusedText(page, value); } async function replaceFocusedText(page: Page, value: string): Promise { - await page.keyboard.press('End'); + await page.keyboard.press("End"); for (let index = 0; index < 64; index += 1) { - await page.keyboard.press('Backspace'); + await page.keyboard.press("Backspace"); } - if (value !== '') { + if (value !== "") { await page.keyboard.insertText(value); } await page.waitForTimeout(100); @@ -89,8 +94,12 @@ type BoxCenter = { y: number; }; -async function resolveLocatorCenter(locator: ReturnType): Promise { - const handle = await locator.elementHandle({ timeout: 1_000 }).catch(() => null); +async function resolveLocatorCenter( + locator: ReturnType, +): Promise { + const handle = await locator + .elementHandle({ timeout: 1_000 }) + .catch(() => null); if (!handle) { return null; } @@ -115,11 +124,14 @@ async function resolveLocatorCenter(locator: ReturnType): Promi }; } -async function clickGlassPaneAt(page: Page, center: BoxCenter | null): Promise { +async function clickGlassPaneAt( + page: Page, + center: BoxCenter | null, +): Promise { if (!center) { return false; } - await page.locator('flt-glass-pane').click({ + await page.locator("flt-glass-pane").click({ position: center, force: true, }); @@ -128,22 +140,25 @@ async function clickGlassPaneAt(page: Page, center: BoxCenter | null): Promise { - return (await page.getByRole('textbox', { name: '소속' }).count()) > 0; + return (await page.getByRole("textbox", { name: "소속" }).count()) > 0; } async function openDepartmentEditor(page: Page): Promise { const accessibleEditor = page - .getByRole('group', { name: '소속 QA' }) - .getByRole('button', { name: '편집' }); - const textbox = page.getByRole('textbox', { name: '소속' }); + .getByRole("group", { name: "소속 QA" }) + .getByRole("button", { name: "편집" }); + const textbox = page.getByRole("textbox", { name: "소속" }); if ((await accessibleEditor.count()) > 0) { const editorCenter = await resolveLocatorCenter(accessibleEditor); await accessibleEditor - .evaluate((element) => { - if (element instanceof HTMLElement) { - element.click(); - } - }, { timeout: 1_000 }) + .evaluate( + (element) => { + if (element instanceof HTMLElement) { + element.click(); + } + }, + { timeout: 1_000 }, + ) .catch(() => undefined); await page.waitForTimeout(200); if (await departmentTextboxIsOpen(page)) { @@ -153,14 +168,16 @@ async function openDepartmentEditor(page: Page): Promise { if (await departmentTextboxIsOpen(page)) { return; } - await accessibleEditor.click({ force: true, timeout: 1_000 }).catch(() => undefined); + await accessibleEditor + .click({ force: true, timeout: 1_000 }) + .catch(() => undefined); await page.waitForTimeout(200); if (await departmentTextboxIsOpen(page)) { return; } } if (isMobileProject(page)) { - throw new Error('Department editor accessibility button was not found.'); + throw new Error("Department editor accessibility button was not found."); } const coords = coordsFor(page); const viewport = page.viewportSize(); @@ -180,17 +197,17 @@ async function openDepartmentEditor(page: Page): Promise { } async function blurDepartmentEditor(page: Page): Promise { - const textbox = page.getByRole('textbox', { name: '소속' }); + const textbox = page.getByRole("textbox", { name: "소속" }); if ((await textbox.count()) > 0) { await textbox.blur(); await page.waitForTimeout(250); return; } if (isMobileProject(page)) { - throw new Error('Department textbox was not found.'); + throw new Error("Department textbox was not found."); } const coords = coordsFor(page); - await page.locator('flt-glass-pane').click({ + await page.locator("flt-glass-pane").click({ position: { x: coords.blurX, y: coords.blurY }, force: true, }); @@ -198,21 +215,21 @@ async function blurDepartmentEditor(page: Page): Promise { } async function submitDepartmentEditor(page: Page): Promise { - const textbox = page.getByRole('textbox', { name: '소속' }); + const textbox = page.getByRole("textbox", { name: "소속" }); if ((await textbox.count()) > 0) { - await textbox.press('Enter'); + await textbox.press("Enter"); await page.waitForTimeout(250); return; } if (isMobileProject(page)) { - throw new Error('Department textbox was not found.'); + throw new Error("Department textbox was not found."); } - await page.keyboard.press('Enter'); + await page.keyboard.press("Enter"); await page.waitForTimeout(250); } async function fillDepartmentField(page: Page, value: string): Promise { - const textbox = page.getByRole('textbox', { name: '소속' }); + const textbox = page.getByRole("textbox", { name: "소속" }); if (!isMobileProject(page)) { if ((await textbox.count()) > 0) { await textbox.click({ force: true }); @@ -230,92 +247,92 @@ async function fillDepartmentField(page: Page, value: string): Promise { return; } if (isMobileProject(page)) { - throw new Error('Department textbox was not found.'); + throw new Error("Department textbox was not found."); } const coords = coordsFor(page); await fillAt(page, coords.departmentInputX, coords.departmentInputY, value); } async function mockProfileApis(page: Page, state: ProfileState): Promise { - await page.route('**/api/v1/**', async (route: Route) => { + await page.route("**/api/v1/**", async (route: Route) => { const request = route.request(); const requestUrl = new URL(request.url()); const path = requestUrl.pathname; const method = request.method().toUpperCase(); - if (path.endsWith('/api/v1/user/me') && method === 'GET') { - const authHeader = request.headers()['authorization'] ?? ''; - if (!authHeader.startsWith('Bearer ')) { + if (path.endsWith("/api/v1/user/me") && method === "GET") { + const authHeader = request.headers()["authorization"] ?? ""; + if (!authHeader.startsWith("Bearer ")) { await route.fulfill({ status: 401, - contentType: 'application/json', - body: JSON.stringify({ error: 'unauthorized' }), + contentType: "application/json", + body: JSON.stringify({ error: "unauthorized" }), }); return; } state.getMeCount += 1; await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ - id: 'e2e-user', - email: 'e2e@example.com', - name: 'E2E User', - phone: '+821012341234', + id: "e2e-user", + email: "e2e@example.com", + name: "E2E User", + phone: "+821012341234", department: state.department, - affiliationType: 'employee', - companyCode: 'BARON', + affiliationType: "employee", + companyCode: "BARON", tenant: { - id: 'tenant-1', - name: 'Baron', - slug: 'baron', - description: 'E2E tenant', + id: "tenant-1", + name: "Baron", + slug: "baron", + description: "E2E tenant", }, }), }); return; } - if (path.endsWith('/api/v1/user/me') && method === 'PUT') { + if (path.endsWith("/api/v1/user/me") && method === "PUT") { const body = (request.postDataJSON() ?? {}) as Record; state.putBodies.push(body); - const nextDepartment = String(body.department ?? '').trim(); - if (nextDepartment !== '') { + const nextDepartment = String(body.department ?? "").trim(); + if (nextDepartment !== "") { state.department = nextDepartment; } await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ - status: 'success', - updatedAt: '2026-02-24T00:00:00Z', + status: "success", + updatedAt: "2026-02-24T00:00:00Z", }), }); return; } - if (path.endsWith('/api/v1/user/rp/linked')) { + if (path.endsWith("/api/v1/user/rp/linked")) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ items: [] }), }); return; } - if (path.endsWith('/api/v1/audit/auth/timeline')) { + if (path.endsWith("/api/v1/audit/auth/timeline")) { await route.fulfill({ status: 200, - contentType: 'application/json', - body: JSON.stringify({ items: [], next_cursor: '' }), + contentType: "application/json", + body: JSON.stringify({ items: [], next_cursor: "" }), }); return; } - if (path.endsWith('/api/v1/client-log')) { + if (path.endsWith("/api/v1/client-log")) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ ok: true }), }); return; @@ -323,14 +340,14 @@ async function mockProfileApis(page: Page, state: ProfileState): Promise { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ ok: true }), }); }); } async function openProfilePage(page: Page): Promise { - await page.goto('/ko/profile'); + await page.goto("/ko/profile"); await expect(page).toHaveURL(/\/ko\/profile$/); await enableFlutterAccessibility(page); await page.waitForTimeout(1200); @@ -340,22 +357,22 @@ async function waitForInitialProfileLoad(state: ProfileState): Promise { await expect.poll(() => state.getMeCount).toBeGreaterThan(0); } -test.describe('UserFront WASM profile department editing', () => { - test.skip(({ isMobile }) => isMobile, 'Desktop only (hardcoded coordinates)'); +test.describe("UserFront WASM profile department editing", () => { + test.skip(({ isMobile }) => isMobile, "Desktop only (hardcoded coordinates)"); test.skip( - ({ browserName }) => browserName === 'webkit', - 'WebKit headless does not consistently open Flutter profile edit controls; Chromium and Firefox cover this flow.', + ({ browserName }) => browserName === "webkit", + "WebKit headless does not consistently open Flutter profile edit controls; Chromium and Firefox cover this flow.", ); test.afterEach(async ({ page }) => { - await page.unroute('**/api/v1/**'); + await page.unroute("**/api/v1/**"); }); - test('소속 수정 후 명시 저장하면 저장 요청이 전송되고 새로고침 후 최신 값으로 재조회된다', async ({ + test("소속 수정 후 명시 저장하면 저장 요청이 전송되고 새로고침 후 최신 값으로 재조회된다", async ({ page, }) => { const state: ProfileState = { - department: 'QA', + department: "QA", getMeCount: 0, putBodies: [], }; @@ -365,24 +382,26 @@ test.describe('UserFront WASM profile department editing', () => { await waitForInitialProfileLoad(state); await openDepartmentEditor(page); - await fillDepartmentField(page, 'QA-Updated'); + await fillDepartmentField(page, "QA-Updated"); await submitDepartmentEditor(page); await expect.poll(() => state.putBodies.length).toBe(1); - expect(state.putBodies[0]?.department).toBe('QA-Updated'); - expect(state.department).toBe('QA-Updated'); + expect(state.putBodies[0]?.department).toBe("QA-Updated"); + expect(state.department).toBe("QA-Updated"); const getCountBeforeReload = state.getMeCount; await page.reload(); await expect(page).toHaveURL(/\/ko\/profile$/); - await expect.poll(() => state.getMeCount).toBeGreaterThan(getCountBeforeReload); + await expect + .poll(() => state.getMeCount) + .toBeGreaterThan(getCountBeforeReload); }); - test('소속 입력 후 즉시 새로고침해도 저장 요청이 중복 전송되지 않는다', async ({ + test("소속 입력 후 즉시 새로고침해도 저장 요청이 중복 전송되지 않는다", async ({ page, }) => { const state: ProfileState = { - department: 'QA', + department: "QA", getMeCount: 0, putBodies: [], }; @@ -392,24 +411,24 @@ test.describe('UserFront WASM profile department editing', () => { await waitForInitialProfileLoad(state); await openDepartmentEditor(page); - await fillDepartmentField(page, 'QA-Repro'); + await fillDepartmentField(page, "QA-Repro"); await page.reload(); await expect(page).toHaveURL(/\/ko\/profile$/); expect(state.putBodies.length).toBeLessThanOrEqual(1); if (state.putBodies.length > 0) { - expect(state.putBodies[0]?.department).toBe('QA-Repro'); - expect(state.department).toBe('QA-Repro'); + expect(state.putBodies[0]?.department).toBe("QA-Repro"); + expect(state.department).toBe("QA-Repro"); return; } - expect(state.department).toBe('QA'); + expect(state.department).toBe("QA"); }); - test('소속에 기존값을 그대로 입력하고 포커스 아웃하면 저장 요청이 전송되지 않는다', async ({ + test("소속에 기존값을 그대로 입력하고 포커스 아웃하면 저장 요청이 전송되지 않는다", async ({ page, }) => { const state: ProfileState = { - department: 'QA', + department: "QA", getMeCount: 0, putBodies: [], }; @@ -419,15 +438,17 @@ test.describe('UserFront WASM profile department editing', () => { await waitForInitialProfileLoad(state); await openDepartmentEditor(page); - await fillDepartmentField(page, 'QA'); + await fillDepartmentField(page, "QA"); await blurDepartmentEditor(page); expect(state.putBodies).toHaveLength(0); }); - test('소속을 빈 값으로 입력하면 저장 요청이 전송되지 않는다', async ({ page }) => { + test("소속을 빈 값으로 입력하면 저장 요청이 전송되지 않는다", async ({ + page, + }) => { const state: ProfileState = { - department: 'QA', + department: "QA", getMeCount: 0, putBodies: [], }; @@ -437,16 +458,18 @@ test.describe('UserFront WASM profile department editing', () => { await waitForInitialProfileLoad(state); await openDepartmentEditor(page); - await fillDepartmentField(page, ''); + await fillDepartmentField(page, ""); await blurDepartmentEditor(page); expect(state.putBodies).toHaveLength(0); - expect(state.department).toBe('QA'); + expect(state.department).toBe("QA"); }); - test('소속을 저장한 뒤 새로고침 후 다시 저장해도 저장 요청이 누락되지 않는다', async ({ page }) => { + test("소속을 저장한 뒤 새로고침 후 다시 저장해도 저장 요청이 누락되지 않는다", async ({ + page, + }) => { const state: ProfileState = { - department: 'QA', + department: "QA", getMeCount: 0, putBodies: [], }; @@ -456,7 +479,7 @@ test.describe('UserFront WASM profile department editing', () => { await waitForInitialProfileLoad(state); await openDepartmentEditor(page); - await fillDepartmentField(page, 'QA-1'); + await fillDepartmentField(page, "QA-1"); await submitDepartmentEditor(page); await expect.poll(() => state.putBodies.length).toBe(1); @@ -464,16 +487,18 @@ test.describe('UserFront WASM profile department editing', () => { await page.reload(); await expect(page).toHaveURL(/\/ko\/profile$/); await enableFlutterAccessibility(page); - await expect.poll(() => state.getMeCount).toBeGreaterThan(getCountBeforeReload); + await expect + .poll(() => state.getMeCount) + .toBeGreaterThan(getCountBeforeReload); await page.waitForTimeout(1200); await openDepartmentEditor(page); - await fillDepartmentField(page, 'QA-2'); + await fillDepartmentField(page, "QA-2"); await submitDepartmentEditor(page); await expect.poll(() => state.putBodies.length).toBe(2); - expect(state.putBodies[0]?.department).toBe('QA-1'); - expect(state.putBodies[1]?.department).toBe('QA-2'); - expect(state.department).toBe('QA-2'); + expect(state.putBodies[0]?.department).toBe("QA-1"); + expect(state.putBodies[1]?.department).toBe("QA-2"); + expect(state.department).toBe("QA-2"); }); }); diff --git a/userfront-e2e/tests/route-inventory.spec.ts b/userfront-e2e/tests/route-inventory.spec.ts index 0cb4b04f..3a2673e8 100644 --- a/userfront-e2e/tests/route-inventory.spec.ts +++ b/userfront-e2e/tests/route-inventory.spec.ts @@ -1,39 +1,39 @@ -import { expect, test, type Page, type Route } from '@playwright/test'; +import { expect, type Page, type Route, test } from "@playwright/test"; async function seedTokenLogin(page: Page): Promise { await page.addInitScript(() => { - window.localStorage.setItem('baron_auth_token', 'e30.e30.e30'); - window.localStorage.setItem('baron_auth_provider', 'ory'); - window.localStorage.removeItem('baron_auth_cookie_mode'); - window.localStorage.removeItem('baron_auth_pending_provider'); + window.localStorage.setItem("baron_auth_token", "e30.e30.e30"); + window.localStorage.setItem("baron_auth_provider", "ory"); + window.localStorage.removeItem("baron_auth_cookie_mode"); + window.localStorage.removeItem("baron_auth_pending_provider"); }); } async function mockInventoryApis(page: Page): Promise { - await page.route('**/api/v1/**', async (route: Route) => { + await page.route("**/api/v1/**", async (route: Route) => { const requestUrl = new URL(route.request().url()); const path = requestUrl.pathname; const method = route.request().method().toUpperCase(); - if (path.endsWith('/api/v1/user/me')) { - const authHeader = route.request().headers()['authorization'] ?? ''; - if (authHeader.startsWith('Bearer ')) { + if (path.endsWith("/api/v1/user/me")) { + const authHeader = route.request().headers()["authorization"] ?? ""; + if (authHeader.startsWith("Bearer ")) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ - id: 'e2e-user', - email: 'e2e@example.com', - name: 'E2E User', - phone: '+821012341234', - department: 'QA', - affiliationType: 'employee', - companyCode: 'BARON', + id: "e2e-user", + email: "e2e@example.com", + name: "E2E User", + phone: "+821012341234", + department: "QA", + affiliationType: "employee", + companyCode: "BARON", tenant: { - id: 'tenant-1', - name: 'Baron', - slug: 'baron', - description: 'E2E tenant', + id: "tenant-1", + name: "Baron", + slug: "baron", + description: "E2E tenant", }, }), }); @@ -42,34 +42,34 @@ async function mockInventoryApis(page: Page): Promise { await route.fulfill({ status: 401, - contentType: 'application/json', - body: JSON.stringify({ error: 'unauthorized' }), + contentType: "application/json", + body: JSON.stringify({ error: "unauthorized" }), }); return; } - if (path.endsWith('/api/v1/user/rp/linked')) { + if (path.endsWith("/api/v1/user/rp/linked")) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ items: [] }), }); return; } - if (path.endsWith('/api/v1/audit/auth/timeline')) { + if (path.endsWith("/api/v1/audit/auth/timeline")) { await route.fulfill({ status: 200, - contentType: 'application/json', - body: JSON.stringify({ items: [], next_cursor: '' }), + contentType: "application/json", + body: JSON.stringify({ items: [], next_cursor: "" }), }); return; } - if (path.endsWith('/api/v1/auth/password/policy')) { + if (path.endsWith("/api/v1/auth/password/policy")) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ minLength: 12, minCharacterTypes: 3, @@ -82,46 +82,46 @@ async function mockInventoryApis(page: Page): Promise { return; } - if (path.endsWith('/api/v1/auth/magic-link/verify')) { + if (path.endsWith("/api/v1/auth/magic-link/verify")) { await route.fulfill({ status: 200, - contentType: 'application/json', - body: JSON.stringify({ status: 'approved' }), + contentType: "application/json", + body: JSON.stringify({ status: "approved" }), }); return; } - if (path.endsWith('/api/v1/auth/login/code/verify')) { + if (path.endsWith("/api/v1/auth/login/code/verify")) { await route.fulfill({ status: 200, - contentType: 'application/json', - body: JSON.stringify({ status: 'approved' }), + contentType: "application/json", + body: JSON.stringify({ status: "approved" }), }); return; } - if (path.endsWith('/api/v1/auth/login/code/verify-short')) { + if (path.endsWith("/api/v1/auth/login/code/verify-short")) { await route.fulfill({ status: 200, - contentType: 'application/json', - body: JSON.stringify({ status: 'approved' }), + contentType: "application/json", + body: JSON.stringify({ status: "approved" }), }); return; } - if (path.endsWith('/api/v1/auth/consent') && method === 'GET') { + if (path.endsWith("/api/v1/auth/consent") && method === "GET") { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ client: { - client_name: 'E2E Client', - client_id: 'e2e-client', + client_name: "E2E Client", + client_id: "e2e-client", }, - requested_scope: ['openid'], + requested_scope: ["openid"], scope_details: { openid: { - description: 'OpenID', + description: "OpenID", mandatory: true, }, }, @@ -130,19 +130,19 @@ async function mockInventoryApis(page: Page): Promise { return; } - if (path.endsWith('/api/v1/auth/qr/approve')) { + if (path.endsWith("/api/v1/auth/qr/approve")) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ ok: true }), }); return; } - if (path.endsWith('/api/v1/client-log')) { + if (path.endsWith("/api/v1/client-log")) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ ok: true }), }); return; @@ -150,182 +150,186 @@ async function mockInventoryApis(page: Page): Promise { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({}), }); }); } -test.describe('UserFront WASM route inventory (unauth)', () => { +test.describe("UserFront WASM route inventory (unauth)", () => { test.beforeEach(async ({ page }) => { await mockInventoryApis(page); }); - test('route: /', async ({ page }) => { - await page.goto('/'); + test("route: /", async ({ page }) => { + await page.goto("/"); await expect(page).toHaveURL(/\/(ko|en)\/signin(?:\?.*)?$/); }); - test('route: /ko', async ({ page }) => { - await page.goto('/ko'); + test("route: /ko", async ({ page }) => { + await page.goto("/ko"); await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/); }); - test('route: /ko/dashboard', async ({ page }) => { - await page.goto('/ko/dashboard'); + test("route: /ko/dashboard", async ({ page }) => { + await page.goto("/ko/dashboard"); await expect(page).toHaveURL(/\/ko\/signin$/); }); - test('route: /ko/profile', async ({ page }) => { - await page.goto('/ko/profile'); + test("route: /ko/profile", async ({ page }) => { + await page.goto("/ko/profile"); await expect(page).toHaveURL(/\/ko\/signin$/); }); - test('route: /ko/admin/users', async ({ page }) => { - await page.goto('/ko/admin/users'); + test("route: /ko/admin/users", async ({ page }) => { + await page.goto("/ko/admin/users"); await expect(page).toHaveURL(/\/ko\/signin$/); }); - test('route: /ko/scan', async ({ page }) => { - await page.goto('/ko/scan'); + test("route: /ko/scan", async ({ page }) => { + await page.goto("/ko/scan"); await expect(page).toHaveURL(/\/ko\/signin$/); }); - test('route: /ko/signin', async ({ page }) => { - await page.goto('/ko/signin'); + test("route: /ko/signin", async ({ page }) => { + await page.goto("/ko/signin"); await expect(page).toHaveURL(/\/ko\/signin$/); }); - test('route: /ko/login', async ({ page }) => { - await page.goto('/ko/login'); + test("route: /ko/login", async ({ page }) => { + await page.goto("/ko/login"); await expect(page).toHaveURL(/\/ko\/login$/); }); - test('route: /ko/signup', async ({ page }) => { - await page.goto('/ko/signup'); + test("route: /ko/signup", async ({ page }) => { + await page.goto("/ko/signup"); await expect(page).toHaveURL(/\/ko\/signup$/); }); - test('route: /ko/registration', async ({ page }) => { - await page.goto('/ko/registration'); + test("route: /ko/registration", async ({ page }) => { + await page.goto("/ko/registration"); await expect(page).toHaveURL(/\/ko\/registration$/); }); - test('route: /ko/verify', async ({ page }) => { - await page.goto('/ko/verify'); + test("route: /ko/verify", async ({ page }) => { + await page.goto("/ko/verify"); await expect(page).toHaveURL(/\/ko\/verify$/); }); - test('route: /ko/verify/:token', async ({ page }) => { - await page.goto('/ko/verify/e2e-token'); + test("route: /ko/verify/:token", async ({ page }) => { + await page.goto("/ko/verify/e2e-token"); await expect(page).toHaveURL(/\/ko\/verify\/e2e-token$/); }); - test('route: /ko/verification', async ({ page }) => { - await page.goto('/ko/verification'); + test("route: /ko/verification", async ({ page }) => { + await page.goto("/ko/verification"); await expect(page).toHaveURL(/\/ko\/verification$/); }); - test('route: /ko/verify-complete', async ({ page }) => { - await page.goto('/ko/verify-complete'); + test("route: /ko/verify-complete", async ({ page }) => { + await page.goto("/ko/verify-complete"); await expect(page).toHaveURL(/\/ko\/verify-complete$/); }); - test('route: /ko/l/:shortCode', async ({ page }) => { - await page.goto('/ko/l/AB123456'); + test("route: /ko/l/:shortCode", async ({ page }) => { + await page.goto("/ko/l/AB123456"); await expect(page).toHaveURL(/\/ko\/l\/AB123456$/); }); - test('route: /ko/forgot-password', async ({ page }) => { - await page.goto('/ko/forgot-password'); + test("route: /ko/forgot-password", async ({ page }) => { + await page.goto("/ko/forgot-password"); await expect(page).toHaveURL(/\/ko\/forgot-password$/); }); - test('route: /ko/recovery', async ({ page }) => { - await page.goto('/ko/recovery'); + test("route: /ko/recovery", async ({ page }) => { + await page.goto("/ko/recovery"); await expect(page).toHaveURL(/\/ko\/recovery$/); }); - test('route: /ko/reset-password', async ({ page }) => { - await page.goto('/ko/reset-password?token=e2e-reset-token'); - await expect(page).toHaveURL(/\/ko\/reset-password\?token=e2e-reset-token$/); + test("route: /ko/reset-password", async ({ page }) => { + await page.goto("/ko/reset-password?token=e2e-reset-token"); + await expect(page).toHaveURL( + /\/ko\/reset-password\?token=e2e-reset-token$/, + ); }); - test('route: /ko/error', async ({ page }) => { - await page.goto('/ko/error?error=invalid_request'); + test("route: /ko/error", async ({ page }) => { + await page.goto("/ko/error?error=invalid_request"); await expect(page).toHaveURL(/\/ko\/error\?error=invalid_request$/); }); - test('route: /ko/settings', async ({ page }) => { - await page.goto('/ko/settings'); + test("route: /ko/settings", async ({ page }) => { + await page.goto("/ko/settings"); await expect(page).toHaveURL(/\/ko\/settings$/); }); - test('route: /ko/consent (missing challenge)', async ({ page }) => { - await page.goto('/ko/consent'); + test("route: /ko/consent (missing challenge)", async ({ page }) => { + await page.goto("/ko/consent"); await expect(page).toHaveURL(/\/ko\/consent$/); }); - test('route: /ko/consent?consent_challenge=...', async ({ page }) => { - await page.goto('/ko/consent?consent_challenge=e2e-consent'); - await expect(page).toHaveURL(/\/ko\/consent\?consent_challenge=e2e-consent$/); + test("route: /ko/consent?consent_challenge=...", async ({ page }) => { + await page.goto("/ko/consent?consent_challenge=e2e-consent"); + await expect(page).toHaveURL( + /\/ko\/consent\?consent_challenge=e2e-consent$/, + ); }); - test('route: /ko/approve?ref=...', async ({ page }) => { - await page.goto('/ko/approve?ref=e2e-ref'); + test("route: /ko/approve?ref=...", async ({ page }) => { + await page.goto("/ko/approve?ref=e2e-ref"); await expect(page).toHaveURL(/\/ko\/signin\?notice=qr_login_required$/); }); - test('route: /ko/ql/:ref', async ({ page }) => { - await page.goto('/ko/ql/e2e-ref'); + test("route: /ko/ql/:ref", async ({ page }) => { + await page.goto("/ko/ql/e2e-ref"); await expect(page).toHaveURL(/\/ko\/signin\?notice=qr_login_required$/); }); }); -test.describe('UserFront WASM route inventory (authed)', () => { +test.describe("UserFront WASM route inventory (authed)", () => { test.beforeEach(async ({ page }) => { await seedTokenLogin(page); await mockInventoryApis(page); }); - test('route: /ko -> /ko/dashboard', async ({ page }) => { - await page.goto('/ko'); + test("route: /ko -> /ko/dashboard", async ({ page }) => { + await page.goto("/ko"); await expect(page).toHaveURL(/\/ko\/dashboard$/); }); - test('route: /ko/dashboard', async ({ page }) => { - await page.goto('/ko/dashboard'); + test("route: /ko/dashboard", async ({ page }) => { + await page.goto("/ko/dashboard"); await expect(page).toHaveURL(/\/ko\/dashboard$/); }); - test('route: /ko/profile', async ({ page }) => { - await page.goto('/ko/profile'); + test("route: /ko/profile", async ({ page }) => { + await page.goto("/ko/profile"); await expect(page).toHaveURL(/\/ko\/profile$/); }); - test('route: /ko/admin/users', async ({ page }) => { - await page.goto('/ko/admin/users'); + test("route: /ko/admin/users", async ({ page }) => { + await page.goto("/ko/admin/users"); await expect(page).toHaveURL(/\/ko\/admin\/users$/); }); - test('route: /ko/scan', async ({ page }) => { - await page.goto('/ko/scan'); + test("route: /ko/scan", async ({ page }) => { + await page.goto("/ko/scan"); await expect(page).toHaveURL(/\/ko\/scan$/); }); - test('route: /ko/approve?ref=... -> /ko/dashboard', async ({ + test("route: /ko/approve?ref=... -> /ko/dashboard", async ({ page, }, testInfo) => { - await page.goto('/ko/approve?ref=e2e-ref'); + await page.goto("/ko/approve?ref=e2e-ref"); await expect(page).toHaveURL(/\/ko\/dashboard$/, { - timeout: testInfo.project.name === 'webkit-desktop' ? 15_000 : 5_000, + timeout: testInfo.project.name === "webkit-desktop" ? 15_000 : 5_000, }); }); - test('route: /ko/ql/:ref -> /ko/dashboard', async ({ page }, testInfo) => { - await page.goto('/ko/ql/e2e-ref'); + test("route: /ko/ql/:ref -> /ko/dashboard", async ({ page }, testInfo) => { + await page.goto("/ko/ql/e2e-ref"); await expect(page).toHaveURL(/\/ko\/dashboard$/, { - timeout: testInfo.project.name === 'webkit-desktop' ? 15_000 : 5_000, + timeout: testInfo.project.name === "webkit-desktop" ? 15_000 : 5_000, }); }); }); diff --git a/userfront-e2e/tests/runtime-env-mobile.spec.ts b/userfront-e2e/tests/runtime-env-mobile.spec.ts index 94d7090c..7b0bde5c 100644 --- a/userfront-e2e/tests/runtime-env-mobile.spec.ts +++ b/userfront-e2e/tests/runtime-env-mobile.spec.ts @@ -1,45 +1,45 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { inflateSync } from "node:zlib"; import { - expect, - test, type BrowserContext, + expect, type Page, type TestInfo, -} from '@playwright/test'; -import { readFileSync, writeFileSync } from 'node:fs'; -import { inflateSync } from 'node:zlib'; + test, +} from "@playwright/test"; const lightweightTestFont = readFileSync( - new URL('../fixtures/fonts/NotoSansKR-TestSubset.woff2', import.meta.url), + new URL("../fixtures/fonts/NotoSansKR-TestSubset.woff2", import.meta.url), ); type SigninCase = { - path: '/ko/signin' | '/en/signin'; - theme: 'light' | 'dark'; + path: "/ko/signin" | "/en/signin"; + theme: "light" | "dark"; }; const signinCases: SigninCase[] = [ - { path: '/ko/signin', theme: 'light' }, - { path: '/ko/signin', theme: 'dark' }, - { path: '/en/signin', theme: 'light' }, - { path: '/en/signin', theme: 'dark' }, + { path: "/ko/signin", theme: "light" }, + { path: "/ko/signin", theme: "dark" }, + { path: "/en/signin", theme: "light" }, + { path: "/en/signin", theme: "dark" }, ]; async function mockPublicApis(context: BrowserContext): Promise { await context.route(/\/api\/v1\//, async (route) => { const requestUrl = new URL(route.request().url()); - if (requestUrl.pathname.endsWith('/api/v1/user/me')) { + if (requestUrl.pathname.endsWith("/api/v1/user/me")) { await route.fulfill({ status: 401, - contentType: 'application/json', - body: JSON.stringify({ error: 'unauthorized' }), + contentType: "application/json", + body: JSON.stringify({ error: "unauthorized" }), }); return; } - if (requestUrl.pathname.endsWith('/api/v1/auth/tenant-info')) { + if (requestUrl.pathname.endsWith("/api/v1/auth/tenant-info")) { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({}), }); return; @@ -47,21 +47,23 @@ async function mockPublicApis(context: BrowserContext): Promise { await route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ ok: true }), }); }); } -async function routeLightweightTestFonts(context: BrowserContext): Promise { - await context.route('https://fonts.gstatic.com/**', async (route) => { +async function routeLightweightTestFonts( + context: BrowserContext, +): Promise { + await context.route("https://fonts.gstatic.com/**", async (route) => { await route.fulfill({ status: 200, - contentType: 'font/woff2', + contentType: "font/woff2", body: lightweightTestFont, headers: { - 'access-control-allow-origin': '*', - 'cache-control': 'public, max-age=31536000, immutable', + "access-control-allow-origin": "*", + "cache-control": "public, max-age=31536000, immutable", }, }); }); @@ -71,21 +73,26 @@ async function expectFlutterCanvasRendered( page: Page, timeoutMs = 10_000, ): Promise { - await expect(page.locator('#baron-bootstrap-shell')).toBeHidden({ + await expect(page.locator("#baron-bootstrap-shell")).toBeHidden({ timeout: timeoutMs, }); await expect - .poll(async () => { - const screenshot = await captureFlutterCanvasPng(page); - return screenshot === null ? false : screenshotHasSigninPaint(screenshot); - }, { - timeout: timeoutMs, - }) + .poll( + async () => { + const screenshot = await captureFlutterCanvasPng(page); + return screenshot === null + ? false + : screenshotHasSigninPaint(screenshot); + }, + { + timeout: timeoutMs, + }, + ) .toBe(true); } async function expectBootstrapShellVisible(page: Page): Promise { - const shell = page.locator('#baron-bootstrap-shell'); + const shell = page.locator("#baron-bootstrap-shell"); await expect(shell).toBeVisible({ timeout: 1_000 }); await expect(shell).toContainText(/Baron SW Portal/); } @@ -96,9 +103,9 @@ async function expectSigninSurfaceWithinBudget( entry: SigninCase, ): Promise { await seedAuthState(page, entry); - await page.goto(entry.path, { waitUntil: 'domcontentloaded' }); + await page.goto(entry.path, { waitUntil: "domcontentloaded" }); - const slug = `${entry.path.slice(1).replace('/', '-')}-${entry.theme}`; + const slug = `${entry.path.slice(1).replace("/", "-")}-${entry.theme}`; let paintedAtMs: number | null = null; let previousElapsedMs = 0; for (const elapsedMs of [500, 1000]) { @@ -106,7 +113,9 @@ async function expectSigninSurfaceWithinBudget( previousElapsedMs = elapsedMs; const screenshot = await captureFlutterCanvasPng( page, - testInfo.outputPath(`${testInfo.project.name}-${slug}-${elapsedMs}ms.png`), + testInfo.outputPath( + `${testInfo.project.name}-${slug}-${elapsedMs}ms.png`, + ), ); if ( paintedAtMs === null && @@ -129,7 +138,7 @@ async function captureFlutterCanvasPng( path?: string, ): Promise { const dataUrl = await page.evaluate(() => { - const canvas = Array.from(document.querySelectorAll('canvas')) + const canvas = Array.from(document.querySelectorAll("canvas")) .filter((candidate) => candidate.width > 0 && candidate.height > 0) .sort((left, right) => { return right.width * right.height - left.width * left.height; @@ -138,16 +147,16 @@ async function captureFlutterCanvasPng( return null; } try { - return canvas.toDataURL('image/png'); + return canvas.toDataURL("image/png"); } catch { return null; } }); - if (dataUrl?.startsWith('data:image/png;base64,')) { + if (dataUrl?.startsWith("data:image/png;base64,")) { const screenshot = Buffer.from( - dataUrl.slice('data:image/png;base64,'.length), - 'base64', + dataUrl.slice("data:image/png;base64,".length), + "base64", ); if (path) { writeFileSync(path, screenshot); @@ -197,7 +206,9 @@ function screenshotHasSigninPaint(buffer: Buffer): boolean { } } - return sampled > 0 && nonWhite / sampled > 0.02 && dark > 12 && buttonBlue > 12; + return ( + sampled > 0 && nonWhite / sampled > 0.02 && dark > 12 && buttonBlue > 12 + ); } function decodePng(buffer: Buffer): { @@ -205,9 +216,9 @@ function decodePng(buffer: Buffer): { height: number; pixels: Uint8Array; } { - const signature = buffer.subarray(0, 8).toString('hex'); - if (signature !== '89504e470d0a1a0a') { - throw new Error('invalid png signature'); + const signature = buffer.subarray(0, 8).toString("hex"); + if (signature !== "89504e470d0a1a0a") { + throw new Error("invalid png signature"); } let offset = 8; @@ -218,23 +229,25 @@ function decodePng(buffer: Buffer): { while (offset < buffer.length) { const length = buffer.readUInt32BE(offset); - const type = buffer.subarray(offset + 4, offset + 8).toString('ascii'); + const type = buffer.subarray(offset + 4, offset + 8).toString("ascii"); const data = buffer.subarray(offset + 8, offset + 8 + length); offset += 12 + length; - if (type === 'IHDR') { + if (type === "IHDR") { width = data.readUInt32BE(0); height = data.readUInt32BE(4); colorType = data[9]; - } else if (type === 'IDAT') { + } else if (type === "IDAT") { idat.push(data); - } else if (type === 'IEND') { + } else if (type === "IEND") { break; } } if (!width || !height || ![2, 6].includes(colorType)) { - throw new Error(`unsupported png format: ${width}x${height}, color=${colorType}`); + throw new Error( + `unsupported png format: ${width}x${height}, color=${colorType}`, + ); } const bytesPerPixel = colorType === 6 ? 4 : 3; @@ -249,7 +262,8 @@ function decodePng(buffer: Buffer): { sourceOffset += 1; for (let x = 0; x < stride; x += 1) { const value = inflated[sourceOffset + x]; - const left = x >= bytesPerPixel ? raw[targetOffset + x - bytesPerPixel] : 0; + const left = + x >= bytesPerPixel ? raw[targetOffset + x - bytesPerPixel] : 0; const up = y > 0 ? raw[targetOffset + x - stride] : 0; const upLeft = y > 0 && x >= bytesPerPixel @@ -313,27 +327,30 @@ function paeth(left: number, up: number, upLeft: number): number { async function seedAuthState(page: Page, entry: SigninCase): Promise { const localeCode = entry.path.slice(1, 3); - await page.addInitScript(({ themeValue, localeValue }) => { - window.localStorage.setItem('userfront_auth_theme', themeValue); - window.localStorage.setItem('flutter.userfront_auth_theme', themeValue); - window.localStorage.setItem('locale', localeValue); - window.localStorage.setItem('flutter.locale', localeValue); - }, { themeValue: entry.theme, localeValue: localeCode }); + await page.addInitScript( + ({ themeValue, localeValue }) => { + window.localStorage.setItem("userfront_auth_theme", themeValue); + window.localStorage.setItem("flutter.userfront_auth_theme", themeValue); + window.localStorage.setItem("locale", localeValue); + window.localStorage.setItem("flutter.locale", localeValue); + }, + { themeValue: entry.theme, localeValue: localeCode }, + ); } -test.describe('UserFront signin runtime matrix', () => { +test.describe("UserFront signin runtime matrix", () => { test.beforeEach(async ({ context }) => { await mockPublicApis(context); await routeLightweightTestFonts(context); }); - test('first paint exposes bootstrap shell before Flutter renders', async ({ + test("first paint exposes bootstrap shell before Flutter renders", async ({ page, }, testInfo) => { - await page.goto('/ko/signin', { waitUntil: 'domcontentloaded' }); + await page.goto("/ko/signin", { waitUntil: "domcontentloaded" }); await expectBootstrapShellVisible(page); await page.screenshot({ - path: testInfo.outputPath('mobile-first-paint-ko.png'), + path: testInfo.outputPath("mobile-first-paint-ko.png"), fullPage: true, }); }); @@ -351,26 +368,27 @@ test.describe('UserFront signin runtime matrix', () => { page, }, testInfo) => { test.skip( - testInfo.project.name === 'webkit-desktop' && entry.path === '/en/signin', - 'WebKit headless keeps /en/signin canvas blank after load; Chromium covers English rendering.', + testInfo.project.name === "webkit-desktop" && + entry.path === "/en/signin", + "WebKit headless keeps /en/signin canvas blank after load; Chromium covers English rendering.", ); await seedAuthState(page, entry); - await page.goto(entry.path, { waitUntil: 'domcontentloaded' }); + await page.goto(entry.path, { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(new RegExp(`${entry.path}(?:\\?.*)?$`)); await expectFlutterCanvasRendered(page); }); } - test('signin uses configured BACKEND_URL for public API requests', async ({ + test("signin uses configured BACKEND_URL for public API requests", async ({ page, }) => { const expectedBackendOrigin = process.env.EXPECTED_BACKEND_ORIGIN; - test.skip(!expectedBackendOrigin, 'set EXPECTED_BACKEND_ORIGIN'); + test.skip(!expectedBackendOrigin, "set EXPECTED_BACKEND_ORIGIN"); const requestedApiOrigins = new Set(); - page.on('request', (request) => { + page.on("request", (request) => { const requestUrl = new URL(request.url()); - if (requestUrl.pathname.startsWith('/api/v1/')) { + if (requestUrl.pathname.startsWith("/api/v1/")) { requestedApiOrigins.add(requestUrl.origin); } }); @@ -382,35 +400,37 @@ test.describe('UserFront signin runtime matrix', () => { await expect .poll(() => [...requestedApiOrigins], { timeout: 30_000 }) .toContain(expectedBackendOrigin); - expect(requestedApiOrigins).not.toContain('https://sso.example.test'); + expect(requestedApiOrigins).not.toContain("https://sso.example.test"); } }); - test('Korean signin renders with test-only lightweight web font', async ({ + test("Korean signin renders with test-only lightweight web font", async ({ context, page, }, testInfo) => { - if (testInfo.project.name === 'webkit-desktop') { + if (testInfo.project.name === "webkit-desktop") { await routeLightweightTestFonts(context); } const requestedUrls: string[] = []; - page.on('request', (request) => { + page.on("request", (request) => { requestedUrls.push(request.url()); }); - await seedAuthState(page, { path: '/ko/signin', theme: 'light' }); - await page.goto('/ko/signin', { waitUntil: 'domcontentloaded' }); + await seedAuthState(page, { path: "/ko/signin", theme: "light" }); + await page.goto("/ko/signin", { waitUntil: "domcontentloaded" }); await expectFlutterCanvasRendered(page, 10_000); await page.screenshot({ - path: testInfo.outputPath(`${testInfo.project.name}-ko-signin-korean-font.png`), + path: testInfo.outputPath( + `${testInfo.project.name}-ko-signin-korean-font.png`, + ), fullPage: true, }); expect(requestedUrls).toContainEqual( - expect.stringContaining('https://fonts.gstatic.com/'), + expect.stringContaining("https://fonts.gstatic.com/"), ); expect(requestedUrls).not.toContainEqual( - expect.stringContaining('/assets/assets/fonts/NotoSansKR-Regular.ttf'), + expect.stringContaining("/assets/assets/fonts/NotoSansKR-Regular.ttf"), ); }); }); diff --git a/userfront-e2e/tests/session-cross-browser-debug.spec.ts b/userfront-e2e/tests/session-cross-browser-debug.spec.ts index 9c07e4d1..8ff74fbe 100644 --- a/userfront-e2e/tests/session-cross-browser-debug.spec.ts +++ b/userfront-e2e/tests/session-cross-browser-debug.spec.ts @@ -1,9 +1,10 @@ -import { expect, test, type BrowserContext, type Page } from '@playwright/test'; +import { type BrowserContext, expect, type Page, test } from "@playwright/test"; -const USERFRONT_BASE_URL = process.env.USERFRONT_BASE_URL ?? 'https://sso.example.test'; -const ADMINFRONT_URL = process.env.ADMINFRONT_URL ?? 'http://localhost:5173'; -const LOGIN_ID = process.env.E2E_LOGIN_ID ?? ''; -const PASSWORD = process.env.E2E_PASSWORD ?? ''; +const USERFRONT_BASE_URL = + process.env.USERFRONT_BASE_URL ?? "https://sso.example.test"; +const ADMINFRONT_URL = process.env.ADMINFRONT_URL ?? "http://localhost:5173"; +const LOGIN_ID = process.env.E2E_LOGIN_ID ?? ""; +const PASSWORD = process.env.E2E_PASSWORD ?? ""; type SessionApiResponse = { items?: Array<{ @@ -18,20 +19,20 @@ type SessionApiResponse = { function ensureCredentials(): void { if (!LOGIN_ID || !PASSWORD) { - test.skip(true, 'E2E credentials are required'); + test.skip(true, "E2E credentials are required"); } } async function enableFlutterAccessibility(page: Page): Promise { await page.waitForTimeout(300); - const button = page.getByRole('button', { name: 'Enable accessibility' }); + const button = page.getByRole("button", { name: "Enable accessibility" }); if (await button.count()) { try { await button.click({ force: true }); } catch { return; } - const placeholder = page.locator('flt-semantics-placeholder'); + const placeholder = page.locator("flt-semantics-placeholder"); if (await placeholder.count()) { await placeholder.first().click({ force: true }); } @@ -41,7 +42,7 @@ async function enableFlutterAccessibility(page: Page): Promise { async function clickPasswordTab(page: Page): Promise { await page.waitForTimeout(900); - const pane = page.locator('flt-glass-pane'); + const pane = page.locator("flt-glass-pane"); await pane.click({ position: { x: 522, y: 158 }, force: true, @@ -54,20 +55,27 @@ async function clickPasswordTab(page: Page): Promise { await page.waitForTimeout(200); } -async function fillAt(page: Page, x: number, y: number, value: string): Promise { - const pane = page.locator('flt-glass-pane'); +async function fillAt( + page: Page, + x: number, + y: number, + value: string, +): Promise { + const pane = page.locator("flt-glass-pane"); await pane.click({ position: { x, y }, force: true }); await page.waitForTimeout(100); - await page.keyboard.press('Control+A'); - await page.keyboard.press('Backspace'); + await page.keyboard.press("Control+A"); + await page.keyboard.press("Backspace"); await page.keyboard.type(value); } async function loginViaUserFront(page: Page): Promise { await page.waitForURL(/\/ko\/(signin|login)/, { timeout: 30_000 }); - const loginIdInput = page.getByPlaceholder(/이메일 또는 휴대폰 번호|email|phone/i); + const loginIdInput = page.getByPlaceholder( + /이메일 또는 휴대폰 번호|email|phone/i, + ); const passwordInput = page.getByPlaceholder(/비밀번호|password/i); - const submitButton = page.getByRole('button', { name: /로그인|Login/i }); + const submitButton = page.getByRole("button", { name: /로그인|Login/i }); if ((await loginIdInput.count()) >= 1 && (await passwordInput.count()) >= 1) { await loginIdInput.first().fill(LOGIN_ID); @@ -79,7 +87,7 @@ async function loginViaUserFront(page: Page): Promise { await clickPasswordTab(page); await fillAt(page, 640, 245, LOGIN_ID); await fillAt(page, 640, 311, PASSWORD); - await page.locator('flt-glass-pane').click({ + await page.locator("flt-glass-pane").click({ position: { x: 640, y: 381 }, force: true, }); @@ -91,7 +99,7 @@ async function ensureConsentIfNeeded(page: Page): Promise { } const allowButton = page - .getByRole('button') + .getByRole("button") .filter({ hasText: /허용|동의|Accept|Allow/i }) .first(); @@ -100,15 +108,17 @@ async function ensureConsentIfNeeded(page: Page): Promise { } } -async function captureUserSessionsOnReload(page: Page): Promise { +async function captureUserSessionsOnReload( + page: Page, +): Promise { const responsePromise = page.waitForResponse( (response) => - response.request().method() === 'GET' && - response.url().includes('/api/v1/user/sessions'), + response.request().method() === "GET" && + response.url().includes("/api/v1/user/sessions"), { timeout: 30_000 }, ); - await page.reload({ waitUntil: 'domcontentloaded' }); + await page.reload({ waitUntil: "domcontentloaded" }); const response = await responsePromise; return (await response.json()) as SessionApiResponse; } @@ -116,7 +126,7 @@ async function captureUserSessionsOnReload(page: Page): Promise { const page = await context.newPage(); await page.goto(`${USERFRONT_BASE_URL}/ko/signin`, { - waitUntil: 'domcontentloaded', + waitUntil: "domcontentloaded", }); await loginViaUserFront(page); await expect(page).toHaveURL(/\/ko\/dashboard/, { timeout: 60_000 }); @@ -125,8 +135,10 @@ async function loginUserFront(context: BrowserContext): Promise { async function loginAdminFront(context: BrowserContext): Promise { const page = await context.newPage(); - await page.goto(ADMINFRONT_URL, { waitUntil: 'domcontentloaded' }); - const ssoButton = page.getByRole('button', { name: /SSO 계정으로 로그인|SSO/i }); + await page.goto(ADMINFRONT_URL, { waitUntil: "domcontentloaded" }); + const ssoButton = page.getByRole("button", { + name: /SSO 계정으로 로그인|SSO/i, + }); if (await ssoButton.count()) { await ssoButton.click({ force: true }); await page.waitForTimeout(1500); @@ -136,35 +148,38 @@ async function loginAdminFront(context: BrowserContext): Promise { const origin = window.location.origin; const authority = `${USERFRONT_BASE_URL}/oidc`; const params = new URLSearchParams({ - client_id: 'adminfront', + client_id: "adminfront", redirect_uri: `${origin}/auth/callback`, - response_type: 'code', - scope: 'openid offline_access profile email', + response_type: "code", + scope: "openid offline_access profile email", state: `pw-${Date.now()}`, nonce: `pw-${Date.now()}`, - code_challenge: 'test-code-challenge-test-code-challenge-test', - code_challenge_method: 'plain', + code_challenge: "test-code-challenge-test-code-challenge-test", + code_challenge_method: "plain", }); return `${authority}/oauth2/auth?${params.toString()}`; }); - await page.goto(authorizeUrl, { waitUntil: 'domcontentloaded' }); + await page.goto(authorizeUrl, { waitUntil: "domcontentloaded" }); } await loginViaUserFront(page); await ensureConsentIfNeeded(page); - await page.waitForURL(/localhost:5173|\/auth\/callback|\/dashboard|\/tenants/, { - timeout: 60_000, - }); + await page.waitForURL( + /localhost:5173|\/auth\/callback|\/dashboard|\/tenants/, + { + timeout: 60_000, + }, + ); return page; } -test.describe('cross-browser session debug', () => { - test('userfront session card should map adminfront session metadata across contexts', async ({ +test.describe("cross-browser session debug", () => { + test("userfront session card should map adminfront session metadata across contexts", async ({ browser, }, testInfo) => { ensureCredentials(); - const userfrontContext = await browser.newContext({ locale: 'ko-KR' }); - const adminfrontContext = await browser.newContext({ locale: 'ko-KR' }); + const userfrontContext = await browser.newContext({ locale: "ko-KR" }); + const adminfrontContext = await browser.newContext({ locale: "ko-KR" }); const userfrontPage = await loginUserFront(userfrontContext); const adminfrontPage = await loginAdminFront(adminfrontContext); @@ -172,16 +187,20 @@ test.describe('cross-browser session debug', () => { const sessionsPayload = await captureUserSessionsOnReload(userfrontPage); const items = sessionsPayload.items ?? []; const adminfrontItems = items.filter((item) => - (item.client_id ?? '').toLowerCase().includes('adminfront'), + (item.client_id ?? "").toLowerCase().includes("adminfront"), ); - const unknownCards = await userfrontPage.locator('text=세션 정보').allTextContents(); - const adminFrontCards = await userfrontPage.locator('text=AdminFront').allTextContents(); + const unknownCards = await userfrontPage + .locator("text=세션 정보") + .allTextContents(); + const adminFrontCards = await userfrontPage + .locator("text=AdminFront") + .allTextContents(); - await testInfo.attach('user-sessions.json', { + await testInfo.attach("user-sessions.json", { body: JSON.stringify(sessionsPayload, null, 2), - contentType: 'application/json', + contentType: "application/json", }); - await testInfo.attach('card-summary.json', { + await testInfo.attach("card-summary.json", { body: JSON.stringify( { unknownCards, @@ -192,7 +211,7 @@ test.describe('cross-browser session debug', () => { null, 2, ), - contentType: 'application/json', + contentType: "application/json", }); expect(adminfrontItems.length).toBeGreaterThan(0);