diff --git a/.gitignore b/.gitignore
index 3c2843f..c321178 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,4 +79,5 @@ temp/
# E2E 產物(make e2e-full / e2e-up 生成)
test/e2e/fixtures/state.json
test/e2e/fixtures/gateway.pid
+.dev/
.cache/
diff --git a/Makefile b/Makefile
index 5b6866c..649dd40 100644
--- a/Makefile
+++ b/Makefile
@@ -214,4 +214,101 @@ k6-seed-admin: k6-build ## 註冊 k6-admin 並 seed tenant_admin role(rbac jou
k6-down: ## 停 k6 stack 並清 volume(會清 Postgres / Mongo / Redis 資料)
$(COMPOSE) --profile k6 down -v
@rm -f $(K6_PAT_FILE) $(K6_ENV_FILE) $(K6_ENV_FILE).admin $(K6_ENV_FILE).tmp deploy/zitadel/machinekey/zitadel-admin-sa.json
- @echo "k6 stack stopped, volumes & PAT removed"
\ No newline at end of file
+ @echo "k6 stack stopped, volumes & PAT removed"
+
+# ============================================================
+# 一鍵本機測試環境(Docker + Gateway + 種子資料)
+# ============================================================
+
+DEV_DIR := .dev
+DEV_GATEWAY_PID := $(DEV_DIR)/gateway.pid
+DEV_GATEWAY_LOG := $(DEV_DIR)/gateway.log
+
+.PHONY: dev-up dev-down dev-status dev-restart-gateway
+
+dev-up: k6-up k6-wait k6-build ## 一鍵起全套:mongo/redis/mailhog/zitadel + seed + Gateway 背景
+ @mkdir -p $(DEV_DIR)
+ @if [ -f $(DEV_GATEWAY_PID) ] && kill -0 $$(cat $(DEV_GATEWAY_PID)) 2>/dev/null; then \
+ echo "gateway already running (pid $$(cat $(DEV_GATEWAY_PID)))"; \
+ else \
+ set -a; . $(K6_ENV_FILE); set +a; \
+ nohup $(K6_GATEWAY_BIN) -f $(K6_GATEWAY_CONFIG) > $(DEV_GATEWAY_LOG) 2>&1 & \
+ echo $$! > $(DEV_GATEWAY_PID); \
+ echo "gateway starting (pid $$(cat $(DEV_GATEWAY_PID)), log $(DEV_GATEWAY_LOG))…"; \
+ for i in $$(seq 1 30); do \
+ if curl -fsS http://localhost:8888/api/v1/health >/dev/null 2>&1; then \
+ echo "gateway ready ($$i s)"; break; \
+ fi; \
+ sleep 1; \
+ if [ $$i -eq 30 ]; then \
+ echo "gateway did not become ready — tail $(DEV_GATEWAY_LOG)"; \
+ tail -20 $(DEV_GATEWAY_LOG); exit 1; \
+ fi; \
+ done; \
+ fi
+ @echo ""
+ @echo "=========================================="
+ @echo " 本機測試環境已就緒"
+ @echo "=========================================="
+ @echo " Gateway API http://localhost:8888"
+ @echo " 前端(另開終端) make frontend-dev → http://localhost:5173"
+ @echo " MailHog 收信 http://localhost:8025 (註冊 OTP 在這裡看)"
+ @echo " ZITADEL 主控台 http://localhost:8080/ui/console"
+ @echo ""
+ @echo " 註冊預設:租戶 k6-tenant · 邀請碼 K6INVITE"
+ @echo " Email 可填任意地址(例:you@test.com),OTP 不會進真實信箱"
+ @echo ""
+ @echo " 管理後台:make dev-seed-admin 後用輸出的帳密登入"
+ @echo " 關閉環境:make dev-down"
+ @echo " 查看狀態:make dev-status"
+ @echo "=========================================="
+
+dev-seed-admin: k6-seed-admin ## 建立具 tenant_admin 的管理員(dev-up 之後執行)
+
+dev-down: ## 停 Gateway 背景行程 + k6 docker stack
+ @if [ -f $(DEV_GATEWAY_PID) ]; then \
+ pid=$$(cat $(DEV_GATEWAY_PID)); \
+ if kill -0 $$pid 2>/dev/null; then kill $$pid && echo "stopped gateway (pid $$pid)"; fi; \
+ rm -f $(DEV_GATEWAY_PID); \
+ fi
+ @$(MAKE) -s k6-down
+ @rm -rf $(DEV_DIR)
+ @echo "dev environment stopped"
+
+dev-status: ## 顯示 docker / gateway / health 狀態
+ @echo "=== docker (k6 profile) ==="
+ @$(COMPOSE) --profile k6 ps 2>/dev/null || true
+ @echo ""
+ @echo "=== gateway :8888 ==="
+ @if [ -f $(DEV_GATEWAY_PID) ] && kill -0 $$(cat $(DEV_GATEWAY_PID)) 2>/dev/null; then \
+ echo "running pid $$(cat $(DEV_GATEWAY_PID))"; \
+ curl -fsS http://localhost:8888/api/v1/health 2>/dev/null | head -c 200 || echo "(health check failed)"; \
+ echo ""; \
+ else \
+ echo "not running (run: make dev-up)"; \
+ fi
+
+dev-restart-gateway: k6-build ## 只重啟 Gateway(docker 不動)
+ @if [ -f $(DEV_GATEWAY_PID) ]; then \
+ pid=$$(cat $(DEV_GATEWAY_PID)); \
+ kill -0 $$pid 2>/dev/null && kill $$pid || true; \
+ rm -f $(DEV_GATEWAY_PID); \
+ fi
+ @mkdir -p $(DEV_DIR)
+ @set -a; . $(K6_ENV_FILE); set +a; \
+ nohup $(K6_GATEWAY_BIN) -f $(K6_GATEWAY_CONFIG) > $(DEV_GATEWAY_LOG) 2>&1 & \
+ echo $$! > $(DEV_GATEWAY_PID); \
+ echo "gateway restarted (pid $$(cat $(DEV_GATEWAY_PID)))"
+
+# ============================================================
+# Frontend(使用者前台 + 管理後台)
+# ============================================================
+
+frontend-install: ## 安裝 frontend 依賴
+ cd frontend && npm install
+
+frontend-dev: frontend-install ## 啟動前端 dev server(:5173,proxy /api → :8888)
+ cd frontend && npm run dev
+
+frontend-build: frontend-install ## 建置前端靜態檔 → frontend/dist/
+ cd frontend && npm run build
\ No newline at end of file
diff --git a/README.md b/README.md
index feea142..9786c6b 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Portal API Gateway (PGW)
+我# Portal API Gateway (PGW)
基於 [go-zero](https://github.com/zeromicro/go-zero) 的 API Gateway,提供統一 HTTP JSON 回應、8 碼業務錯誤碼,以及由 `.api` 定義驅動的程式碼與 OpenAPI 3.0 文件生成。
@@ -96,11 +96,15 @@ curl -s http://127.0.0.1:8888/api/v1/health | jq
| `make run-dev` | 啟動 Gateway(`etc/gateway.dev.yaml`,需 Docker) |
| `make config-check` | 驗證 yaml 可載入 |
| `make mongo-index` | 建立 notification Mongo 索引 |
+| `make dev-up` | **一鍵**本機全套(Docker + ZITADEL + MailHog + Gateway) |
+| `make frontend-dev` | 啟動 React 前台 + 管理後台(`frontend/`,:5173) |
+| `make dev-down` | 關閉 `dev-up` 啟動的一切 |
## 專案結構
```
gateway/
+├── frontend/ # Vite + React 使用者前台與管理後台
├── gateway.go # 程式入口
├── etc/gateway.yaml # 服務設定(埠號等)
├── generate/
diff --git a/deploy/zitadel/README.md b/deploy/zitadel/README.md
index 32a78a8..67b4e39 100644
--- a/deploy/zitadel/README.md
+++ b/deploy/zitadel/README.md
@@ -32,6 +32,18 @@ export ZITADEL_SERVICE_TOKEN=$(cat deploy/zitadel/machinekey/zitadel-admin-sa.to
`make k6-gateway` 會自動做這件事。
+## 密碼登入(`/auth/login`)
+
+ZITADEL v2 **預設停用** OAuth Resource Owner Password Grant(`unsupported_grant_type`)。
+本 repo 的 Gateway 在**未設定** `OAuthClientID` / `OAuthClientSecret` 時,會改用 **v2 Sessions API**(PAT)驗證密碼,無需額外建立 OIDC App。
+
+若要在正式環境使用 ROPG,請自行建立 OIDC Application 並設定:
+
+```bash
+export ZITADEL_OAUTH_CLIENT_ID=...
+export ZITADEL_OAUTH_CLIENT_SECRET=...
+```
+
## 重設
```bash
diff --git a/deploy/zitadel/machinekey/k6.env b/deploy/zitadel/machinekey/k6.env
index 1561991..6755819 100644
--- a/deploy/zitadel/machinekey/k6.env
+++ b/deploy/zitadel/machinekey/k6.env
@@ -1,10 +1,4 @@
-export ZITADEL_SERVICE_TOKEN=lvYZ4FNjNap4H2Lx7h9s02gr1tB4VxdWFk2yl4Fj3T3lSHhSn1Mv4lS6dGygiuP2cQ6j1D4
+export ZITADEL_SERVICE_TOKEN=4ozUZSXQZ2iaObRwejZT95BtuEPI4j_UZoCqUOM4B26KhYAW4k3uSVokd-sat49HrDMSkxM
export BASE_URL=http://localhost:8888
export MAILHOG_URL=http://localhost:8025
export REDIS_ADDR=localhost:6379
-export ADMIN_EMAIL=k6-admin-1779775084@k6.local
-export ADMIN_PASSWORD=K6-Admin-Pass-1!
-export ADMIN_UID=K6-10000075
-export ADMIN_TENANT_ID=k6-tenant
-export ADMIN_ACCESS_TOKEN=eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJ0ZW5hbnRfaWQiOiJrNi10ZW5hbnQiLCJ1aWQiOiJLNi0xMDAwMDA3NSIsInR5cCI6ImFjY2VzcyIsImF1dGhfZ2VuIjowLCJleHAiOjE3Nzk3NzU5ODUsImlhdCI6MTc3OTc3NTA4NSwianRpIjoiMzJlYjYyMGMtNmU5Ny00YWM5LTgzYTItMGQyMjRhODcwOWVjIn0.TQiRHCk-QVKShBNIR4F9TGQrSCc9YatmxCgE2oxnV6I
-export ADMIN_REFRESH_TOKEN=eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJ0ZW5hbnRfaWQiOiJrNi10ZW5hbnQiLCJ1aWQiOiJLNi0xMDAwMDA3NSIsInR5cCI6InJlZnJlc2giLCJhdXRoX2dlbiI6MCwiZXhwIjoxNzgwMzc5ODg1LCJpYXQiOjE3Nzk3NzUwODUsImp0aSI6IjA5ODczZTQyLTg2NGUtNDQ4ZS04OTBiLTBlMjhlZTZkOTA2YyJ9.jIIZykM2L9kmNeUzSU-fHtUsIUS6eyvGb06eT7DWPUk
diff --git a/deploy/zitadel/machinekey/k6.env.admin b/deploy/zitadel/machinekey/k6.env.admin
deleted file mode 100644
index e69de29..0000000
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..a0d3e21
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,73 @@
+# Gateway Frontend
+
+一般使用者前台 + 簡易管理後台,對應本 repo 的 Gateway API。
+
+## 技術
+
+- Vite + React + TypeScript + React Router
+- 無 UI 框架,表單與頁面導覽即可日常使用
+
+## 啟動(推薦一鍵後端)
+
+```bash
+# 終端 1:後端全套(首次約 1–2 分鐘,等 ZITADEL 起來)
+make dev-up
+
+# 終端 2:前端
+make frontend-dev
+```
+
+開啟 http://localhost:5173 · 註冊 OTP 到 http://localhost:8025(MailHog)
+
+關閉:`make dev-down`
+
+## 頁面結構
+
+### 使用者前台
+
+| 路徑 | 功能 |
+|------|------|
+| `/login` | 登入 |
+| `/register` | 註冊(寄 Email OTP) |
+| `/register/confirm` | 輸入 6 碼完成註冊 |
+| `/app` | 首頁(個人摘要、驗證狀態、角色) |
+| `/app/profile` | 編輯顯示名稱、語系、幣別、電話 |
+| `/app/security` | 商業 Email/手機驗證、TOTP 綁定 |
+
+### 管理後台(需 `tenant_admin` 或 `tenant_owner`)
+
+| 路徑 | 功能 |
+|------|------|
+| `/admin` | 總覽、Policy 重載 |
+| `/admin/roles` | 角色列表、新增、改名、刪除 |
+| `/admin/users` | 依 UID 查詢 / 指派 / 撤銷角色 |
+
+有管理權限時,使用者前台頂部會出現「管理後台」連結。
+
+## 本機預設
+
+- 租戶:`k6-tenant`
+- 邀請碼:`K6INVITE`(需先 `make k6-seed-fixtures` 或對應 Mongo 資料)
+- OTP:開發環境到 MailHog http://localhost:8025 查看
+
+### 取得管理員權限
+
+```bash
+make k6-seed-admin
+# 將輸出的 ADMIN_ACCESS_TOKEN 貼到瀏覽器 — 需自行在 localStorage 設 access_token
+# 或:用 seed 產生的帳密在 /login 登入(若 ZITADEL password grant 可用)
+```
+
+較簡單做法:完成一般註冊登入後,在 Mongo 手動指派 `tenant_admin`,或跑 `k6-seed-admin` 用新帳號登入。
+
+## 環境變數
+
+| 變數 | 說明 |
+|------|------|
+| `VITE_API_BASE` | API 根網址;留空則走 Vite proxy `/api` → `:8888` |
+
+## 建置
+
+```bash
+npm run build # → dist/
+```
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 0000000..ef614d2
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,22 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..d9027c4
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Gateway API 控制台
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..8c92fc9
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,3133 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "qrcode": "^1.5.4",
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "react-router-dom": "^7.15.1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/node": "^24.12.3",
+ "@types/qrcode": "^1.5.6",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^10.3.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.6.0",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.59.2",
+ "vite": "^8.0.12"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "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",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "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/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "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/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "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/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
+ "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz",
+ "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "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": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "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": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "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": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "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": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "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": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "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": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "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": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "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"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.12.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz",
+ "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/qrcode": {
+ "version": "1.5.6",
+ "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
+ "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.15",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
+ "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz",
+ "integrity": "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.60.0",
+ "@typescript-eslint/type-utils": "8.60.0",
+ "@typescript-eslint/utils": "8.60.0",
+ "@typescript-eslint/visitor-keys": "8.60.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.60.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz",
+ "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.60.0",
+ "@typescript-eslint/types": "8.60.0",
+ "@typescript-eslint/typescript-estree": "8.60.0",
+ "@typescript-eslint/visitor-keys": "8.60.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz",
+ "integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.60.0",
+ "@typescript-eslint/types": "^8.60.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz",
+ "integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.60.0",
+ "@typescript-eslint/visitor-keys": "8.60.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz",
+ "integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz",
+ "integrity": "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.60.0",
+ "@typescript-eslint/typescript-estree": "8.60.0",
+ "@typescript-eslint/utils": "8.60.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz",
+ "integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz",
+ "integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.60.0",
+ "@typescript-eslint/tsconfig-utils": "8.60.0",
+ "@typescript-eslint/types": "8.60.0",
+ "@typescript-eslint/visitor-keys": "8.60.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": {
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz",
+ "integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.60.0",
+ "@typescript-eslint/types": "8.60.0",
+ "@typescript-eslint/typescript-estree": "8.60.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz",
+ "integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.60.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz",
+ "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.32",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz",
+ "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001793",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
+ "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dijkstrajs": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.361",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
+ "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz",
+ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.6.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
+ "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.6.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
+ "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "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": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.46",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
+ "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pngjs": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+ "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "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/qrcode": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+ "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "pngjs": "^5.0.0",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "qrcode": "bin/qrcode"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
+ "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
+ "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.6"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.15.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz",
+ "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.15.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz",
+ "integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.15.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "license": "ISC"
+ },
+ "node_modules/rolldown": {
+ "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.132.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@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/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.0.tgz",
+ "integrity": "sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.60.0",
+ "@typescript-eslint/parser": "8.60.0",
+ "@typescript-eslint/typescript-estree": "8.60.0",
+ "@typescript-eslint/utils": "8.60.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "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.15",
+ "rolldown": "1.0.2",
+ "tinyglobby": "^0.2.16"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.18",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+ "license": "ISC"
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yargs/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yargs/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..4035f13
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "qrcode": "^1.5.4",
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "react-router-dom": "^7.15.1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/node": "^24.12.3",
+ "@types/qrcode": "^1.5.6",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^10.3.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.6.0",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.59.2",
+ "vite": "^8.0.12"
+ }
+}
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/frontend/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/frontend/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..7936527
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,533 @@
+:root {
+ --bg: #f4f6f9;
+ --surface: #fff;
+ --border: #e2e8f0;
+ --text: #1e293b;
+ --muted: #64748b;
+ --primary: #2563eb;
+ --primary-hover: #1d4ed8;
+ --danger: #dc2626;
+ --ok: #16a34a;
+ font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
+ line-height: 1.5;
+ color: var(--text);
+ background: var(--bg);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+}
+
+a {
+ color: var(--primary);
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+.loading {
+ padding: 2rem;
+ color: var(--muted);
+}
+
+/* Auth pages */
+.auth-card {
+ max-width: 400px;
+ margin: 4rem auto;
+ padding: 2rem;
+ background: var(--surface);
+ border-radius: 8px;
+ border: 1px solid var(--border);
+ box-shadow: 0 1px 3px rgb(0 0 0 / 6%);
+}
+
+.auth-card h1 {
+ margin: 0 0 1.5rem;
+ font-size: 1.5rem;
+}
+
+.auth-footer {
+ margin-top: 1.5rem;
+ text-align: center;
+ color: var(--muted);
+ font-size: 0.9rem;
+}
+
+/* Shell */
+.shell {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.topbar {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+ padding: 0 1.25rem;
+ height: 56px;
+ background: var(--surface);
+ border-bottom: 1px solid var(--border);
+}
+
+.brand {
+ font-weight: 700;
+ color: var(--text);
+ text-decoration: none;
+}
+
+.nav {
+ display: flex;
+ gap: 1rem;
+ flex: 1;
+}
+
+.nav a {
+ color: var(--muted);
+ font-size: 0.9rem;
+}
+
+.nav a:hover,
+.nav a.active {
+ color: var(--primary);
+}
+
+.topbar-right {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.uid {
+ font-size: 0.75rem;
+ color: var(--muted);
+ font-family: ui-monospace, monospace;
+}
+
+.page {
+ flex: 1;
+ padding: 1.5rem 2rem;
+ max-width: 960px;
+}
+
+/* Admin */
+.admin-shell {
+ flex-direction: row;
+}
+
+.admin-sidebar {
+ width: 220px;
+ background: #1e293b;
+ color: #e2e8f0;
+ padding: 1.25rem 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ min-height: 100vh;
+}
+
+.admin-sidebar .brand {
+ color: #fff;
+ margin-bottom: 1rem;
+}
+
+.admin-nav {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ flex: 1;
+}
+
+.admin-nav a {
+ color: #94a3b8;
+ padding: 0.5rem 0.75rem;
+ border-radius: 4px;
+}
+
+.admin-nav a:hover {
+ background: #334155;
+ color: #fff;
+ text-decoration: none;
+}
+
+.back-link {
+ color: #94a3b8;
+ font-size: 0.85rem;
+ margin-top: auto;
+}
+
+.admin-page {
+ max-width: none;
+ flex: 1;
+}
+
+/* Forms */
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.form-narrow {
+ max-width: 420px;
+}
+
+.form label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ font-size: 0.85rem;
+ font-weight: 500;
+}
+
+.form input,
+.form select,
+.table-input {
+ padding: 0.5rem 0.65rem;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font: inherit;
+}
+
+.form-inline {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ margin-top: 0.5rem;
+}
+
+.form-inline input {
+ flex: 1;
+ min-width: 160px;
+ padding: 0.5rem;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+}
+
+.form-inline-block {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ margin-bottom: 1.5rem;
+}
+
+.form-inline-block input,
+.form-inline-block select {
+ padding: 0.5rem;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+}
+
+.form-error {
+ color: var(--danger);
+ margin: 0;
+ font-size: 0.9rem;
+}
+
+.form-ok {
+ color: var(--ok);
+ margin: 0;
+ font-size: 0.9rem;
+}
+
+.hint {
+ color: var(--muted);
+ font-size: 0.9rem;
+}
+
+/* Buttons */
+.btn-primary {
+ padding: 0.55rem 1rem;
+ background: var(--primary);
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font: inherit;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: var(--primary-hover);
+}
+
+.btn-primary:disabled {
+ opacity: 0.6;
+}
+
+.btn-ghost {
+ padding: 0.4rem 0.75rem;
+ background: transparent;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.85rem;
+}
+
+.btn-link {
+ background: none;
+ border: none;
+ color: var(--primary);
+ cursor: pointer;
+ margin-top: 0.75rem;
+}
+
+.btn-danger,
+.btn-danger-sm {
+ background: var(--danger);
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ padding: 0.35rem 0.65rem;
+ font-size: 0.85rem;
+}
+
+.admin-sidebar .btn-ghost {
+ color: #e2e8f0;
+ border-color: #475569;
+}
+
+/* Cards */
+.cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+ gap: 1rem;
+ margin-top: 1rem;
+}
+
+.card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 1rem 1.25rem;
+}
+
+.card h3 {
+ margin: 0 0 0.75rem;
+ font-size: 1rem;
+}
+
+.card dl {
+ margin: 0;
+ font-size: 0.9rem;
+}
+
+.card dt {
+ color: var(--muted);
+ margin-top: 0.35rem;
+}
+
+.card dd {
+ margin: 0;
+}
+
+.tag-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+}
+
+.tag {
+ background: #e0e7ff;
+ color: #3730a3;
+ padding: 0.2rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.8rem;
+}
+
+.muted {
+ color: var(--muted);
+}
+
+.section {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 1.25rem;
+ margin-bottom: 1rem;
+}
+
+.section h2 {
+ margin: 0 0 0.75rem;
+ font-size: 1.1rem;
+}
+
+.backup-codes {
+ background: #f8fafc;
+ padding: 0.75rem;
+ border-radius: 4px;
+ font-size: 0.85rem;
+ margin-top: 0.5rem;
+}
+
+/* Table */
+.table {
+ width: 100%;
+ border-collapse: collapse;
+ background: var(--surface);
+ border-radius: 8px;
+ overflow: hidden;
+ border: 1px solid var(--border);
+}
+
+.table th,
+.table td {
+ padding: 0.65rem 0.75rem;
+ text-align: left;
+ border-bottom: 1px solid var(--border);
+ font-size: 0.9rem;
+}
+
+.table th {
+ background: #f8fafc;
+ font-weight: 600;
+}
+
+.table-actions {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+}
+
+.role-assign-list {
+ list-style: none;
+ padding: 0;
+}
+
+.role-assign-list li {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem 0;
+ border-bottom: 1px solid var(--border);
+}
+
+/* TOTP QR */
+.totp-qr-panel {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1.25rem;
+ align-items: flex-start;
+ margin: 1rem 0;
+ padding: 1rem;
+ background: #f8fafc;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+}
+
+.totp-qr-panel img {
+ display: block;
+ border-radius: 4px;
+ background: #fff;
+}
+
+.totp-qr-meta {
+ flex: 1;
+ min-width: 200px;
+}
+
+.totp-manual summary {
+ cursor: pointer;
+ font-size: 0.85rem;
+ color: var(--muted);
+}
+
+.otpauth-url {
+ display: block;
+ margin-top: 0.5rem;
+ font-size: 0.7rem;
+ word-break: break-all;
+ color: var(--muted);
+}
+
+/* Admin permissions */
+.admin-links {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.admin-links li {
+ padding: 0.35rem 0;
+}
+
+.admin-toolbar {
+ margin: 1rem 0;
+}
+
+.admin-toolbar select {
+ min-width: 240px;
+ margin-left: 0.5rem;
+}
+
+.perm-grid {
+ display: grid;
+ gap: 1rem;
+ margin-top: 1rem;
+}
+
+.perm-group {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 1rem;
+}
+
+.perm-group h3 {
+ margin: 0 0 0.75rem;
+ font-size: 0.95rem;
+}
+
+.perm-check-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.perm-check-list li {
+ padding: 0.35rem 0;
+}
+
+.perm-check-list label {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ cursor: pointer;
+}
+
+.perm-label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
+}
+
+.perm-api {
+ font-size: 0.8rem;
+ color: var(--muted);
+ font-family: ui-monospace, monospace;
+}
+
+@media (max-width: 768px) {
+ .admin-shell {
+ flex-direction: column;
+ }
+ .admin-sidebar {
+ width: 100%;
+ min-height: auto;
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+ .admin-nav {
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..424a9bf
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,59 @@
+import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
+import { AdminRoute } from './components/AdminRoute';
+import { ProtectedRoute } from './components/ProtectedRoute';
+import { AuthProvider, useAuth } from './context/AuthContext';
+import { AdminLayout } from './layouts/AdminLayout';
+import { UserLayout } from './layouts/UserLayout';
+import { AdminHomePage } from './pages/admin/AdminHomePage';
+import { RolesPage } from './pages/admin/RolesPage';
+import { UserRolesPage } from './pages/admin/UserRolesPage';
+import { RolePermissionsPage } from './pages/admin/RolePermissionsPage';
+import { ConfirmPage } from './pages/user/ConfirmPage';
+import { HomePage } from './pages/user/HomePage';
+import { LoginPage } from './pages/user/LoginPage';
+import { ProfilePage } from './pages/user/ProfilePage';
+import { RegisterPage } from './pages/user/RegisterPage';
+import { SecurityPage } from './pages/user/SecurityPage';
+import './App.css';
+
+function RootRedirect() {
+ const { ready, token } = useAuth();
+ if (!ready) return 載入中…
;
+ return ;
+}
+
+function App() {
+ return (
+
+
+
+ } />
+ } />
+ } />
+
+ }>
+ }>
+ } />
+ } />
+ } />
+
+
+
+ }>
+ }>
+ } />
+ } />
+ } />
+ } />
+
+
+
+ } />
+ } />
+
+
+
+ );
+}
+
+export default App;
diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts
new file mode 100644
index 0000000..fcf5449
--- /dev/null
+++ b/frontend/src/api/auth.ts
@@ -0,0 +1,93 @@
+import { api, setTokens } from './http';
+
+export interface AuthTokenData {
+ access_token: string;
+ refresh_token: string;
+ expires_in: number;
+ uid: string;
+ token_type: string;
+}
+
+export interface RegisterData {
+ challenge_id: string;
+ expires_in: number;
+ uid: string;
+}
+
+export async function login(tenantSlug: string, email: string, password: string) {
+ const data = await api('/api/v1/auth/login', {
+ auth: false,
+ method: 'POST',
+ body: JSON.stringify({ tenant_slug: tenantSlug, email, password }),
+ });
+ setTokens(data.access_token, data.refresh_token);
+ localStorage.setItem('uid', data.uid);
+ return data;
+}
+
+export async function register(body: {
+ tenant_slug: string;
+ invite_code: string;
+ email: string;
+ password: string;
+ display_name?: string;
+ language?: string;
+}) {
+ return api('/api/v1/auth/register', {
+ auth: false,
+ method: 'POST',
+ body: JSON.stringify({
+ ...body,
+ accept_terms_version: '2025-01-01',
+ marketing_opt_in: false,
+ }),
+ });
+}
+
+export async function registerConfirm(
+ tenantSlug: string,
+ challengeId: string,
+ code: string,
+) {
+ const data = await api('/api/v1/auth/register/confirm', {
+ auth: false,
+ method: 'POST',
+ body: JSON.stringify({
+ tenant_slug: tenantSlug,
+ challenge_id: challengeId,
+ code,
+ }),
+ });
+ setTokens(data.access_token, data.refresh_token);
+ localStorage.setItem('uid', data.uid);
+ return data;
+}
+
+export async function registerResend(tenantSlug: string, challengeId: string) {
+ return api('/api/v1/auth/register/resend', {
+ auth: false,
+ method: 'POST',
+ body: JSON.stringify({ tenant_slug: tenantSlug, challenge_id: challengeId }),
+ });
+}
+
+export async function logout() {
+ try {
+ await api('/api/v1/auth/logout', { method: 'POST', body: '{}' });
+ } finally {
+ /* always clear local */
+ }
+}
+
+export async function refreshToken() {
+ const refresh = localStorage.getItem('refresh_token');
+ if (!refresh) throw new Error('無 refresh token');
+ const data = await api('/api/v1/auth/token/refresh', {
+ auth: false,
+ method: 'POST',
+ body: JSON.stringify({ refresh_token: refresh }),
+ });
+ setTokens(data.access_token, data.refresh_token);
+ localStorage.setItem('uid', data.uid);
+ return data;
+}
diff --git a/frontend/src/api/http.ts b/frontend/src/api/http.ts
new file mode 100644
index 0000000..cac2eb3
--- /dev/null
+++ b/frontend/src/api/http.ts
@@ -0,0 +1,71 @@
+export class ApiError extends Error {
+ status: number;
+ code?: number;
+ body?: unknown;
+
+ constructor(message: string, status: number, code?: number, body?: unknown) {
+ super(message);
+ this.name = 'ApiError';
+ this.status = status;
+ this.code = code;
+ this.body = body;
+ }
+}
+
+export interface Envelope {
+ code: number;
+ message: string;
+ data?: T;
+ error?: { biz_code?: string };
+}
+
+function getBaseUrl() {
+ return (import.meta.env.VITE_API_BASE as string | undefined) ?? '';
+}
+
+export function getToken(): string {
+ return localStorage.getItem('access_token') ?? '';
+}
+
+export function setTokens(access: string, refresh: string) {
+ localStorage.setItem('access_token', access);
+ localStorage.setItem('refresh_token', refresh);
+}
+
+export function clearTokens() {
+ localStorage.removeItem('access_token');
+ localStorage.removeItem('refresh_token');
+ localStorage.removeItem('uid');
+ localStorage.removeItem('roles');
+}
+
+export async function api(
+ path: string,
+ init: RequestInit & { auth?: boolean } = {},
+): Promise {
+ const { auth = true, headers: initHeaders, ...rest } = init;
+ const headers = new Headers(initHeaders);
+ headers.set('Accept', 'application/json');
+ if (rest.body && !headers.has('Content-Type')) {
+ headers.set('Content-Type', 'application/json');
+ }
+ if (auth) {
+ const token = getToken();
+ if (token) headers.set('Authorization', `Bearer ${token}`);
+ }
+
+ const res = await fetch(`${getBaseUrl()}${path}`, { ...rest, headers });
+ const text = await res.text();
+ let json: Envelope | null = null;
+ try {
+ json = text ? (JSON.parse(text) as Envelope) : null;
+ } catch {
+ /* non-json */
+ }
+
+ if (!res.ok || (json && json.code !== 102000 && json.code !== 0)) {
+ const msg = json?.message ?? res.statusText ?? '請求失敗';
+ throw new ApiError(msg, res.status, json?.code, json);
+ }
+ return (json?.data ?? json) as T;
+}
diff --git a/frontend/src/api/member.ts b/frontend/src/api/member.ts
new file mode 100644
index 0000000..2166577
--- /dev/null
+++ b/frontend/src/api/member.ts
@@ -0,0 +1,100 @@
+import { api } from './http';
+
+export interface MemberMe {
+ tenant_id: string;
+ uid: string;
+ zitadel_email?: string;
+ display_name?: string;
+ avatar?: string;
+ phone?: string;
+ language?: string;
+ currency?: string;
+ status: string;
+ business_email?: string;
+ business_email_verified: boolean;
+ business_phone?: string;
+ business_phone_verified: boolean;
+ totp_enrolled: boolean;
+}
+
+export interface VerificationStart {
+ challenge_id: string;
+ expires_in: number;
+}
+
+export interface TOTPStatus {
+ enrolled: boolean;
+ backup_codes_remaining: number;
+}
+
+export interface TOTPEnrollStart {
+ otpauth_url: string;
+ issuer: string;
+ account: string;
+}
+
+export function getMe() {
+ return api('/api/v1/members/me');
+}
+
+export function updateMe(body: {
+ display_name?: string;
+ language?: string;
+ currency?: string;
+ phone?: string;
+}) {
+ return api('/api/v1/members/me', {
+ method: 'PATCH',
+ body: JSON.stringify(body),
+ });
+}
+
+export function startEmailVerification(target: string) {
+ return api('/api/v1/members/me/verifications/email/start', {
+ method: 'POST',
+ body: JSON.stringify({ target }),
+ });
+}
+
+export function confirmEmailVerification(challengeId: string, code: string) {
+ return api('/api/v1/members/me/verifications/email/confirm', {
+ method: 'POST',
+ body: JSON.stringify({ challenge_id: challengeId, code }),
+ });
+}
+
+export function startPhoneVerification(target: string) {
+ return api('/api/v1/members/me/verifications/phone/start', {
+ method: 'POST',
+ body: JSON.stringify({ target }),
+ });
+}
+
+export function confirmPhoneVerification(challengeId: string, code: string) {
+ return api('/api/v1/members/me/verifications/phone/confirm', {
+ method: 'POST',
+ body: JSON.stringify({ challenge_id: challengeId, code }),
+ });
+}
+
+export function getTOTPStatus() {
+ return api('/api/v1/members/me/totp');
+}
+
+export function startTOTPEnroll() {
+ return api('/api/v1/members/me/totp/enroll-start', {
+ method: 'POST',
+ body: '{}',
+ });
+}
+
+export function confirmTOTPEnroll(code: string) {
+ return api<{ backup_codes: string[] }>(
+ '/api/v1/members/me/totp/enroll-confirm',
+ { method: 'POST', body: JSON.stringify({ code }) },
+ );
+}
+
+export function disableTOTP() {
+ return api('/api/v1/members/me/totp', { method: 'DELETE' });
+}
diff --git a/frontend/src/api/permission.ts b/frontend/src/api/permission.ts
new file mode 100644
index 0000000..10b159d
--- /dev/null
+++ b/frontend/src/api/permission.ts
@@ -0,0 +1,125 @@
+import { api } from './http';
+
+export interface MePermissions {
+ uid: string;
+ tenant_id: string;
+ roles: string[];
+ permissions: Record;
+}
+
+export interface Role {
+ id: string;
+ key: string;
+ display_name: string;
+ status: string;
+ is_system: boolean;
+}
+
+export interface RoleList {
+ roles: Role[];
+}
+
+export interface UserRoleList {
+ user_roles: Array<{
+ role_id: string;
+ role_key: string;
+ display_name: string;
+ }>;
+}
+
+export interface PermissionNode {
+ id: string;
+ parent?: string;
+ name: string;
+ http_methods?: string;
+ http_path?: string;
+ status: string;
+ type: string;
+ children?: PermissionNode[];
+}
+
+export interface PermissionCatalog {
+ tree?: PermissionNode[];
+ list?: PermissionNode[];
+}
+
+export interface RolePermissions {
+ permissions: PermissionNode[];
+}
+
+const ADMIN_ROLES = new Set(['tenant_admin', 'tenant_owner']);
+
+export function isAdminRole(roles: string[]) {
+ return roles.some((r) => ADMIN_ROLES.has(r));
+}
+
+export function getMyPermissions() {
+ return api('/api/v1/permissions/me');
+}
+
+export function listRoles() {
+ return api('/api/v1/permissions/roles');
+}
+
+export function createRole(key: string, displayName: string) {
+ return api('/api/v1/permissions/roles', {
+ method: 'POST',
+ body: JSON.stringify({ key, display_name: displayName, status: 'open' }),
+ });
+}
+
+export function updateRole(id: string, displayName: string) {
+ return api(`/api/v1/permissions/roles/${id}`, {
+ method: 'PATCH',
+ body: JSON.stringify({ display_name: displayName }),
+ });
+}
+
+export function deleteRole(id: string) {
+ return api(`/api/v1/permissions/roles/${id}`, { method: 'DELETE' });
+}
+
+export function listUserRoles(uid: string) {
+ return api(`/api/v1/permissions/users/${uid}/roles`);
+}
+
+export function assignUserRole(uid: string, roleId: string) {
+ return api(`/api/v1/permissions/users/${uid}/roles`, {
+ method: 'POST',
+ body: JSON.stringify({ role_id: roleId, source: 'manual' }),
+ });
+}
+
+export function revokeUserRole(uid: string, roleId: string) {
+ return api(`/api/v1/permissions/users/${uid}/roles/${roleId}`, {
+ method: 'DELETE',
+ });
+}
+
+export function reloadPolicy(tenantId: string) {
+ return api('/api/v1/permissions/policy/reload', {
+ method: 'POST',
+ body: JSON.stringify({ tenant_id: tenantId }),
+ });
+}
+
+export function getPermissionCatalog(opts?: { tree?: boolean; type?: string }) {
+ const q = new URLSearchParams();
+ if (opts?.tree) q.set('tree', 'true');
+ if (opts?.type) q.set('type', opts.type);
+ const qs = q.toString();
+ return api(
+ `/api/v1/permissions/catalog${qs ? `?${qs}` : ''}`,
+ );
+}
+
+export function getRolePermissions(roleId: string) {
+ return api(`/api/v1/permissions/roles/${roleId}/permissions`);
+}
+
+export function replaceRolePermissions(roleId: string, permissionIds: string[]) {
+ return api(`/api/v1/permissions/roles/${roleId}/permissions`, {
+ method: 'PUT',
+ body: JSON.stringify({ permission_ids: permissionIds }),
+ });
+}
diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png
new file mode 100644
index 0000000..02251f4
Binary files /dev/null and b/frontend/src/assets/hero.png differ
diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg
new file mode 100644
index 0000000..5101b67
--- /dev/null
+++ b/frontend/src/assets/vite.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/src/components/AdminRoute.tsx b/frontend/src/components/AdminRoute.tsx
new file mode 100644
index 0000000..7a54806
--- /dev/null
+++ b/frontend/src/components/AdminRoute.tsx
@@ -0,0 +1,10 @@
+import { Navigate, Outlet } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+
+export function AdminRoute() {
+ const { ready, token, isAdmin } = useAuth();
+ if (!ready) return 載入中…
;
+ if (!token) return ;
+ if (!isAdmin) return ;
+ return ;
+}
diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx
new file mode 100644
index 0000000..ba85719
--- /dev/null
+++ b/frontend/src/components/ProtectedRoute.tsx
@@ -0,0 +1,9 @@
+import { Navigate, Outlet } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+
+export function ProtectedRoute() {
+ const { ready, token } = useAuth();
+ if (!ready) return 載入中…
;
+ if (!token) return ;
+ return ;
+}
diff --git a/frontend/src/components/TotpQrPanel.tsx b/frontend/src/components/TotpQrPanel.tsx
new file mode 100644
index 0000000..265e99e
--- /dev/null
+++ b/frontend/src/components/TotpQrPanel.tsx
@@ -0,0 +1,64 @@
+import { useEffect, useState } from 'react';
+import QRCode from 'qrcode';
+
+interface Props {
+ otpauthUrl: string;
+ issuer?: string;
+ account?: string;
+}
+
+export function TotpQrPanel({ otpauthUrl, issuer, account }: Props) {
+ const [dataUrl, setDataUrl] = useState('');
+ const [err, setErr] = useState('');
+
+ useEffect(() => {
+ if (!otpauthUrl) {
+ setDataUrl('');
+ return;
+ }
+ let cancelled = false;
+ QRCode.toDataURL(otpauthUrl, { width: 220, margin: 2, errorCorrectionLevel: 'M' })
+ .then((url) => {
+ if (!cancelled) setDataUrl(url);
+ })
+ .catch(() => {
+ if (!cancelled) setErr('無法產生 QR Code');
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [otpauthUrl]);
+
+ if (!otpauthUrl) return null;
+
+ return (
+
+ {dataUrl ? (
+

+ ) : err ? (
+
{err}
+ ) : (
+
產生 QR Code 中…
+ )}
+
+ {issuer && (
+
+ Issuer {issuer}
+
+ )}
+ {account && (
+
+ 帳號 {account}
+
+ )}
+
+ 用 Google Authenticator、1Password 等 App 掃描 QR,或手動輸入密鑰(進階設定)。
+
+
+ 無法掃描?顯示 otpauth 連結
+ {otpauthUrl}
+
+
+
+ );
+}
diff --git a/frontend/src/config.ts b/frontend/src/config.ts
new file mode 100644
index 0000000..a1656d4
--- /dev/null
+++ b/frontend/src/config.ts
@@ -0,0 +1,3 @@
+/** 本機 k6 / dev 預設值,可在登入頁覆寫 */
+export const DEFAULT_TENANT = 'k6-tenant';
+export const DEFAULT_INVITE = 'K6INVITE';
diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx
new file mode 100644
index 0000000..9dcc569
--- /dev/null
+++ b/frontend/src/context/AuthContext.tsx
@@ -0,0 +1,96 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactNode,
+} from 'react';
+import { clearTokens, getToken } from '../api/http';
+import * as permApi from '../api/permission';
+
+interface AuthState {
+ ready: boolean;
+ token: string;
+ uid: string;
+ roles: string[];
+ isAdmin: boolean;
+ syncSession: () => void;
+ refreshRoles: () => Promise;
+ signOut: () => void;
+}
+
+const AuthContext = createContext(null);
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [ready, setReady] = useState(false);
+ const [token, setToken] = useState(getToken);
+ const [uid, setUid] = useState(() => localStorage.getItem('uid') ?? '');
+ const [roles, setRoles] = useState(() => {
+ try {
+ return JSON.parse(localStorage.getItem('roles') ?? '[]') as string[];
+ } catch {
+ return [];
+ }
+ });
+
+ const syncSession = useCallback(() => {
+ setToken(getToken());
+ setUid(localStorage.getItem('uid') ?? '');
+ }, []);
+
+ const refreshRoles = useCallback(async () => {
+ if (!getToken()) {
+ setRoles([]);
+ return;
+ }
+ try {
+ const me = await permApi.getMyPermissions();
+ setRoles(me.roles ?? []);
+ localStorage.setItem('roles', JSON.stringify(me.roles ?? []));
+ if (me.uid) {
+ setUid(me.uid);
+ localStorage.setItem('uid', me.uid);
+ }
+ } catch {
+ setRoles([]);
+ }
+ }, []);
+
+ useEffect(() => {
+ (async () => {
+ if (getToken()) await refreshRoles();
+ setReady(true);
+ })();
+ }, [refreshRoles]);
+
+ const signOut = useCallback(() => {
+ clearTokens();
+ setToken('');
+ setUid('');
+ setRoles([]);
+ }, []);
+
+ const value = useMemo(
+ () => ({
+ ready,
+ token,
+ uid,
+ roles,
+ isAdmin: permApi.isAdminRole(roles),
+ syncSession,
+ refreshRoles,
+ signOut,
+ }),
+ [ready, token, uid, roles, syncSession, refreshRoles, signOut],
+ );
+
+ return {children};
+}
+
+export function useAuth() {
+ const ctx = useContext(AuthContext);
+ if (!ctx) throw new Error('useAuth 需在 AuthProvider 內');
+ return ctx;
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..904ba9b
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,4 @@
+/* Global reset — layout in App.css */
+body {
+ margin: 0;
+}
diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx
new file mode 100644
index 0000000..ccfc0a0
--- /dev/null
+++ b/frontend/src/layouts/AdminLayout.tsx
@@ -0,0 +1,41 @@
+import { Link, Outlet, useNavigate } from 'react-router-dom';
+import * as authApi from '../api/auth';
+import { useAuth } from '../context/AuthContext';
+
+export function AdminLayout() {
+ const { signOut } = useAuth();
+ const navigate = useNavigate();
+
+ const handleLogout = async () => {
+ try {
+ await authApi.logout();
+ } catch {
+ /* ignore */
+ }
+ signOut();
+ navigate('/login');
+ };
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/layouts/UserLayout.tsx b/frontend/src/layouts/UserLayout.tsx
new file mode 100644
index 0000000..00cbc15
--- /dev/null
+++ b/frontend/src/layouts/UserLayout.tsx
@@ -0,0 +1,43 @@
+import { Link, Outlet, useNavigate } from 'react-router-dom';
+import * as authApi from '../api/auth';
+import { useAuth } from '../context/AuthContext';
+
+export function UserLayout() {
+ const { signOut, isAdmin, uid } = useAuth();
+ const navigate = useNavigate();
+
+ const handleLogout = async () => {
+ try {
+ await authApi.logout();
+ } catch {
+ /* ignore */
+ }
+ signOut();
+ navigate('/login');
+ };
+
+ return (
+
+
+
+ Gateway
+
+
+
+ {uid}
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/frontend/src/pages/admin/AdminHomePage.tsx b/frontend/src/pages/admin/AdminHomePage.tsx
new file mode 100644
index 0000000..0a721e0
--- /dev/null
+++ b/frontend/src/pages/admin/AdminHomePage.tsx
@@ -0,0 +1,67 @@
+import { Link } from 'react-router-dom';
+import { useState } from 'react';
+import * as permApi from '../../api/permission';
+import { ApiError } from '../../api/http';
+import { useAuth } from '../../context/AuthContext';
+
+export function AdminHomePage() {
+ const { roles, uid } = useAuth();
+ const [msg, setMsg] = useState('');
+ const [error, setError] = useState('');
+
+ const reloadPolicy = async () => {
+ setError('');
+ setMsg('');
+ try {
+ const tenant = localStorage.getItem('tenant_slug') ?? 'k6-tenant';
+ await permApi.reloadPolicy(tenant);
+ setMsg('Casbin policy 已重載');
+ } catch (e) {
+ setError(e instanceof ApiError ? e.message : '重載失敗');
+ }
+ };
+
+ return (
+
+
管理後台
+
以 tenant_admin / tenant_owner 角色登入後可使用。
+
+
+
目前管理員
+
UID: {uid}
+
+ {roles.map((r) => (
+ -
+ {r}
+
+ ))}
+
+
+
+
權限管理
+
+ -
+ 角色管理 — 新增 / 改名 / 刪除角色
+
+ -
+ 角色權限 — 勾選 API
+ 權限
+
+ -
+ 使用者角色 — 指派 tenant_admin 等
+
+
+
+
+
系統維護
+
角色或權限變更後,可手動觸發 policy 重載。
+
+ {msg &&
{msg}
}
+ {error &&
{error}
}
+
+
+
+ );
+}
diff --git a/frontend/src/pages/admin/RolePermissionsPage.tsx b/frontend/src/pages/admin/RolePermissionsPage.tsx
new file mode 100644
index 0000000..a75508c
--- /dev/null
+++ b/frontend/src/pages/admin/RolePermissionsPage.tsx
@@ -0,0 +1,174 @@
+import { useEffect, useMemo, useState } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import * as permApi from '../../api/permission';
+import type { PermissionNode, Role } from '../../api/permission';
+import { ApiError } from '../../api/http';
+
+/** 可勾選的權限節點(有實際 API path 的葉節點) */
+function leafPermissions(nodes: PermissionNode[]): PermissionNode[] {
+ return nodes.filter((n) => n.http_path);
+}
+
+function groupByParent(leaves: PermissionNode[]) {
+ const groups = new Map();
+ for (const p of leaves) {
+ const key = p.parent || '(root)';
+ const arr = groups.get(key) ?? [];
+ arr.push(p);
+ groups.set(key, arr);
+ }
+ return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b));
+}
+
+export function RolePermissionsPage() {
+ const [searchParams] = useSearchParams();
+ const presetRole = searchParams.get('role') ?? '';
+ const [roles, setRoles] = useState([]);
+ const [roleId, setRoleId] = useState(presetRole);
+ const [catalog, setCatalog] = useState([]);
+ const [selected, setSelected] = useState>(new Set());
+ const [error, setError] = useState('');
+ const [msg, setMsg] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const leaves = useMemo(() => leafPermissions(catalog), [catalog]);
+ const groups = useMemo(() => groupByParent(leaves), [leaves]);
+ const selectedRole = roles.find((r) => r.id === roleId);
+
+ useEffect(() => {
+ if (presetRole) setRoleId(presetRole);
+ }, [presetRole]);
+
+ useEffect(() => {
+ permApi
+ .listRoles()
+ .then((r) => setRoles(r.roles ?? []))
+ .catch((e) => setError(e instanceof ApiError ? e.message : '載入角色失敗'));
+ permApi
+ .getPermissionCatalog({ type: 'backend_user' })
+ .then((c) => setCatalog(c.list ?? []))
+ .catch((e) => setError(e instanceof ApiError ? e.message : '載入權限目錄失敗'));
+ }, []);
+
+ useEffect(() => {
+ if (!roleId) {
+ setSelected(new Set());
+ return;
+ }
+ setError('');
+ permApi
+ .getRolePermissions(roleId)
+ .then((r) => {
+ const ids = new Set((r.permissions ?? []).map((p) => p.id));
+ setSelected(ids);
+ })
+ .catch((e) => {
+ setError(e instanceof ApiError ? e.message : '載入角色權限失敗');
+ setSelected(new Set());
+ });
+ }, [roleId]);
+
+ const toggle = (id: string) => {
+ setSelected((prev) => {
+ const next = new Set(prev);
+ if (next.has(id)) next.delete(id);
+ else next.add(id);
+ return next;
+ });
+ };
+
+ const save = async () => {
+ if (!roleId) return;
+ setLoading(true);
+ setError('');
+ setMsg('');
+ try {
+ // 只送葉節點 ID;後端會自動補齊父權限
+ const leafIds = leaves.filter((l) => selected.has(l.id)).map((l) => l.id);
+ await permApi.replaceRolePermissions(roleId, leafIds);
+ const tenant = localStorage.getItem('tenant_slug') ?? 'k6-tenant';
+ await permApi.reloadPolicy(tenant);
+ setMsg('已儲存並重載 Casbin policy');
+ const r = await permApi.getRolePermissions(roleId);
+ setSelected(new Set((r.permissions ?? []).map((p) => p.id)));
+ } catch (e) {
+ setError(e instanceof ApiError ? e.message : '儲存失敗');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
角色權限設定
+
+ 為自訂角色勾選 API 權限。儲存後會自動重載 policy。系統內建角色(tenant_admin
+ 等)權限由種子資料定義,建議只調整非系統角色。
+
+
+ {error &&
{error}
}
+ {msg &&
{msg}
}
+
+
+
+ {roleId && (
+
+ )}
+
+
+ {selectedRole?.is_system && (
+
系統角色不建議在此修改;請建立自訂角色(如 support)再指派權限。
+ )}
+
+ {roleId && !selectedRole?.is_system && (
+
+ {groups.map(([parent, items]) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/admin/RolesPage.tsx b/frontend/src/pages/admin/RolesPage.tsx
new file mode 100644
index 0000000..29db411
--- /dev/null
+++ b/frontend/src/pages/admin/RolesPage.tsx
@@ -0,0 +1,137 @@
+import { useEffect, useState, type FormEvent } from 'react';
+import { Link } from 'react-router-dom';
+import * as permApi from '../../api/permission';
+import type { Role } from '../../api/permission';
+import { ApiError } from '../../api/http';
+
+export function RolesPage() {
+ const [roles, setRoles] = useState([]);
+ const [key, setKey] = useState('');
+ const [displayName, setDisplayName] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const load = () => {
+ permApi
+ .listRoles()
+ .then((r) => setRoles(r.roles ?? []))
+ .catch((e) => setError(e instanceof ApiError ? e.message : '載入失敗'));
+ };
+
+ useEffect(() => {
+ load();
+ }, []);
+
+ const create = async (e: FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+ try {
+ await permApi.createRole(key, displayName || key);
+ setKey('');
+ setDisplayName('');
+ load();
+ } catch (e) {
+ setError(e instanceof ApiError ? e.message : '建立失敗');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const remove = async (id: string, isSystem: boolean) => {
+ if (isSystem) {
+ alert('系統角色不可刪除');
+ return;
+ }
+ if (!confirm('確定刪除此角色?')) return;
+ try {
+ await permApi.deleteRole(id);
+ load();
+ } catch (e) {
+ setError(e instanceof ApiError ? e.message : '刪除失敗');
+ }
+ };
+
+ const rename = async (id: string, name: string) => {
+ try {
+ await permApi.updateRole(id, name);
+ load();
+ } catch (e) {
+ setError(e instanceof ApiError ? e.message : '更新失敗');
+ }
+ };
+
+ return (
+
+
角色管理
+ {error &&
{error}
}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/admin/UserRolesPage.tsx b/frontend/src/pages/admin/UserRolesPage.tsx
new file mode 100644
index 0000000..f56bb93
--- /dev/null
+++ b/frontend/src/pages/admin/UserRolesPage.tsx
@@ -0,0 +1,136 @@
+import { useState, type FormEvent } from 'react';
+import * as permApi from '../../api/permission';
+import type { Role } from '../../api/permission';
+import { ApiError } from '../../api/http';
+import { useAuth } from '../../context/AuthContext';
+
+export function UserRolesPage() {
+ const { uid: myUid } = useAuth();
+ const [uid, setUid] = useState('');
+ const [roles, setRoles] = useState([]);
+ const [userRoles, setUserRoles] = useState<
+ Array<{ role_id: string; role_key: string; display_name: string }>
+ >([]);
+ const [assignRoleId, setAssignRoleId] = useState('');
+ const [error, setError] = useState('');
+ const [msg, setMsg] = useState('');
+
+ const loadRoleOptions = () => {
+ permApi.listRoles().then((r) => setRoles(r.roles ?? []));
+ };
+
+ const search = async (e: FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setMsg('');
+ loadRoleOptions();
+ try {
+ const r = await permApi.listUserRoles(uid.trim());
+ setUserRoles(r.user_roles ?? []);
+ setMsg('已載入');
+ } catch (e) {
+ setError(e instanceof ApiError ? e.message : '查詢失敗');
+ setUserRoles([]);
+ }
+ };
+
+ const assign = async () => {
+ if (!assignRoleId) return;
+ setError('');
+ try {
+ await permApi.assignUserRole(uid.trim(), assignRoleId);
+ const r = await permApi.listUserRoles(uid.trim());
+ setUserRoles(r.user_roles ?? []);
+ setMsg('已指派角色');
+ } catch (e) {
+ setError(e instanceof ApiError ? e.message : '指派失敗');
+ }
+ };
+
+ const revoke = async (roleId: string) => {
+ if (!confirm('撤銷此角色?')) return;
+ setError('');
+ try {
+ await permApi.revokeUserRole(uid.trim(), roleId);
+ const r = await permApi.listUserRoles(uid.trim());
+ setUserRoles(r.user_roles ?? []);
+ setMsg('已撤銷');
+ } catch (e) {
+ setError(e instanceof ApiError ? e.message : '撤銷失敗');
+ }
+ };
+
+ return (
+
+
使用者角色
+
+ 輸入成員 UID(註冊後可在首頁查看)。指派 tenant_admin{' '}
+ 或 tenant_owner 後,對方重新登入即可進管理後台。
+
+
+
+
+ {msg &&
{msg}
}
+ {error &&
{error}
}
+
+ {userRoles.length > 0 && (
+ <>
+
目前角色
+
+ {userRoles.map((ur) => (
+ -
+
+ {ur.display_name} (
{ur.role_key})
+
+
+
+ ))}
+
+
+
指派新角色
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/user/ConfirmPage.tsx b/frontend/src/pages/user/ConfirmPage.tsx
new file mode 100644
index 0000000..dad9ea8
--- /dev/null
+++ b/frontend/src/pages/user/ConfirmPage.tsx
@@ -0,0 +1,95 @@
+import { useEffect, useState, type FormEvent } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import * as authApi from '../../api/auth';
+import { ApiError } from '../../api/http';
+import { useAuth } from '../../context/AuthContext';
+
+export function ConfirmPage() {
+ const navigate = useNavigate();
+ const { syncSession, refreshRoles } = useAuth();
+ const [tenant, setTenant] = useState('');
+ const [challengeId, setChallengeId] = useState('');
+ const [email, setEmail] = useState('');
+ const [code, setCode] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [resendMsg, setResendMsg] = useState('');
+
+ useEffect(() => {
+ const raw = sessionStorage.getItem('register_pending');
+ if (!raw) return;
+ const p = JSON.parse(raw) as {
+ tenant: string;
+ challenge_id: string;
+ email: string;
+ };
+ setTenant(p.tenant);
+ setChallengeId(p.challenge_id);
+ setEmail(p.email);
+ }, []);
+
+ const submit = async (e: FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+ try {
+ await authApi.registerConfirm(tenant, challengeId, code);
+ sessionStorage.removeItem('register_pending');
+ syncSession();
+ await refreshRoles();
+ navigate('/app');
+ } catch (err) {
+ setError(err instanceof ApiError ? err.message : '驗證失敗');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const resend = async () => {
+ setResendMsg('');
+ try {
+ await authApi.registerResend(tenant, challengeId);
+ setResendMsg('已重新寄送驗證碼');
+ } catch (err) {
+ setResendMsg(err instanceof ApiError ? err.message : '重送失敗');
+ }
+ };
+
+ if (!challengeId) {
+ return (
+
+ );
+ }
+
+ return (
+
+
Email 驗證
+
+ 驗證碼已寄至 {email}(開發環境請到 MailHog 查看)
+
+
+
+ {resendMsg &&
{resendMsg}
}
+
+ );
+}
diff --git a/frontend/src/pages/user/HomePage.tsx b/frontend/src/pages/user/HomePage.tsx
new file mode 100644
index 0000000..23f2856
--- /dev/null
+++ b/frontend/src/pages/user/HomePage.tsx
@@ -0,0 +1,70 @@
+import { useEffect, useState } from 'react';
+import * as memberApi from '../../api/member';
+import { ApiError } from '../../api/http';
+import { useAuth } from '../../context/AuthContext';
+
+export function HomePage() {
+ const { roles, uid } = useAuth();
+ const [me, setMe] = useState(null);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ memberApi
+ .getMe()
+ .then(setMe)
+ .catch((e) => setError(e instanceof ApiError ? e.message : '載入失敗'));
+ }, []);
+
+ if (error) return {error}
;
+ if (!me) return 載入個人資料…
;
+
+ return (
+
+
你好,{me.display_name || me.zitadel_email || uid}
+
+
+
帳號
+
+ - UID
+ - {me.uid}
+ - 登入 Email
+ - {me.zitadel_email ?? '—'}
+ - 狀態
+ - {me.status}
+
+
+
+
驗證狀態
+
+ - 商業聯絡 Email
+ -
+ {me.business_email || me.zitadel_email || '未設定'}{' '}
+ {me.business_email_verified ? '✓ 已驗證' : '未驗證'}
+
+ - 商業手機
+ -
+ {me.business_phone || '未設定'}{' '}
+ {me.business_phone_verified ? '✓' : '未驗證'}
+
+ - 雙因素 (TOTP)
+ - {me.totp_enrolled ? '已啟用' : '未啟用'}
+
+
+
+
我的角色
+
+ {roles.length === 0 ? (
+ - 尚無指派角色
+ ) : (
+ roles.map((r) => (
+ -
+ {r}
+
+ ))
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/user/LoginPage.tsx b/frontend/src/pages/user/LoginPage.tsx
new file mode 100644
index 0000000..f074f47
--- /dev/null
+++ b/frontend/src/pages/user/LoginPage.tsx
@@ -0,0 +1,75 @@
+import { useState, type FormEvent } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import * as authApi from '../../api/auth';
+import { ApiError } from '../../api/http';
+import { DEFAULT_TENANT } from '../../config';
+import { useAuth } from '../../context/AuthContext';
+import * as permApi from '../../api/permission';
+
+export function LoginPage() {
+ const navigate = useNavigate();
+ const { syncSession, refreshRoles } = useAuth();
+ const [tenant, setTenant] = useState(
+ () => localStorage.getItem('tenant_slug') ?? DEFAULT_TENANT,
+ );
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const submit = async (e: FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+ try {
+ localStorage.setItem('tenant_slug', tenant);
+ await authApi.login(tenant, email, password);
+ syncSession();
+ await refreshRoles();
+ const me = await permApi.getMyPermissions();
+ const admin = permApi.isAdminRole(me.roles ?? []);
+ navigate(admin ? '/admin' : '/app');
+ } catch (err) {
+ setError(err instanceof ApiError ? err.message : '登入失敗');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/pages/user/ProfilePage.tsx b/frontend/src/pages/user/ProfilePage.tsx
new file mode 100644
index 0000000..13846ff
--- /dev/null
+++ b/frontend/src/pages/user/ProfilePage.tsx
@@ -0,0 +1,74 @@
+import { useEffect, useState, type FormEvent } from 'react';
+import * as memberApi from '../../api/member';
+import { ApiError } from '../../api/http';
+
+export function ProfilePage() {
+ const [displayName, setDisplayName] = useState('');
+ const [language, setLanguage] = useState('zh-TW');
+ const [currency, setCurrency] = useState('TWD');
+ const [phone, setPhone] = useState('');
+ const [msg, setMsg] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ memberApi.getMe().then((m) => {
+ setDisplayName(m.display_name ?? '');
+ setLanguage(m.language ?? 'zh-TW');
+ setCurrency(m.currency ?? 'TWD');
+ setPhone(m.phone ?? '');
+ });
+ }, []);
+
+ const submit = async (e: FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setMsg('');
+ setLoading(true);
+ try {
+ await memberApi.updateMe({
+ display_name: displayName,
+ language,
+ currency,
+ phone: phone || undefined,
+ });
+ setMsg('已儲存');
+ } catch (err) {
+ setError(err instanceof ApiError ? err.message : '儲存失敗');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/pages/user/RegisterPage.tsx b/frontend/src/pages/user/RegisterPage.tsx
new file mode 100644
index 0000000..4a0e5da
--- /dev/null
+++ b/frontend/src/pages/user/RegisterPage.tsx
@@ -0,0 +1,97 @@
+import { useState, type FormEvent } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import * as authApi from '../../api/auth';
+import { ApiError } from '../../api/http';
+import { DEFAULT_INVITE, DEFAULT_TENANT } from '../../config';
+
+export function RegisterPage() {
+ const navigate = useNavigate();
+ const [tenant, setTenant] = useState(
+ () => localStorage.getItem('tenant_slug') ?? DEFAULT_TENANT,
+ );
+ const [invite, setInvite] = useState(DEFAULT_INVITE);
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [displayName, setDisplayName] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const submit = async (e: FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+ try {
+ localStorage.setItem('tenant_slug', tenant);
+ const data = await authApi.register({
+ tenant_slug: tenant,
+ invite_code: invite,
+ email,
+ password,
+ display_name: displayName || undefined,
+ language: 'zh-TW',
+ });
+ sessionStorage.setItem(
+ 'register_pending',
+ JSON.stringify({
+ tenant,
+ challenge_id: data.challenge_id,
+ email,
+ }),
+ );
+ navigate('/register/confirm');
+ } catch (err) {
+ setError(err instanceof ApiError ? err.message : '註冊失敗');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/pages/user/SecurityPage.tsx b/frontend/src/pages/user/SecurityPage.tsx
new file mode 100644
index 0000000..72d5b89
--- /dev/null
+++ b/frontend/src/pages/user/SecurityPage.tsx
@@ -0,0 +1,251 @@
+import { useEffect, useState, type FormEvent } from 'react';
+import * as memberApi from '../../api/member';
+import { ApiError } from '../../api/http';
+import { TotpQrPanel } from '../../components/TotpQrPanel';
+
+export function SecurityPage() {
+ const [me, setMe] = useState(null);
+ const [totp, setTotp] = useState(null);
+ const [enrollInfo, setEnrollInfo] = useState(
+ null,
+ );
+ const [backupCodes, setBackupCodes] = useState([]);
+
+ const [emailTarget, setEmailTarget] = useState('');
+ const [emailChallenge, setEmailChallenge] = useState('');
+ const [emailCode, setEmailCode] = useState('');
+
+ const [phoneTarget, setPhoneTarget] = useState('');
+ const [phoneChallenge, setPhoneChallenge] = useState('');
+ const [phoneCode, setPhoneCode] = useState('');
+
+ const [totpCode, setTotpCode] = useState('');
+ const [msg, setMsg] = useState('');
+ const [error, setError] = useState('');
+
+ const reload = async () => {
+ const [profile, totpStatus] = await Promise.all([
+ memberApi.getMe().catch(() => null),
+ memberApi.getTOTPStatus().catch(() => null),
+ ]);
+ setMe(profile);
+ setTotp(totpStatus);
+ if (profile?.zitadel_email && !emailTarget) {
+ setEmailTarget(profile.zitadel_email);
+ }
+ };
+
+ useEffect(() => {
+ reload();
+ }, []);
+
+ const showErr = (e: unknown) =>
+ setError(e instanceof ApiError ? e.message : '操作失敗');
+
+ const startEmail = async (e: FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setMsg('');
+ try {
+ const r = await memberApi.startEmailVerification(emailTarget);
+ setEmailChallenge(r.challenge_id);
+ setMsg('Email 驗證碼已寄出');
+ } catch (e) {
+ showErr(e);
+ }
+ };
+
+ const confirmEmail = async (e: FormEvent) => {
+ e.preventDefault();
+ setError('');
+ try {
+ await memberApi.confirmEmailVerification(emailChallenge, emailCode);
+ setMsg('商業 Email 已驗證');
+ await reload();
+ } catch (e) {
+ showErr(e);
+ }
+ };
+
+ const startPhone = async (e: FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setMsg('');
+ try {
+ const r = await memberApi.startPhoneVerification(phoneTarget);
+ setPhoneChallenge(r.challenge_id);
+ setMsg('簡訊驗證碼已送出(開發環境請查 Redis / mock)');
+ } catch (e) {
+ showErr(e);
+ }
+ };
+
+ const confirmPhone = async (e: FormEvent) => {
+ e.preventDefault();
+ setError('');
+ try {
+ await memberApi.confirmPhoneVerification(phoneChallenge, phoneCode);
+ setMsg('商業手機已驗證');
+ } catch (e) {
+ showErr(e);
+ }
+ };
+
+ const startTotp = async () => {
+ setError('');
+ setEnrollInfo(null);
+ try {
+ const r = await memberApi.startTOTPEnroll();
+ setEnrollInfo(r);
+ setMsg('請用 Authenticator 掃描 QR Code,再輸入 6 位驗證碼完成綁定');
+ } catch (e) {
+ showErr(e);
+ }
+ };
+
+ const confirmTotp = async (e: FormEvent) => {
+ e.preventDefault();
+ setError('');
+ try {
+ const r = await memberApi.confirmTOTPEnroll(totpCode);
+ setBackupCodes(r.backup_codes);
+ setMsg('TOTP 已啟用,請妥善保存備援碼');
+ setEnrollInfo(null);
+ setTotpCode('');
+ await reload();
+ } catch (e) {
+ showErr(e);
+ }
+ };
+
+ const disableTotp = async () => {
+ if (!confirm('確定停用 TOTP?')) return;
+ setError('');
+ try {
+ await memberApi.disableTOTP();
+ setEnrollInfo(null);
+ setBackupCodes([]);
+ await reload();
+ setMsg('TOTP 已停用');
+ } catch (e) {
+ showErr(e);
+ }
+ };
+
+ return (
+
+
安全設定
+ {msg &&
{msg}
}
+ {error &&
{error}
}
+
+
+ 商業聯絡 Email
+
+ 註冊時驗證的是登入信箱(ZITADEL);此處為業務聯絡信箱(通知、帳務等)。
+ 若與註冊信箱相同,完成註冊 OTP 後會自動標為已驗證。
+
+ {me?.business_email_verified ? (
+
+ 已驗證:{me.business_email || me.zitadel_email}
+
+ ) : (
+ <>
+ {me?.zitadel_email && (
+
+ 登入信箱:{me.zitadel_email}
+ {me.business_email && me.business_email !== me.zitadel_email
+ ? ` · 目前商業信箱:${me.business_email}(未驗證)`
+ : ''}
+
+ )}
+
+ {emailChallenge && (
+
+ )}
+ >
+ )}
+
+
+
+ 商業手機驗證
+
+ {phoneChallenge && (
+
+ )}
+
+
+
+ 雙因素驗證 (TOTP)
+
+ 狀態:{totp?.enrolled ? '已啟用' : '未啟用'}
+ {totp?.enrolled &&
+ ` · 備援碼剩餘 ${totp.backup_codes_remaining} 組`}
+
+ {!totp?.enrolled ? (
+ <>
+
+ {enrollInfo && (
+
+ )}
+ {enrollInfo && (
+
+ )}
+ >
+ ) : (
+
+ )}
+ {backupCodes.length > 0 && (
+ {backupCodes.join('\n')}
+ )}
+
+
+ );
+}
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 0000000..7f42e5f
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000..d3c52ea
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "module": "esnext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..8aef668
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 5173,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8888',
+ changeOrigin: true,
+ },
+ },
+ },
+});
diff --git a/internal/library/zitadel/client.go b/internal/library/zitadel/client.go
index d60c113..6b40dfb 100644
--- a/internal/library/zitadel/client.go
+++ b/internal/library/zitadel/client.go
@@ -163,22 +163,31 @@ func (c *Client) DeactivateUser(ctx context.Context, userID string) error {
return c.doJSON(ctx, http.MethodPost, c.apiBase+"/v2/users/"+url.PathEscape(userID)+"/deactivate", c.serviceAuth(), map[string]any{}, http.StatusOK, nil)
}
-// TokenResult holds OAuth tokens from a successful password grant.
+// TokenResult holds OAuth tokens from a successful password grant, or session-verified
+// identity fields when OAuth ROPG is not configured (ZITADEL v2 default).
type TokenResult struct {
AccessToken string
IDToken string
ExpiresIn int
TokenType string
+ // Subject and Email are set by session-based verification (no OAuth tokens).
+ Subject string
+ Email string
}
-// VerifyPassword checks credentials using the OAuth2 resource-owner password grant.
+// VerifyPassword checks email/password credentials. Uses OAuth2 ROPG when OAuthClientID
+// and OAuthClientSecret are configured; otherwise uses ZITADEL v2 Sessions API (PAT).
func (c *Client) VerifyPassword(ctx context.Context, username, password string) (*TokenResult, error) {
if c == nil {
return nil, ErrNotConfigured
}
- if c.conf.OAuthClientID == "" || c.conf.OAuthClientSecret == "" {
- return nil, fmt.Errorf("zitadel: oauth client credentials are required for password verification")
+ if c.conf.OAuthClientID != "" && c.conf.OAuthClientSecret != "" {
+ return c.verifyPasswordROPG(ctx, username, password)
}
+ return c.verifyPasswordSession(ctx, username, password)
+}
+
+func (c *Client) verifyPasswordROPG(ctx context.Context, username, password string) (*TokenResult, error) {
form := url.Values{}
form.Set("grant_type", "password")
form.Set("client_id", c.conf.OAuthClientID)
diff --git a/internal/library/zitadel/client_test.go b/internal/library/zitadel/client_test.go
index 4b11e7f..e8c6040 100644
--- a/internal/library/zitadel/client_test.go
+++ b/internal/library/zitadel/client_test.go
@@ -119,6 +119,33 @@ func TestVerifyPassword(t *testing.T) {
require.ErrorIs(t, err, zitadel.ErrInvalidCredentials)
}
+func TestVerifyPasswordSession(t *testing.T) {
+ t.Parallel()
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ switch {
+ case r.Method == http.MethodPost && r.URL.Path == "/v2/sessions":
+ _, _ = w.Write([]byte(`{"sessionId":"sess-1"}`))
+ case r.Method == http.MethodPatch && r.URL.Path == "/v2/sessions/sess-1":
+ _, _ = w.Write([]byte(`{"sessionToken":"tok"}`))
+ case r.Method == http.MethodGet && r.URL.Path == "/v2/sessions/sess-1":
+ _, _ = w.Write([]byte(`{"session":{"factors":{"user":{"id":"user-42","loginName":"alice@example.com"},"password":{"verifiedAt":"2026-01-01T00:00:00Z"}}}}`))
+ default:
+ http.NotFound(w, r)
+ }
+ }))
+ t.Cleanup(srv.Close)
+
+ c, err := zitadel.NewClient(zitadel.Conf{Issuer: srv.URL, APIBase: srv.URL, ServiceUserToken: testPAT})
+ require.NoError(t, err)
+
+ tok, err := c.VerifyPassword(context.Background(), "alice@example.com", "ok")
+ require.NoError(t, err)
+ require.Equal(t, "user-42", tok.Subject)
+ require.Equal(t, "alice@example.com", tok.Email)
+ require.Empty(t, tok.AccessToken)
+}
+
func TestNewClientDisabledWhenIssuerEmpty(t *testing.T) {
t.Parallel()
c, err := zitadel.NewClient(zitadel.Conf{})
diff --git a/internal/library/zitadel/session.go b/internal/library/zitadel/session.go
new file mode 100644
index 0000000..2711816
--- /dev/null
+++ b/internal/library/zitadel/session.go
@@ -0,0 +1,127 @@
+package zitadel
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+)
+
+// verifyPasswordSession checks credentials via ZITADEL v2 Sessions API (PAT-backed).
+// Used when OAuth resource-owner password grant is unavailable (default in ZITADEL v2).
+func (c *Client) verifyPasswordSession(ctx context.Context, loginName, password string) (*TokenResult, error) {
+ if c.conf.ServiceUserToken == "" {
+ return nil, fmt.Errorf("zitadel: service user token is required for session password verification")
+ }
+ loginName = strings.TrimSpace(loginName)
+ if loginName == "" || password == "" {
+ return nil, ErrInvalidCredentials
+ }
+
+ var created struct {
+ SessionID string `json:"sessionId"`
+ }
+ if err := c.doSessionJSON(ctx, http.MethodPost, c.apiBase+"/v2/sessions", map[string]any{
+ "checks": map[string]any{
+ "user": map[string]any{"loginName": loginName},
+ },
+ }, &created); err != nil {
+ if isSessionUserNotFound(err) {
+ return nil, ErrInvalidCredentials
+ }
+ return nil, err
+ }
+ if created.SessionID == "" {
+ return nil, fmt.Errorf("zitadel: create session: empty session id")
+ }
+
+ if err := c.doSessionJSON(ctx, http.MethodPatch, c.apiBase+"/v2/sessions/"+created.SessionID, map[string]any{
+ "checks": map[string]any{
+ "password": map[string]any{"password": password},
+ },
+ }, nil); err != nil {
+ if isSessionPasswordInvalid(err) {
+ return nil, ErrInvalidCredentials
+ }
+ return nil, err
+ }
+
+ var got struct {
+ Session struct {
+ Factors struct {
+ User struct {
+ ID string `json:"id"`
+ LoginName string `json:"loginName"`
+ } `json:"user"`
+ Password struct {
+ VerifiedAt string `json:"verifiedAt"`
+ } `json:"password"`
+ } `json:"factors"`
+ } `json:"session"`
+ }
+ if err := c.doSessionJSON(ctx, http.MethodGet, c.apiBase+"/v2/sessions/"+created.SessionID, nil, &got); err != nil {
+ return nil, err
+ }
+ if got.Session.Factors.Password.VerifiedAt == "" || got.Session.Factors.User.ID == "" {
+ return nil, ErrInvalidCredentials
+ }
+ return &TokenResult{
+ Subject: got.Session.Factors.User.ID,
+ Email: got.Session.Factors.User.LoginName,
+ }, nil
+}
+
+func (c *Client) doSessionJSON(ctx context.Context, method, endpoint string, body any, out any) error {
+ var r io.Reader
+ if body != nil {
+ raw, err := json.Marshal(body)
+ if err != nil {
+ return fmt.Errorf("zitadel: marshal request: %w", err)
+ }
+ r = bytes.NewReader(raw)
+ }
+ req, err := http.NewRequestWithContext(ctx, method, endpoint, r)
+ if err != nil {
+ return fmt.Errorf("zitadel: new request: %w", err)
+ }
+ if body != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("Authorization", c.serviceAuth())
+
+ resp, err := c.http.Do(req)
+ if err != nil {
+ return fmt.Errorf("zitadel: %s %s: %w", method, endpoint, err)
+ }
+ defer resp.Body.Close()
+
+ raw, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("zitadel: read response body: %w", err)
+ }
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return fmt.Errorf("zitadel: %s %s: status %d: %s", method, endpoint, resp.StatusCode, truncateBody(raw))
+ }
+ if out != nil && len(raw) > 0 {
+ if err := json.Unmarshal(raw, out); err != nil {
+ return fmt.Errorf("zitadel: decode response: %w", err)
+ }
+ }
+ return nil
+}
+
+func isSessionUserNotFound(err error) bool {
+ return err != nil && strings.Contains(err.Error(), "status 404")
+}
+
+func isSessionPasswordInvalid(err error) bool {
+ if err == nil {
+ return false
+ }
+ s := err.Error()
+ return strings.Contains(s, "status 400") && strings.Contains(s, "Password is invalid")
+}
diff --git a/internal/logic/auth/login_helper.go b/internal/logic/auth/login_helper.go
index 218511f..5941f2d 100644
--- a/internal/logic/auth/login_helper.go
+++ b/internal/logic/auth/login_helper.go
@@ -60,6 +60,12 @@ func zitadelIdentityFromToken(ctx context.Context, client *zitadel.Client, tok *
if tok == nil {
return nil, errb.SvcThirdParty("empty token result")
}
+ if tok.Subject != "" {
+ return &zitadel.IDTokenClaims{
+ Sub: tok.Subject,
+ Email: tok.Email,
+ }, nil
+ }
if tok.IDToken != "" {
claims, err := zitadel.ParseIDTokenClaims(tok.IDToken)
if err != nil {
diff --git a/internal/logic/auth/register_confirm_logic.go b/internal/logic/auth/register_confirm_logic.go
index 38a7b2a..9e8276a 100644
--- a/internal/logic/auth/register_confirm_logic.go
+++ b/internal/logic/auth/register_confirm_logic.go
@@ -2,6 +2,7 @@ package auth
import (
"context"
+ "strings"
dommember "gateway/internal/model/member/domain/usecase"
"gateway/internal/svc"
@@ -62,5 +63,13 @@ func (l *RegisterConfirmLogic) RegisterConfirm(req *types.RegisterConfirmReq) (*
return nil, err
}
+ // Email 註冊 OTP 已證明使用者擁有該信箱;同步為已驗證的商業聯絡 Email,
+ // 避免完成註冊後仍要在安全設定重驗同一個地址。
+ if email := strings.TrimSpace(strings.ToLower(ch.Target)); email != "" && l.svcCtx.MemberProfile != nil {
+ if err := l.svcCtx.MemberProfile.SetBusinessEmailVerified(l.ctx, tenant.TenantID, ch.UID, email); err != nil {
+ return nil, err
+ }
+ }
+
return issueAuthToken(l.ctx, l.svcCtx, tenant.TenantID, ch.UID)
}