diff --git a/.env.sample b/.env.sample
index 2e393b60..67ad0f1e 100644
--- a/.env.sample
+++ b/.env.sample
@@ -11,6 +11,8 @@ DB_PORT=5432
CLICKHOUSE_PORT_HTTP=8123
CLICKHOUSE_PORT_NATIVE=9000
BACKEND_PORT=3000
+ADMINFRONT_PORT=5173
+DEVFRONT_PORT=5174
USERFRONT_PORT=5000
# --- Database Credentials (PostgreSQL) ---
@@ -68,7 +70,7 @@ KETO_DB=ory_keto
# Ory Kratos Configuration
KRATOS_VERSION=v25.4.0-distroless
KRATOS_PUBLIC_PORT=4433
-KRATOS_ADMIN_PORT=4434
+KRATOS_ADMINFRONT_PORT=4434
KRATOS_UI_NODE_VERSION=v25.4.0
KRATOS_UI_PORT=4455
@@ -76,7 +78,7 @@ KRATOS_UI_PORT=4455
# Ory Hydra Configuration
HYDRA_VERSION=v25.4.0-distroless
HYDRA_PUBLIC_PORT=4441
-HYDRA_ADMIN_PORT=4445
+HYDRA_ADMINFRONT_PORT=4445
# Ory Keto Configuration
KETO_VERSION=v25.4.0-distroless
@@ -88,6 +90,7 @@ ORY_SDK_URL=http://kratos:4433
KRATOS_PUBLIC_URL=http://kratos:4433
KRATOS_ADMIN_URL=http://kratos:4434
HYDRA_ADMIN_URL=http://hydra:4445
+HYDRA_PUBLIC_URL=http://hydra:4444
JWKS_URL=http://oathkeeper:4456/.well-known/jwks.json
# Kratos Selfservice UI required secrets (local only)
diff --git a/README.md b/README.md
index 15ff9e62..ce939770 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,7 @@ Kratos가 사용자 SoT이며 Hydra는 순수 OIDC 토큰 엔진입니다. Magic
IDP_PROVIDER=ory,descope
KRATOS_ADMIN_URL=http://kratos:4434
HYDRA_ADMIN_URL=http://hydra:4445
+ HYDRA_PUBLIC_URL=http://hydra:4444
```
### 전체 스택 실행 (Running the Stack)
@@ -124,6 +125,35 @@ docker compose -f docker-compose.yaml up -d
- **Hydra Public**: http://localhost:4444
- **Kratos UI**: http://localhost:4455
+### MCP 서버 (Hydra/Kratos)
+MCP 서버는 기존 Hydra/Kratos에 연결하며 별도 Ory 스택이나 포트를 추가로 띄우지 않습니다.
+프로덕션에서는 실행하지 않도록 `mcp` 프로파일을 로컬에서만 켜세요.
+
+```bash
+docker compose -f compose.ory.yaml --profile mcp up -d hydra-mcp-server kratos-mcp-server
+```
+
+- MCP 서버는 stdio 기반이라 외부 포트를 열지 않습니다.
+- MCP 클라이언트에서 `npx`로 실행하는 설정 예시입니다.
+- `hydra-mcp`는 첫 실행 시 캐시 디렉터리에 의존성을 자동 설치합니다(수동 `npm install` 불필요).
+
+```toml
+[mcp_servers.kratos-mcp]
+command = "npx"
+args = ["-y", "mcp-ory-kratos"]
+
+[mcp_servers.kratos-mcp.env]
+KRATOS_ADMIN_URL = "http://localhost:4434"
+
+[mcp_servers.hydra-mcp]
+command = "npx"
+args = ["-y", "/home/lectom/repos/baron-sso/mcp/hydra-mcp"]
+
+[mcp_servers.hydra-mcp.env]
+HYDRA_PUBLIC_URL = "http://localhost:4441"
+HYDRA_ADMIN_URL = "http://localhost:4445"
+```
+
### 로컬 개발 (Manual)
Docker 없이 코드를 수정하며 개발하려면:
@@ -141,6 +171,20 @@ flutter pub get
flutter run -d chrome
```
+**adminfront:**
+```bash
+cd adminfront
+npm install
+npm run dev
+```
+
+**devfront:**
+```bash
+cd devfront
+npm install
+npm run dev
+```
+
---
## 📂 프로젝트 구조 (Project Structure)
diff --git a/adminfront/Dockerfile b/adminfront/Dockerfile
new file mode 100644
index 00000000..31e5a2b1
--- /dev/null
+++ b/adminfront/Dockerfile
@@ -0,0 +1,25 @@
+FROM node:lts
+
+WORKDIR /app
+
+# 패키지 정보 복사 및 의존성 설치
+COPY package*.json ./
+RUN npm ci
+
+# 프로덕션 서빙을 위한 serve 패키지 글로벌 설치
+RUN npm install -g serve
+
+# 소스 코드 복사
+COPY . .
+
+# Vite 기본 포트
+EXPOSE 5173
+
+# 실행 스크립트: APP_ENV에 따라 개발 서버 또는 빌드 후 서빙
+CMD sh -c "if [ \"$APP_ENV\" = 'production' ]; then \
+ echo 'Running in production mode...'; \
+ npm run build && serve -s dist -l 5173; \
+ else \
+ echo 'Running in development mode...'; \
+ npm run dev -- --host 0.0.0.0; \
+ fi"
diff --git a/adminfront/README.md b/adminfront/README.md
index b184572e..ec7cb49c 100644
--- a/adminfront/README.md
+++ b/adminfront/README.md
@@ -1,6 +1,6 @@
# Admin Front (React 19 + Vite)
-관리자 포털을 위한 React/Vite 기반 SPA입니다. 이슈 #60 스펙을 바탕으로 라우팅, 서버 상태, 스타일 토큰을 세팅했고 특정 벤더에 종속되지 않는 IDP 연동 훅 포인트를 남겨두었습니다.
+관리자 포털을 위한 React/Vite 기반 웹입니다. 이슈 #60 스펙을 바탕으로 라우팅, 서버 상태, 스타일 토큰을 세팅했고 특정 벤더에 종속되지 않는 IDP 연동 훅 포인트를 남겨두었습니다.
## 주요 스택
- React 19, Vite 7, TypeScript(strict)
diff --git a/adminfront/index.html b/adminfront/index.html
index 544bf2dd..7e8d7020 100644
--- a/adminfront/index.html
+++ b/adminfront/index.html
@@ -4,7 +4,7 @@
-
Loading audit logs...
;
+ }
+
+ if (error) {
+ const errMsg =
+ (error as any).response?.data?.error || (error as Error).message;
+ return (
+
@@ -84,28 +83,42 @@ function AuditLogsPage() {
))}
- {auditRows.map((row) => (
-
-
{row.action}
-
{row.tenant}
-
{row.actor}
-
-
- {row.result}
-
- {row.ts}
-
+ {logs.length === 0 ? (
+
+ No audit logs found.
- ))}
+ ) : (
+ logs.map((row, idx) => (
+
+
{row.event_type}
+
+ {/* Tenant info not yet in basic schema, show generic or details snippet */}
+ Tenant-?
+
+
+ {row.user_id}
+
+
+
+ {row.status}
+
+
+ {new Date(row.timestamp).toLocaleString()}
+
+
+
+ ))
+ )}
diff --git a/adminfront/src/features/clients/ClientsPage.tsx b/adminfront/src/features/clients/ClientsPage.tsx
deleted file mode 100644
index c3202799..00000000
--- a/adminfront/src/features/clients/ClientsPage.tsx
+++ /dev/null
@@ -1,388 +0,0 @@
-import {
- Activity,
- BookOpenText,
- Copy,
- Laptop,
- Plus,
- Search,
- ServerCog,
- ShieldHalf,
-} from "lucide-react";
-import { Link } from "react-router-dom";
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
-} from "../../components/ui/avatar";
-import { Badge } from "../../components/ui/badge";
-import { Button } from "../../components/ui/button";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "../../components/ui/card";
-import { Input } from "../../components/ui/input";
-import { Separator } from "../../components/ui/separator";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "../../components/ui/table";
-import { cn } from "../../lib/utils";
-
-const clients = [
- {
- name: "Customer Portal",
- type: "Confidential",
- clientId: "cli_481...8k2",
- status: "Active",
- created: "2023-10-12",
- icon:
,
- },
- {
- name: "Mobile App (iOS)",
- type: "Public",
- clientId: "cli_922...4m1",
- status: "Inactive",
- created: "2023-11-04",
- icon:
,
- },
- {
- name: "Internal Analytics",
- type: "Confidential",
- clientId: "cli_773...5z9",
- status: "Active",
- created: "2024-01-12",
- icon:
,
- },
-];
-
-const stats = [
- { label: "총 클라이언트", value: "24", delta: "+2%", tone: "up" as const },
- {
- label: "활성 세션",
- value: "1,204",
- delta: "-5%",
- tone: "down" as const,
- },
- {
- label: "인증 실패 (24h)",
- value: "12",
- delta: "Stable",
- tone: "stable" as const,
- },
-];
-
-function ClientsPage() {
- return (
-
-
-
-
-
-
- RP registry
-
-
- Relying Parties
-
-
- OIDC 클라이언트, 인증 방식, 리다이렉트 URI,
- 비밀키 재발행을 감사 로그와 함께 관리합니다.
-
-
-
-
- 비밀키 재발행
-
-
- 새 클라이언트
-
-
-
-
-
-
-
-
-
- 테넌트: 선택됨
- 관리자 세션
-
-
-
-
-
- {stats.map((item) => (
-
-
-
- {item.label}
-
-
-
- {item.value}
-
-
- {item.delta}
-
-
-
-
- ))}
-
-
-
-
-
-
-
-
- 클라이언트 목록
-
-
-
- 비밀키 재발행
-
-
- 새 클라이언트
-
-
-
-
-
-
-
-
- 애플리케이션
- Client ID
- 유형
- 상태
- 생성일
-
- 액션
-
-
-
-
- {clients.map((client) => (
-
-
-
-
- {client.icon}
-
-
-
- {client.name}
-
-
- Tenant-scoped
-
-
-
-
-
-
-
- {client.clientId}
-
-
-
-
-
-
-
-
- {client.type === "Confidential"
- ? "기밀(Confidential)"
- : "Public"}
-
-
-
-
-
-
- {client.status === "Active"
- ? "활성"
- : "비활성"}
-
-
-
-
- {client.created}
-
-
-
-
-
- ))}
-
-
-
-
Showing 1 to 3 of 24 clients
-
-
- Previous
-
-
- Next
-
-
-
-
-
-
-
-
-
-
- Need help with OIDC configuration?
-
-
- Developer guides for Confidential/Public clients,
- redirect URIs, and auth methods.
-
-
-
-
-
-
-
-
-
- Docs & Examples
-
-
- Includes PKCE, client_secret_basic, redirect
- URI validation tips.
-
-
-
- View guides
-
-
-
-
-
-
- Owner
-
- Tenant admin on-call
-
-
-
-
-
- AR
-
-
-
AI Admin Bot
-
- admin@brsw.kr
-
-
-
-
-
- Role: Tenant Admin
- Scope: TENANT-12
-
-
-
-
-
- );
-}
-
-export default ClientsPage;
diff --git a/adminfront/src/lib/adminApi.ts b/adminfront/src/lib/adminApi.ts
new file mode 100644
index 00000000..4f6e0663
--- /dev/null
+++ b/adminfront/src/lib/adminApi.ts
@@ -0,0 +1,25 @@
+import apiClient from "./apiClient";
+
+export type AuditLog = {
+ timestamp: string;
+ user_id: string;
+ event_type: string;
+ status: string;
+ ip_address: string;
+ user_agent: string;
+ device_id?: string;
+ details?: string;
+};
+
+export type AuditLogListResponse = {
+ items: AuditLog[];
+ limit: number;
+ offset: number;
+};
+
+export async function fetchAuditLogs(limit = 50, offset = 0) {
+ const { data } = await apiClient.get
("/v1/audit", {
+ params: { limit, offset },
+ });
+ return data;
+}
diff --git a/adminfront/src/lib/apiClient.ts b/adminfront/src/lib/apiClient.ts
index 80927a89..59ea9cc3 100644
--- a/adminfront/src/lib/apiClient.ts
+++ b/adminfront/src/lib/apiClient.ts
@@ -1,7 +1,7 @@
import axios from "axios";
const apiClient = axios.create({
- baseURL: import.meta.env.VITE_ADMIN_API_BASE ?? "/api/admin",
+ baseURL: import.meta.env.VITE_ADMIN_API_BASE ?? "/api",
});
apiClient.interceptors.request.use((config) => {
diff --git a/adminfront/tests/example.spec.ts b/adminfront/tests/example.spec.ts
new file mode 100644
index 00000000..d16766d9
--- /dev/null
+++ b/adminfront/tests/example.spec.ts
@@ -0,0 +1,8 @@
+import { expect, test } from "@playwright/test";
+
+test("has title", async ({ page }) => {
+ await page.goto("/");
+
+ // Expect a title "to contain" a substring.
+ await expect(page).toHaveTitle(/바론 어드민 서비스/);
+});
diff --git a/adminfront/vite.config.ts b/adminfront/vite.config.ts
index f16e2be3..01be851f 100644
--- a/adminfront/vite.config.ts
+++ b/adminfront/vite.config.ts
@@ -4,4 +4,16 @@ import { defineConfig } from "vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
+ server: {
+ host: "0.0.0.0",
+ proxy: {
+ "/api": {
+ target: process.env.API_PROXY_TARGET || "http://baron_backend:3000",
+ changeOrigin: true,
+ },
+ },
+ },
+ esbuild: {
+ drop: process.env.APP_ENV === "production" ? ["console", "debugger"] : [],
+ },
});
diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go
index 78e1a238..ffe8fde7 100644
--- a/backend/cmd/server/main.go
+++ b/backend/cmd/server/main.go
@@ -8,6 +8,7 @@ import (
"baron-sso-backend/internal/repository"
"baron-sso-backend/internal/service"
"baron-sso-backend/internal/validator"
+ "errors"
"fmt"
"log"
"log/slog"
@@ -160,11 +161,45 @@ func main() {
auditHandler := handler.NewAuditHandler(auditRepo)
authHandler := handler.NewAuthHandler(redisService, idpProvider)
adminHandler := handler.NewAdminHandler()
+ devHandler := handler.NewDevHandler()
// 3. Initialize Fiber
+ appEnv := getEnv("APP_ENV", "dev")
app := fiber.New(fiber.Config{
AppName: "Baron SSO Backend",
DisableStartupMessage: true, // Clean logs
+ // Global Error Handler for Production Masking
+ ErrorHandler: func(c *fiber.Ctx, err error) error {
+ // Default status code
+ code := fiber.StatusInternalServerError
+
+ // Check if it's a known fiber.Error
+ var e *fiber.Error
+ if errors.As(err, &e) {
+ code = e.Code
+ }
+
+ // In production or stage, mask detailed 500+ errors
+ if appEnv == "production" || appEnv == "stage" {
+ if code >= 500 {
+ // Log the actual error for developers
+ slog.Error("Internal Server Error",
+ "error", err.Error(),
+ "path", c.Path(),
+ "method", c.Method(),
+ )
+ // Return masked message
+ return c.Status(code).JSON(fiber.Map{
+ "error": "Internal Server Error",
+ })
+ }
+ }
+
+ // For development or non-500 errors, return the actual error message
+ return c.Status(code).JSON(fiber.Map{
+ "error": err.Error(),
+ })
+ },
})
// Middleware
@@ -281,6 +316,7 @@ func main() {
// API Group
api := app.Group("/api/v1")
api.Post("/audit", auditHandler.CreateLog)
+ api.Get("/audit", auditHandler.ListLogs)
// Auth Proxy Routes
auth := api.Group("/auth")
@@ -320,6 +356,14 @@ func main() {
admin := api.Group("/admin")
admin.Get("/check", adminHandler.CheckAuth)
+ // 개발자 포털 라우트 (RP/Consent 관리)
+ dev := api.Group("/dev")
+ dev.Get("/clients", devHandler.ListClients)
+ dev.Get("/clients/:id", devHandler.GetClient)
+ dev.Patch("/clients/:id/status", devHandler.UpdateClientStatus)
+ dev.Get("/consents", devHandler.ListConsents)
+ dev.Delete("/consents", devHandler.RevokeConsents)
+
// Webhook for Descope Generic SMS Gateway
auth.Post("/webhooks/descope-sms", authHandler.HandleDescopeSmsRelay)
diff --git a/backend/internal/domain/models.go b/backend/internal/domain/models.go
index d1962ceb..ac587945 100644
--- a/backend/internal/domain/models.go
+++ b/backend/internal/domain/models.go
@@ -1,6 +1,7 @@
package domain
import (
+ "context"
"time"
)
@@ -19,5 +20,6 @@ type AuditLog struct {
// AuditRepository defines interface for storing logs
type AuditRepository interface {
Create(log *AuditLog) error
- // FindAll(filter Filter) ([]*AuditLog, error) // Future scope
+ FindAll(ctx context.Context, limit, offset int) ([]AuditLog, error)
+ Ping(ctx context.Context) error
}
diff --git a/backend/internal/handler/audit_handler.go b/backend/internal/handler/audit_handler.go
index a7a4ad5b..5d4484d0 100644
--- a/backend/internal/handler/audit_handler.go
+++ b/backend/internal/handler/audit_handler.go
@@ -46,3 +46,22 @@ func (h *AuditHandler) CreateLog(c *fiber.Ctx) error {
"message": "Audit log saved",
})
}
+
+// ListLogs handles GET /api/v1/audit
+func (h *AuditHandler) ListLogs(c *fiber.Ctx) error {
+ limit := c.QueryInt("limit", 50)
+ offset := c.QueryInt("offset", 0)
+
+ logs, err := h.repo.FindAll(c.Context(), limit, offset)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+ "error": "Failed to retrieve audit logs",
+ })
+ }
+
+ return c.JSON(fiber.Map{
+ "items": logs,
+ "limit": limit,
+ "offset": offset,
+ })
+}
diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go
new file mode 100644
index 00000000..6fb14b3f
--- /dev/null
+++ b/backend/internal/handler/dev_handler.go
@@ -0,0 +1,241 @@
+package handler
+
+import (
+ "baron-sso-backend/internal/service"
+ "errors"
+ "strings"
+ "time"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+type DevHandler struct {
+ Hydra *service.HydraAdminService
+}
+
+func NewDevHandler() *DevHandler {
+ return &DevHandler{
+ Hydra: service.NewHydraAdminService(),
+ }
+}
+
+type clientSummary struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Status string `json:"status"`
+ CreatedAt *time.Time `json:"createdAt,omitempty"`
+ RedirectURIs []string `json:"redirectUris"`
+ Scopes []string `json:"scopes"`
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+}
+
+type clientListResponse struct {
+ Items []clientSummary `json:"items"`
+ Limit int `json:"limit"`
+ Offset int `json:"offset"`
+}
+
+type clientDetailResponse struct {
+ Client clientSummary `json:"client"`
+ Endpoints clientEndpoints `json:"endpoints"`
+}
+
+type clientEndpoints struct {
+ Discovery string `json:"discovery"`
+ Issuer string `json:"issuer"`
+ Authorization string `json:"authorization"`
+ Token string `json:"token"`
+ UserInfo string `json:"userinfo"`
+}
+
+type consentSummary struct {
+ Subject string `json:"subject"`
+ ClientID string `json:"clientId"`
+ ClientName string `json:"clientName,omitempty"`
+ GrantedScopes []string `json:"grantedScopes"`
+ AuthenticatedAt string `json:"authenticatedAt,omitempty"`
+}
+
+type consentListResponse struct {
+ Items []consentSummary `json:"items"`
+}
+
+func (h *DevHandler) ListClients(c *fiber.Ctx) error {
+ limit := c.QueryInt("limit", 50)
+ offset := c.QueryInt("offset", 0)
+ if limit <= 0 {
+ limit = 50
+ }
+ if offset < 0 {
+ offset = 0
+ }
+
+ clients, err := h.Hydra.ListClients(c.Context(), limit, offset)
+ if err != nil {
+ if errors.Is(err, service.ErrHydraNotFound) {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "clients not found"})
+ }
+ errMsg := err.Error()
+ if strings.Contains(errMsg, "connection refused") || strings.Contains(errMsg, "dial tcp") {
+ return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
+ "error": "Hydra service is unavailable. Please check if Ory Hydra is running.",
+ })
+ }
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": errMsg})
+ }
+
+ items := make([]clientSummary, 0, len(clients))
+ for _, client := range clients {
+ items = append(items, mapClientSummary(client))
+ }
+
+ return c.JSON(clientListResponse{
+ Items: items,
+ Limit: limit,
+ Offset: offset,
+ })
+}
+
+func (h *DevHandler) GetClient(c *fiber.Ctx) error {
+ clientID := c.Params("id")
+ if clientID == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
+ }
+
+ client, err := h.Hydra.GetClient(c.Context(), clientID)
+ if err != nil {
+ if errors.Is(err, service.ErrHydraNotFound) {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
+ }
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
+ }
+
+ summary := mapClientSummary(*client)
+ return c.JSON(clientDetailResponse{
+ Client: summary,
+ Endpoints: clientEndpoints{
+ Discovery: strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
+ Issuer: h.Hydra.PublicURL,
+ Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
+ Token: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
+ UserInfo: strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
+ },
+ })
+}
+
+func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
+ clientID := c.Params("id")
+ if clientID == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
+ }
+
+ var req struct {
+ Status string `json:"status"`
+ }
+ if err := c.BodyParser(&req); err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
+ }
+
+ status := strings.ToLower(strings.TrimSpace(req.Status))
+ if status != "active" && status != "inactive" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"})
+ }
+
+ updated, err := h.Hydra.PatchClientStatus(c.Context(), clientID, status)
+ if err != nil {
+ if errors.Is(err, service.ErrHydraNotFound) {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
+ }
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
+ }
+
+ summary := mapClientSummary(*updated)
+ return c.JSON(clientDetailResponse{
+ Client: summary,
+ Endpoints: clientEndpoints{
+ Discovery: strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration",
+ Issuer: h.Hydra.PublicURL,
+ Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth",
+ Token: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token",
+ UserInfo: strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo",
+ },
+ })
+}
+
+func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
+ subject := strings.TrimSpace(c.Query("subject"))
+ if subject == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "subject is required"})
+ }
+ clientID := strings.TrimSpace(c.Query("client_id"))
+
+ sessions, err := h.Hydra.ListConsentSessions(c.Context(), subject, clientID)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
+ }
+
+ items := make([]consentSummary, 0, len(sessions))
+ for _, session := range sessions {
+ authAt := ""
+ if session.AuthenticatedAt != nil {
+ authAt = session.AuthenticatedAt.Format(time.RFC3339)
+ } else if session.RequestedAt != nil {
+ authAt = session.RequestedAt.Format(time.RFC3339)
+ }
+ items = append(items, consentSummary{
+ Subject: session.Subject,
+ ClientID: session.Client.ClientID,
+ ClientName: session.Client.ClientName,
+ GrantedScopes: session.GrantedScope,
+ AuthenticatedAt: authAt,
+ })
+ }
+
+ return c.JSON(consentListResponse{Items: items})
+}
+
+func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error {
+ subject := strings.TrimSpace(c.Query("subject"))
+ if subject == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "subject is required"})
+ }
+ clientID := strings.TrimSpace(c.Query("client_id"))
+
+ if err := h.Hydra.RevokeConsentSessions(c.Context(), subject, clientID); err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
+ }
+
+ return c.SendStatus(fiber.StatusNoContent)
+}
+
+func mapClientSummary(client service.HydraClient) clientSummary {
+ status := "active"
+ if client.Metadata != nil {
+ if value, ok := client.Metadata["status"].(string); ok && strings.ToLower(value) == "inactive" {
+ status = "inactive"
+ }
+ }
+
+ clientType := "confidential"
+ if strings.EqualFold(client.TokenEndpointAuthMethod, "none") {
+ clientType = "public"
+ }
+
+ name := strings.TrimSpace(client.ClientName)
+ if name == "" {
+ name = client.ClientID
+ }
+
+ scopes := strings.Fields(client.Scope)
+
+ return clientSummary{
+ ID: client.ClientID,
+ Name: name,
+ Type: clientType,
+ Status: status,
+ RedirectURIs: client.RedirectURIs,
+ Scopes: scopes,
+ Metadata: client.Metadata,
+ }
+}
diff --git a/backend/internal/repository/clickhouse_repo.go b/backend/internal/repository/clickhouse_repo.go
index e688c10d..360820b9 100644
--- a/backend/internal/repository/clickhouse_repo.go
+++ b/backend/internal/repository/clickhouse_repo.go
@@ -78,6 +78,46 @@ func (r *ClickHouseRepository) Create(log *domain.AuditLog) error {
)
}
+func (r *ClickHouseRepository) FindAll(ctx context.Context, limit, offset int) ([]domain.AuditLog, error) {
+ if limit <= 0 {
+ limit = 50
+ }
+ if offset < 0 {
+ offset = 0
+ }
+
+ query := `
+ SELECT timestamp, user_id, event_type, status, ip_address, user_agent, device_id, details
+ FROM audit_logs
+ ORDER BY timestamp DESC
+ LIMIT ? OFFSET ?
+ `
+ rows, err := r.conn.Query(ctx, query, limit, offset)
+ if err != nil {
+ return nil, fmt.Errorf("failed to query audit logs: %w", err)
+ }
+ defer rows.Close()
+
+ var logs []domain.AuditLog
+ for rows.Next() {
+ var log domain.AuditLog
+ if err := rows.Scan(
+ &log.Timestamp,
+ &log.UserID,
+ &log.EventType,
+ &log.Status,
+ &log.IPAddress,
+ &log.UserAgent,
+ &log.DeviceID,
+ &log.Details,
+ ); err != nil {
+ return nil, fmt.Errorf("failed to scan audit log: %w", err)
+ }
+ logs = append(logs, log)
+ }
+ return logs, nil
+}
+
func (r *ClickHouseRepository) Ping(ctx context.Context) error {
if r.conn == nil {
return fmt.Errorf("clickhouse connection is nil")
diff --git a/backend/internal/service/hydra_admin_service.go b/backend/internal/service/hydra_admin_service.go
new file mode 100644
index 00000000..c5373237
--- /dev/null
+++ b/backend/internal/service/hydra_admin_service.go
@@ -0,0 +1,265 @@
+package service
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+)
+
+var ErrHydraNotFound = errors.New("hydra admin: resource not found")
+
+// HydraAdminService는 Hydra Admin API 호출을 래핑합니다.
+type HydraAdminService struct {
+ AdminURL string
+ PublicURL string
+ HTTPClient *http.Client
+}
+
+type HydraClient struct {
+ ClientID string `json:"client_id"`
+ ClientName string `json:"client_name,omitempty"`
+ RedirectURIs []string `json:"redirect_uris,omitempty"`
+ GrantTypes []string `json:"grant_types,omitempty"`
+ ResponseTypes []string `json:"response_types,omitempty"`
+ Scope string `json:"scope,omitempty"`
+ TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+}
+
+type HydraConsentSession struct {
+ Subject string `json:"subject"`
+ GrantedScope []string `json:"granted_scope"`
+ GrantedAudience []string `json:"granted_audience,omitempty"`
+ Remember bool `json:"remember"`
+ AuthenticatedAt *time.Time `json:"authenticated_at,omitempty"`
+ RequestedAt *time.Time `json:"requested_at,omitempty"`
+ Client HydraClient `json:"client"`
+}
+
+func NewHydraAdminService() *HydraAdminService {
+ return &HydraAdminService{
+ AdminURL: getenv("HYDRA_ADMIN_URL", "http://hydra:4445"),
+ PublicURL: getenv("HYDRA_PUBLIC_URL", "http://hydra:4444"),
+ }
+}
+
+func (s *HydraAdminService) ListClients(ctx context.Context, limit, offset int) ([]HydraClient, error) {
+ endpoint, err := s.buildURL("/clients", map[string]int{
+ "limit": limit,
+ "offset": offset,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.httpClient().Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return nil, ErrHydraNotFound
+ }
+ if resp.StatusCode >= 300 {
+ body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
+ return nil, fmt.Errorf("hydra admin: list clients failed status=%d body=%s", resp.StatusCode, string(body))
+ }
+
+ var clients []HydraClient
+ if err := json.NewDecoder(resp.Body).Decode(&clients); err != nil {
+ return nil, fmt.Errorf("hydra admin: decode clients failed: %w", err)
+ }
+ return clients, nil
+}
+
+func (s *HydraAdminService) GetClient(ctx context.Context, clientID string) (*HydraClient, error) {
+ endpoint := fmt.Sprintf("%s/clients/%s", strings.TrimRight(s.AdminURL, "/"), url.PathEscape(clientID))
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.httpClient().Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return nil, ErrHydraNotFound
+ }
+ if resp.StatusCode >= 300 {
+ body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
+ return nil, fmt.Errorf("hydra admin: get client failed status=%d body=%s", resp.StatusCode, string(body))
+ }
+
+ var client HydraClient
+ if err := json.NewDecoder(resp.Body).Decode(&client); err != nil {
+ return nil, fmt.Errorf("hydra admin: decode client failed: %w", err)
+ }
+ return &client, nil
+}
+
+func (s *HydraAdminService) PatchClientStatus(ctx context.Context, clientID, status string) (*HydraClient, error) {
+ payload := map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "status": status,
+ },
+ }
+ body, _ := json.Marshal(payload)
+ endpoint := fmt.Sprintf("%s/clients/%s", strings.TrimRight(s.AdminURL, "/"), url.PathEscape(clientID))
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint, bytes.NewReader(body))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", "application/merge-patch+json")
+
+ resp, err := s.httpClient().Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return nil, ErrHydraNotFound
+ }
+ if resp.StatusCode >= 300 {
+ respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
+ return nil, fmt.Errorf("hydra admin: patch client failed status=%d body=%s", resp.StatusCode, string(respBody))
+ }
+
+ var updated HydraClient
+ if err := json.NewDecoder(resp.Body).Decode(&updated); err != nil {
+ return nil, fmt.Errorf("hydra admin: decode patched client failed: %w", err)
+ }
+ return &updated, nil
+}
+
+func (s *HydraAdminService) ListConsentSessions(ctx context.Context, subject, clientID string) ([]HydraConsentSession, error) {
+ params := map[string]string{
+ "subject": subject,
+ }
+ if clientID != "" {
+ params["client"] = clientID
+ }
+ endpoint, err := s.buildURLWithParams("/oauth2/auth/sessions/consent", params)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.httpClient().Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 300 {
+ body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
+ return nil, fmt.Errorf("hydra admin: list consent sessions failed status=%d body=%s", resp.StatusCode, string(body))
+ }
+
+ var sessions []HydraConsentSession
+ if err := json.NewDecoder(resp.Body).Decode(&sessions); err != nil {
+ return nil, fmt.Errorf("hydra admin: decode consent sessions failed: %w", err)
+ }
+ return sessions, nil
+}
+
+func (s *HydraAdminService) RevokeConsentSessions(ctx context.Context, subject, clientID string) error {
+ params := map[string]string{
+ "subject": subject,
+ }
+ if clientID != "" {
+ params["client"] = clientID
+ }
+ endpoint, err := s.buildURLWithParams("/oauth2/auth/sessions/consent", params)
+ if err != nil {
+ return err
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
+ if err != nil {
+ return err
+ }
+
+ resp, err := s.httpClient().Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 300 {
+ body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
+ return fmt.Errorf("hydra admin: revoke consent failed status=%d body=%s", resp.StatusCode, string(body))
+ }
+ return nil
+}
+
+func (s *HydraAdminService) httpClient() *http.Client {
+ if s.HTTPClient != nil {
+ return s.HTTPClient
+ }
+ return &http.Client{
+ Timeout: 10 * time.Second,
+ Transport: &http.Transport{
+ DialContext: (&net.Dialer{
+ Timeout: 5 * time.Second,
+ KeepAlive: 30 * time.Second,
+ }).DialContext,
+ TLSHandshakeTimeout: 5 * time.Second,
+ },
+ }
+}
+
+func (s *HydraAdminService) buildURL(path string, ints map[string]int) (string, error) {
+ base := strings.TrimRight(s.AdminURL, "/")
+ u, err := url.Parse(base + path)
+ if err != nil {
+ return "", err
+ }
+ q := u.Query()
+ for key, value := range ints {
+ if value > 0 {
+ q.Set(key, strconv.Itoa(value))
+ }
+ }
+ u.RawQuery = q.Encode()
+ return u.String(), nil
+}
+
+func (s *HydraAdminService) buildURLWithParams(path string, params map[string]string) (string, error) {
+ base := strings.TrimRight(s.AdminURL, "/")
+ u, err := url.Parse(base + path)
+ if err != nil {
+ return "", err
+ }
+ q := u.Query()
+ for key, value := range params {
+ if value != "" {
+ q.Set(key, value)
+ }
+ }
+ u.RawQuery = q.Encode()
+ return u.String(), nil
+}
diff --git a/compose.ory.yaml b/compose.ory.yaml
index 7bd8003f..f76ab284 100644
--- a/compose.ory.yaml
+++ b/compose.ory.yaml
@@ -36,7 +36,7 @@ services:
container_name: ory_kratos
ports:
- "${KRATOS_PUBLIC_PORT:-4433}:4433"
- - "${KRATOS_ADMIN_PORT:-4434}:4434"
+ - "${KRATOS_ADMINFRONT_PORT:-4434}:4434"
environment:
- DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${KRATOS_DB}?sslmode=disable&max_conns=20
- COOKIE_SECRET=${COOKIE_SECRET:-localcookie123}
@@ -50,6 +50,22 @@ services:
- ory-net
- kratosnet
+ kratos-mcp-server:
+ build:
+ context: ./mcp/kratos-mcp
+ container_name: mcp_ory_kratos
+ profiles:
+ - mcp
+ stdin_open: true
+ tty: true
+ init: true
+ environment:
+ - KRATOS_ADMIN_URL=http://kratos:4434
+ depends_on:
+ - kratos
+ networks:
+ - ory-net
+
kratos-ui:
image: oryd/kratos-selfservice-ui-node:${KRATOS_UI_NODE_VERSION:-v25.4.0}
container_name: ory_kratos_ui
@@ -83,7 +99,7 @@ services:
container_name: ory_hydra
ports:
- "${HYDRA_PUBLIC_PORT:-4441}:4444"
- - "${HYDRA_ADMIN_PORT:-4445}:4445"
+ - "${HYDRA_ADMINFRONT_PORT:-4445}:4445"
environment:
- DSN=postgres://${ORY_POSTGRES_USER}:${ORY_POSTGRES_PASSWORD}@postgres_ory:5432/${HYDRA_DB}?sslmode=disable&max_conns=20
- URLS_SELF_ISSUER=${BACKEND_URL:-http://127.0.0.1:3000}
@@ -100,6 +116,23 @@ services:
- ory-net
- hydranet
+ hydra-mcp-server:
+ build:
+ context: ./mcp/hydra-mcp
+ container_name: mcp_ory_hydra
+ profiles:
+ - mcp
+ stdin_open: true
+ tty: true
+ init: true
+ environment:
+ - HYDRA_PUBLIC_URL=http://hydra:4444
+ - HYDRA_ADMIN_URL=http://hydra:4445
+ depends_on:
+ - hydra
+ networks:
+ - ory-net
+
# --- Keto ---
keto-migrate:
image: oryd/keto:${KETO_VERSION:-v25.4.0}
@@ -171,14 +204,24 @@ services:
environment:
- HYDRA_ADMIN_URL=http://hydra:4445
command: >
- clients create
+ /bin/sh -c "
+ hydra clients create
--endpoint http://hydra:4445
--id adminfront
--secret admin-secret
--grant-types authorization_code,refresh_token
--response-types code
--scope openid,offline_access,profile,email
- --callbacks http://localhost:5000/callback
+ --callbacks http://localhost:5000/callback;
+ hydra clients create
+ --endpoint http://hydra:4445
+ --id devfront
+ --grant-types authorization_code,refresh_token
+ --response-types code
+ --scope openid,offline_access,profile,email
+ --token-endpoint-auth-method none
+ --callbacks http://localhost:5174/callback;
+ "
depends_on:
ory_stack_check:
condition: service_completed_successfully
diff --git a/devfront/.gitignore b/devfront/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/devfront/.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/devfront/Dockerfile b/devfront/Dockerfile
new file mode 100644
index 00000000..31e5a2b1
--- /dev/null
+++ b/devfront/Dockerfile
@@ -0,0 +1,25 @@
+FROM node:lts
+
+WORKDIR /app
+
+# 패키지 정보 복사 및 의존성 설치
+COPY package*.json ./
+RUN npm ci
+
+# 프로덕션 서빙을 위한 serve 패키지 글로벌 설치
+RUN npm install -g serve
+
+# 소스 코드 복사
+COPY . .
+
+# Vite 기본 포트
+EXPOSE 5173
+
+# 실행 스크립트: APP_ENV에 따라 개발 서버 또는 빌드 후 서빙
+CMD sh -c "if [ \"$APP_ENV\" = 'production' ]; then \
+ echo 'Running in production mode...'; \
+ npm run build && serve -s dist -l 5173; \
+ else \
+ echo 'Running in development mode...'; \
+ npm run dev -- --host 0.0.0.0; \
+ fi"
diff --git a/devfront/README.md b/devfront/README.md
new file mode 100644
index 00000000..a3a44a4e
--- /dev/null
+++ b/devfront/README.md
@@ -0,0 +1,29 @@
+# Dev Front (React 19 + Vite)
+
+RP 등록 현황과 Consent 관리를 담당하는 개발자 포털용 React/Vite 기반 SPA입니다. adminfront와 동일한 스택으로 구성하고, Ory Hydra Admin API 연동을 위한 훅 포인트를 남겨두었습니다.
+
+## 주요 스택
+- React 19, Vite 7, TypeScript(strict)
+- React Router v6 (data router)
+- TanStack Query v5
+- Tailwind CSS v3 + shadcn/ui 컴포넌트(seed: Button/Card/Badge/Input/Table/Avatar)
+- Axios 클라이언트 스텁: Bearer + `X-Tenant-ID` 헤더 주입 준비
+- React Hook Form + Zod (추가 예정)
+- Biome (formatter/linter)
+
+## 실행
+```bash
+npm install
+npm run dev
+```
+
+## 구조
+- `src/app`: 라우터, QueryClient 등 전역 설정
+- `src/components/layout`: App 레이아웃/네비게이션
+- `src/features`: dashboard, clients, audit, auth 등 화면 스캐폴딩
+- `src/lib/apiClient.ts`: Axios 인스턴스(토큰/테넌트 헤더 주입 스텁)
+
+## 다음 작업 가이드
+- Devfront 전용 인증/권한 가드 추가 (RP 관리 권한 검증)
+- 테넌트 선택 UI 추가 → `X-Tenant-ID` 헤더에 반영
+- Hydra Admin API 기반 RP/Consent 실데이터 연동
diff --git a/devfront/biome.json b/devfront/biome.json
new file mode 100644
index 00000000..04f99bc8
--- /dev/null
+++ b/devfront/biome.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
+ "formatter": {
+ "indentStyle": "space"
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "style": {
+ "useEnumInitializers": "off"
+ },
+ "a11y": {
+ "noLabelWithoutControl": "off"
+ }
+ }
+ },
+ "organizeImports": {
+ "enabled": true
+ },
+ "files": {
+ "ignore": ["dist", "node_modules", "tsconfig*.json"]
+ }
+}
diff --git a/devfront/index.html b/devfront/index.html
new file mode 100644
index 00000000..c3ad2141
--- /dev/null
+++ b/devfront/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ 바론 개발자 서비스
+
+
+
+
+
+
diff --git a/devfront/package-lock.json b/devfront/package-lock.json
new file mode 100644
index 00000000..460860c8
--- /dev/null
+++ b/devfront/package-lock.json
@@ -0,0 +1,3622 @@
+{
+ "name": "devfront",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "devfront",
+ "version": "0.0.0",
+ "dependencies": {
+ "@radix-ui/react-avatar": "^1.1.4",
+ "@radix-ui/react-scroll-area": "^1.1.2",
+ "@radix-ui/react-slot": "^1.1.2",
+ "@radix-ui/react-switch": "^1.1.2",
+ "@tanstack/react-query": "^5.66.8",
+ "@tanstack/react-query-devtools": "^5.66.8",
+ "axios": "^1.7.9",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.563.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-hook-form": "^7.71.1",
+ "react-router-dom": "^6.28.2",
+ "tailwind-merge": "^3.4.0",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^1.9.4",
+ "@playwright/test": "^1.58.0",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "autoprefixer": "^10.4.23",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^3.4.14",
+ "tailwindcss-animate": "^1.0.7",
+ "typescript": "~5.9.3",
+ "vite": "npm:rolldown-vite@7.2.5"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
+ "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
+ "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
+ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/generator": "^7.28.6",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6",
+ "@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.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
+ "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6",
+ "@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.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "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.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
+ "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.6"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
+ "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/generator": "^7.28.6",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.6",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
+ "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@biomejs/biome": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
+ "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT OR Apache-2.0",
+ "bin": {
+ "biome": "bin/biome"
+ },
+ "engines": {
+ "node": ">=14.21.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/biome"
+ },
+ "optionalDependencies": {
+ "@biomejs/cli-darwin-arm64": "1.9.4",
+ "@biomejs/cli-darwin-x64": "1.9.4",
+ "@biomejs/cli-linux-arm64": "1.9.4",
+ "@biomejs/cli-linux-arm64-musl": "1.9.4",
+ "@biomejs/cli-linux-x64": "1.9.4",
+ "@biomejs/cli-linux-x64-musl": "1.9.4",
+ "@biomejs/cli-win32-arm64": "1.9.4",
+ "@biomejs/cli-win32-x64": "1.9.4"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-arm64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
+ "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-x64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
+ "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-arm64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
+ "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-arm64-musl": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
+ "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-x64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
+ "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-x64-musl": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
+ "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-win32-arm64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
+ "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-win32-x64": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
+ "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
+ "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.1.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
+ "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
+ "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
+ "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@oxc-project/runtime": {
+ "version": "0.97.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.97.0.tgz",
+ "integrity": "sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.97.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.97.0.tgz",
+ "integrity": "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.58.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.0.tgz",
+ "integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.58.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz",
+ "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.4",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz",
+ "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
+ "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
+ "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
+ "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.50.tgz",
+ "integrity": "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==",
+ "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.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.50.tgz",
+ "integrity": "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==",
+ "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.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.50.tgz",
+ "integrity": "sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA==",
+ "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.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.50.tgz",
+ "integrity": "sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==",
+ "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.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.50.tgz",
+ "integrity": "sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==",
+ "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.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.50.tgz",
+ "integrity": "sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.50.tgz",
+ "integrity": "sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.50.tgz",
+ "integrity": "sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.50.tgz",
+ "integrity": "sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.50.tgz",
+ "integrity": "sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==",
+ "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.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.50.tgz",
+ "integrity": "sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.0.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.50.tgz",
+ "integrity": "sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-ia32-msvc": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.50.tgz",
+ "integrity": "sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==",
+ "cpu": [
+ "ia32"
+ ],
+ "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.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.50.tgz",
+ "integrity": "sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==",
+ "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.0-beta.53",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
+ "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.90.20",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
+ "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/query-devtools": {
+ "version": "5.92.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.92.0.tgz",
+ "integrity": "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.90.20",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz",
+ "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.90.20"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@tanstack/react-query-devtools": {
+ "version": "5.91.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.2.tgz",
+ "integrity": "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-devtools": "5.92.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^5.90.14",
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "24.10.9",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz",
+ "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.9",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
+ "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
+ "devOptional": 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==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
+ "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.5",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.53",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.23",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
+ "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001760",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.3.tgz",
+ "integrity": "sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.18",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
+ "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "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.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001766",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
+ "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
+ "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/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "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==",
+ "devOptional": 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/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.278",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.278.tgz",
+ "integrity": "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "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/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "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/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/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-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/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/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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-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/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "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/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/lightningcss": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
+ "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==",
+ "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.31.1",
+ "lightningcss-darwin-arm64": "1.31.1",
+ "lightningcss-darwin-x64": "1.31.1",
+ "lightningcss-freebsd-x64": "1.31.1",
+ "lightningcss-linux-arm-gnueabihf": "1.31.1",
+ "lightningcss-linux-arm64-gnu": "1.31.1",
+ "lightningcss-linux-arm64-musl": "1.31.1",
+ "lightningcss-linux-x64-gnu": "1.31.1",
+ "lightningcss-linux-x64-musl": "1.31.1",
+ "lightningcss-win32-arm64-msvc": "1.31.1",
+ "lightningcss-win32-x64-msvc": "1.31.1"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz",
+ "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==",
+ "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.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz",
+ "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==",
+ "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.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz",
+ "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==",
+ "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.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz",
+ "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==",
+ "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.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz",
+ "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==",
+ "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.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz",
+ "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==",
+ "cpu": [
+ "arm64"
+ ],
+ "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-musl": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz",
+ "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==",
+ "cpu": [
+ "arm64"
+ ],
+ "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-x64-gnu": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
+ "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
+ "cpu": [
+ "x64"
+ ],
+ "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-x64-musl": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
+ "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
+ "cpu": [
+ "x64"
+ ],
+ "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-win32-arm64-msvc": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz",
+ "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==",
+ "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.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz",
+ "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==",
+ "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/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/lucide-react": {
+ "version": "0.563.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz",
+ "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "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/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.58.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz",
+ "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.58.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz",
+ "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "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.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
+ "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.3"
+ }
+ },
+ "node_modules/react-hook-form": {
+ "version": "7.71.1",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
+ "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/readdirp/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.50.tgz",
+ "integrity": "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.97.0",
+ "@rolldown/pluginutils": "1.0.0-beta.50"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-beta.50",
+ "@rolldown/binding-darwin-arm64": "1.0.0-beta.50",
+ "@rolldown/binding-darwin-x64": "1.0.0-beta.50",
+ "@rolldown/binding-freebsd-x64": "1.0.0-beta.50",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50",
+ "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50"
+ }
+ },
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz",
+ "integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.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/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/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
+ "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "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/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "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/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "name": "rolldown-vite",
+ "version": "7.2.5",
+ "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.2.5.tgz",
+ "integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/runtime": "0.97.0",
+ "fdir": "^6.5.0",
+ "lightningcss": "^1.30.2",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rolldown": "1.0.0-beta.50",
+ "tinyglobby": "^0.2.15"
+ },
+ "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",
+ "esbuild": "^0.25.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
+ },
+ "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/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/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/devfront/package.json b/devfront/package.json
new file mode 100644
index 00000000..cf724b45
--- /dev/null
+++ b/devfront/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "devfront",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 0.0.0.0",
+ "build": "tsc -b && vite build",
+ "lint": "biome check .",
+ "preview": "vite preview",
+ "test": "playwright test",
+ "test:ui": "playwright test --ui"
+ },
+ "dependencies": {
+ "@radix-ui/react-avatar": "^1.1.4",
+ "@radix-ui/react-scroll-area": "^1.1.2",
+ "@radix-ui/react-slot": "^1.1.2",
+ "@radix-ui/react-switch": "^1.1.2",
+ "@tanstack/react-query": "^5.66.8",
+ "@tanstack/react-query-devtools": "^5.66.8",
+ "axios": "^1.7.9",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.563.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-hook-form": "^7.71.1",
+ "react-router-dom": "^6.28.2",
+ "tailwind-merge": "^3.4.0",
+ "zod": "^3.24.1"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^1.9.4",
+ "@playwright/test": "^1.58.0",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "autoprefixer": "^10.4.23",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^3.4.14",
+ "tailwindcss-animate": "^1.0.7",
+ "typescript": "~5.9.3",
+ "vite": "npm:rolldown-vite@7.2.5"
+ },
+ "overrides": {
+ "vite": "npm:rolldown-vite@7.2.5"
+ }
+}
diff --git a/devfront/playwright.config.ts b/devfront/playwright.config.ts
new file mode 100644
index 00000000..9606c1ce
--- /dev/null
+++ b/devfront/playwright.config.ts
@@ -0,0 +1,59 @@
+import { defineConfig, devices } from "@playwright/test";
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// import dotenv from 'dotenv';
+// import path from 'path';
+// dotenv.config({ path: path.resolve(__dirname, '.env') });
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: "./tests",
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: "html",
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: "http://localhost:5174",
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: "on-first-retry",
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: "chromium",
+ use: { ...devices["Desktop Chrome"] },
+ },
+
+ {
+ name: "firefox",
+ use: { ...devices["Desktop Firefox"] },
+ },
+
+ {
+ name: "webkit",
+ use: { ...devices["Desktop Safari"] },
+ },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: "npm run dev -- --port 5174",
+ url: "http://localhost:5174",
+ reuseExistingServer: !process.env.CI,
+ },
+});
diff --git a/devfront/postcss.config.js b/devfront/postcss.config.js
new file mode 100644
index 00000000..2aa7205d
--- /dev/null
+++ b/devfront/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/devfront/public/vite.svg b/devfront/public/vite.svg
new file mode 100644
index 00000000..e7b8dfb1
--- /dev/null
+++ b/devfront/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/devfront/src/app/queryClient.ts b/devfront/src/app/queryClient.ts
new file mode 100644
index 00000000..06c9afde
--- /dev/null
+++ b/devfront/src/app/queryClient.ts
@@ -0,0 +1,11 @@
+import { QueryClient } from "@tanstack/react-query";
+
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 30_000,
+ refetchOnWindowFocus: false,
+ retry: 1,
+ },
+ },
+});
diff --git a/devfront/src/app/routes.tsx b/devfront/src/app/routes.tsx
new file mode 100644
index 00000000..cc285173
--- /dev/null
+++ b/devfront/src/app/routes.tsx
@@ -0,0 +1,28 @@
+import { Navigate, createBrowserRouter } from "react-router-dom";
+import AppLayout from "../components/layout/AppLayout";
+import ClientConsentsPage from "../features/clients/ClientConsentsPage";
+import ClientDetailsPage from "../features/clients/ClientDetailsPage";
+import ClientGeneralPage from "../features/clients/ClientGeneralPage";
+import ClientsPage from "../features/clients/ClientsPage";
+
+export const router = createBrowserRouter(
+ [
+ {
+ path: "/",
+ element: ,
+ children: [
+ { index: true, element: },
+ { path: "clients", element: },
+ { path: "clients/:id", element: },
+ { path: "clients/:id/consents", element: },
+ { path: "clients/:id/settings", element: },
+ ],
+ },
+ ],
+ // React Router v7 플래그 사전 적용 (현재 타입 정의에 없어 any 캐스팅)
+ {
+ future: {
+ v7_startTransition: true,
+ },
+ } as unknown as Parameters[1],
+);
diff --git a/devfront/src/assets/react.svg b/devfront/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/devfront/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/devfront/src/components/layout/AppLayout.tsx b/devfront/src/components/layout/AppLayout.tsx
new file mode 100644
index 00000000..35d9448d
--- /dev/null
+++ b/devfront/src/components/layout/AppLayout.tsx
@@ -0,0 +1,112 @@
+import { BadgeCheck, Moon, ShieldHalf, Sun } from "lucide-react";
+import { useEffect, useState } from "react";
+import { NavLink, Outlet } from "react-router-dom";
+
+const navItems = [{ label: "Clients", to: "/clients", icon: ShieldHalf }];
+
+function AppLayout() {
+ const [theme, setTheme] = useState<"light" | "dark">(() => {
+ const stored = window.localStorage.getItem("admin_theme");
+ return stored === "dark" ? "dark" : "light";
+ });
+
+ useEffect(() => {
+ const root = document.documentElement;
+ root.classList.remove("light", "dark");
+ if (theme === "light") {
+ root.classList.add("light");
+ } else {
+ root.classList.add("dark");
+ }
+ window.localStorage.setItem("admin_theme", theme);
+ }, [theme]);
+
+ const toggleTheme = () => {
+ setTheme((prev) => (prev === "light" ? "dark" : "light"));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ Baron 통합로그인
+
+
Developer Console
+
+
+
+
+ Scoped to /dev
+
+
+
+
+
+ Env: dev
+
+
+
+ {navItems.map(({ label, to, icon: Icon }) => (
+
+ [
+ "flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
+ isActive
+ ? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
+ : "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
+ ].join(" ")
+ }
+ >
+
+ {label}
+
+ ))}
+
+
+
+
개발자 전용 콘솔입니다.
+
클라이언트 애플리케이션 등록 및 관리를 수행할 수 있습니다.
+
+
+
+
+
+ );
+}
+
+export default AppLayout;
diff --git a/devfront/src/components/ui/avatar.tsx b/devfront/src/components/ui/avatar.tsx
new file mode 100644
index 00000000..23e88913
--- /dev/null
+++ b/devfront/src/components/ui/avatar.tsx
@@ -0,0 +1,47 @@
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+import * as React from "react";
+import { cn } from "../../lib/utils";
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/devfront/src/components/ui/badge.tsx b/devfront/src/components/ui/badge.tsx
new file mode 100644
index 00000000..8ef586fd
--- /dev/null
+++ b/devfront/src/components/ui/badge.tsx
@@ -0,0 +1,38 @@
+import { type VariantProps, cva } from "class-variance-authority";
+import type * as React from "react";
+import { cn } from "../../lib/utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ outline: "text-foreground",
+ muted: "border-border bg-secondary/60 text-muted-foreground",
+ success:
+ "border-transparent bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300",
+ warning:
+ "border-transparent bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-200",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/devfront/src/components/ui/button.tsx b/devfront/src/components/ui/button.tsx
new file mode 100644
index 00000000..ee1a84b4
--- /dev/null
+++ b/devfront/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import { Slot } from "@radix-ui/react-slot";
+import { type VariantProps, cva } from "class-variance-authority";
+import * as React from "react";
+import { cn } from "../../lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ muted: "bg-muted text-muted-foreground hover:bg-muted/80",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-6 text-base",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/devfront/src/components/ui/card.tsx b/devfront/src/components/ui/card.tsx
new file mode 100644
index 00000000..7048cb12
--- /dev/null
+++ b/devfront/src/components/ui/card.tsx
@@ -0,0 +1,72 @@
+import type * as React from "react";
+import { cn } from "../../lib/utils";
+
+function Card({ className, ...props }: React.HTMLAttributes) {
+ return (
+
+ );
+}
+
+function CardHeader({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ );
+}
+
+function CardTitle({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ );
+}
+
+function CardDescription({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ );
+}
+
+function CardContent({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return
;
+}
+
+function CardFooter({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ );
+}
+
+export {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ CardFooter,
+};
diff --git a/devfront/src/components/ui/input.tsx b/devfront/src/components/ui/input.tsx
new file mode 100644
index 00000000..41955477
--- /dev/null
+++ b/devfront/src/components/ui/input.tsx
@@ -0,0 +1,24 @@
+import * as React from "react";
+import { cn } from "../../lib/utils";
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Input.displayName = "Input";
+
+export { Input };
diff --git a/devfront/src/components/ui/label.tsx b/devfront/src/components/ui/label.tsx
new file mode 100644
index 00000000..1a555097
--- /dev/null
+++ b/devfront/src/components/ui/label.tsx
@@ -0,0 +1,19 @@
+import * as React from "react";
+import { cn } from "../../lib/utils";
+
+const Label = React.forwardRef<
+ HTMLLabelElement,
+ React.LabelHTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = "Label";
+
+export { Label };
diff --git a/devfront/src/components/ui/scroll-area.tsx b/devfront/src/components/ui/scroll-area.tsx
new file mode 100644
index 00000000..7b80ff13
--- /dev/null
+++ b/devfront/src/components/ui/scroll-area.tsx
@@ -0,0 +1,44 @@
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
+import * as React from "react";
+import { cn } from "../../lib/utils";
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+));
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
+
+export { ScrollArea, ScrollBar };
diff --git a/devfront/src/components/ui/separator.tsx b/devfront/src/components/ui/separator.tsx
new file mode 100644
index 00000000..8f4c741f
--- /dev/null
+++ b/devfront/src/components/ui/separator.tsx
@@ -0,0 +1,16 @@
+import * as React from "react";
+import { cn } from "../../lib/utils";
+
+const Separator = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Separator.displayName = "Separator";
+
+export { Separator };
diff --git a/devfront/src/components/ui/switch.tsx b/devfront/src/components/ui/switch.tsx
new file mode 100644
index 00000000..dfc8f347
--- /dev/null
+++ b/devfront/src/components/ui/switch.tsx
@@ -0,0 +1,26 @@
+import * as SwitchPrimitives from "@radix-ui/react-switch";
+import * as React from "react";
+import { cn } from "../../lib/utils";
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+Switch.displayName = SwitchPrimitives.Root.displayName;
+
+export { Switch };
diff --git a/devfront/src/components/ui/table.tsx b/devfront/src/components/ui/table.tsx
new file mode 100644
index 00000000..b20952d6
--- /dev/null
+++ b/devfront/src/components/ui/table.tsx
@@ -0,0 +1,113 @@
+import * as React from "react";
+import { cn } from "../../lib/utils";
+
+const Table = React.forwardRef<
+ HTMLTableElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Table.displayName = "Table";
+
+const TableHeader = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableHeader.displayName = "TableHeader";
+
+const TableBody = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableBody.displayName = "TableBody";
+
+const TableFooter = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableFooter.displayName = "TableFooter";
+
+const TableRow = React.forwardRef<
+ HTMLTableRowElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableRow.displayName = "TableRow";
+
+const TableHead = React.forwardRef<
+ HTMLTableCellElement,
+ React.ThHTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableHead.displayName = "TableHead";
+
+const TableCell = React.forwardRef<
+ HTMLTableCellElement,
+ React.TdHTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableCell.displayName = "TableCell";
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableCaption.displayName = "TableCaption";
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+};
diff --git a/devfront/src/components/ui/textarea.tsx b/devfront/src/components/ui/textarea.tsx
new file mode 100644
index 00000000..80f0abc2
--- /dev/null
+++ b/devfront/src/components/ui/textarea.tsx
@@ -0,0 +1,23 @@
+import * as React from "react";
+import { cn } from "../../lib/utils";
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes {}
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Textarea.displayName = "Textarea";
+
+export { Textarea };
diff --git a/devfront/src/features/audit/AuditLogsPage.tsx b/devfront/src/features/audit/AuditLogsPage.tsx
new file mode 100644
index 00000000..ebaee008
--- /dev/null
+++ b/devfront/src/features/audit/AuditLogsPage.tsx
@@ -0,0 +1,143 @@
+import { Filter, ListChecks, Search, Terminal } from "lucide-react";
+
+const auditFilters = [
+ "Actor role = admin",
+ "Action = client.rotate_secret",
+ "Tenant = selected header",
+];
+
+const auditRows = [
+ {
+ action: "client.create",
+ tenant: "TENANT-12",
+ actor: "ops.jane@baron",
+ result: "ok",
+ ts: "2026-01-26 15:21 KST",
+ },
+ {
+ action: "client.rotate_secret",
+ tenant: "TENANT-12",
+ actor: "ops.jane@baron",
+ result: "ok",
+ ts: "2026-01-26 15:22 KST",
+ },
+ {
+ action: "audit.export",
+ tenant: "TENANT-07",
+ actor: "auditor.lee@baron",
+ result: "rate_limited",
+ ts: "2026-01-26 15:30 KST",
+ },
+];
+
+function AuditLogsPage() {
+ return (
+
+
+
+
+ Audit stream
+
+
+ Observe admin actions per tenant
+
+
+ ClickHouse-backed feed. Filter by tenant, actor, action, and
+ rate-limit status. Enforce admin-only access under /admin.
+
+
+
+
+
+ Saved filters
+
+
+
+ Export CSV
+
+
+
+
+
+
+
+
+
+ Try: tenant:TENANT-12 action:client.*
+
+
+
+ {auditFilters.map((filter) => (
+
+
+ {filter}
+
+ ))}
+
+
+ {auditRows.map((row) => (
+
+
{row.action}
+
{row.tenant}
+
{row.actor}
+
+
+ {row.result}
+
+ {row.ts}
+
+
+ ))}
+
+
+
+
+
+
+ Guard rails
+
+
Tenant admin only
+
+ Enforce Tenant Admin middleware and admin session TTL before
+ surfacing any audit feed. Super Admin role can bypass tenant
+ filter when needed.
+
+
+
+
+ Export rules
+
+
+ Rate-limit sensitive exports
+
+
+ Keep export endpoints behind admin-only routes with ClickHouse
+ query limits. Log download attempts with IP, role, and tenant
+ scope.
+
+
+
+
+
+ );
+}
+
+export default AuditLogsPage;
diff --git a/devfront/src/features/auth/AuthPage.tsx b/devfront/src/features/auth/AuthPage.tsx
new file mode 100644
index 00000000..015bbb67
--- /dev/null
+++ b/devfront/src/features/auth/AuthPage.tsx
@@ -0,0 +1,111 @@
+import { ArrowRight, Fingerprint, Smartphone, Sparkles } from "lucide-react";
+
+const flows = [
+ {
+ title: "Admin login",
+ description:
+ "Enforce short TTL and step-up MFA. Keep admin session separate from app session.",
+ pill: "15m TTL",
+ },
+ {
+ title: "Tenant pick",
+ description:
+ "Admin chooses target tenant before hitting APIs. Propagate X-Tenant-ID on every call.",
+ pill: "Header-ready",
+ },
+ {
+ title: "Device approval",
+ description:
+ "If app session exists and user opts in, use push/deeplink approval as MFA replacement.",
+ pill: "App session",
+ },
+];
+
+function AuthPage() {
+ return (
+
+
+
+
+
+ Admin auth
+
+
Admin auth guardrails
+
+ Build the admin-only login flow first, keeping app login separate.
+ Respect the “fallback only when user chooses” rule for SMS/email
+ vs app approval.
+
+
+
+
+ IDP session placeholder
+
+
+
+ Connect auth layer
+
+
+
+
+
+
+ {flows.map((flow) => (
+
+
+ {flow.pill}
+
+
+
{flow.title}
+
+ {flow.description}
+
+
+ ))}
+
+
+
+
+
+
+
+ App-based approvals
+
+
+
+ App session as MFA replacement
+
+
+ If the admin keeps the mobile app signed in and opts in, use
+ push/deeplink approval instead of OTP. Otherwise fall back to
+ SMS/email based on user choice.
+
+
+
+
+
+ Keep admin sessions short
+
+
+ Default admin TTL is 15 minutes. Show countdown and nudge re-auth
+ with step-up MFA when critical actions (rotate secret, export logs)
+ happen.
+
+
+
+
+ );
+}
+
+export default AuthPage;
diff --git a/adminfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx
similarity index 100%
rename from adminfront/src/features/clients/ClientConsentsPage.tsx
rename to devfront/src/features/clients/ClientConsentsPage.tsx
diff --git a/adminfront/src/features/clients/ClientDetailsPage.tsx b/devfront/src/features/clients/ClientDetailsPage.tsx
similarity index 100%
rename from adminfront/src/features/clients/ClientDetailsPage.tsx
rename to devfront/src/features/clients/ClientDetailsPage.tsx
diff --git a/adminfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx
similarity index 100%
rename from adminfront/src/features/clients/ClientGeneralPage.tsx
rename to devfront/src/features/clients/ClientGeneralPage.tsx
diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx
new file mode 100644
index 00000000..862a0c4d
--- /dev/null
+++ b/devfront/src/features/clients/ClientsPage.tsx
@@ -0,0 +1,366 @@
+import { useQuery } from "@tanstack/react-query";
+import {
+ Activity,
+ BookOpenText,
+ Copy,
+ Laptop,
+ Plus,
+ Search,
+ ServerCog,
+ ShieldHalf,
+} from "lucide-react";
+import { Link } from "react-router-dom";
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "../../components/ui/avatar";
+import { Badge } from "../../components/ui/badge";
+import { Button } from "../../components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "../../components/ui/card";
+import { Input } from "../../components/ui/input";
+import { Separator } from "../../components/ui/separator";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "../../components/ui/table";
+import { fetchClients } from "../../lib/devApi";
+import { cn } from "../../lib/utils";
+
+function ClientsPage() {
+ const { data, isLoading, error } = useQuery({
+ queryKey: ["clients"],
+ queryFn: fetchClients,
+ });
+
+ const clients = data?.items || [];
+ const totalClients = clients.length;
+ // TODO: Add real stats for active sessions and auth failures
+ const stats = [
+ {
+ label: "총 클라이언트",
+ value: totalClients.toString(),
+ delta: "Realtime",
+ tone: "up" as const,
+ },
+ {
+ label: "활성 세션",
+ value: "-",
+ delta: "Not impl",
+ tone: "stable" as const,
+ },
+ {
+ label: "인증 실패 (24h)",
+ value: "0",
+ delta: "Stable",
+ tone: "stable" as const,
+ },
+ ];
+
+ if (isLoading) {
+ return Loading clients...
;
+ }
+
+ if (error) {
+ const errMsg =
+ (error as any).response?.data?.error || (error as Error).message;
+ return (
+
+ Error loading clients: {errMsg}
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ RP registry
+
+
+ Relying Parties
+
+
+ OIDC 클라이언트, 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사
+ 로그와 함께 관리합니다.
+
+
+
+
+ 비밀키 재발행
+
+
+ 새 클라이언트
+
+
+
+
+
+
+
+
+
+ 테넌트: 선택됨
+ 관리자 세션
+
+
+
+
+
+ {stats.map((item) => (
+
+
+ {item.label}
+
+ {item.value}
+
+ {item.delta}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ 클라이언트 목록
+
+
+
+ 비밀키 재발행
+
+
+ 새 클라이언트
+
+
+
+
+
+
+
+
+ 애플리케이션
+ Client ID
+ 유형
+ 상태
+ 생성일
+ 액션
+
+
+
+ {clients.map((client) => (
+
+
+
+
+ {client.type === "confidential" ? (
+
+ ) : (
+
+ )}
+
+
+
+ {client.name || "Untitled"}
+
+
+ Tenant-scoped
+
+
+
+
+
+
+
+ {client.id}
+
+
+ navigator.clipboard.writeText(client.id)
+ }
+ >
+
+
+
+
+
+
+ {client.type === "confidential"
+ ? "기밀(Confidential)"
+ : "Public"}
+
+
+
+
+
+
+ {client.status === "active" ? "활성" : "비활성"}
+
+
+
+
+ {client.createdAt
+ ? new Date(client.createdAt).toLocaleDateString()
+ : "-"}
+
+
+
+
+
+ ))}
+
+
+
+
+ Showing {clients.length} of {totalClients} clients
+
+
+
+ Previous
+
+
+ Next
+
+
+
+
+
+
+
+
+
+
+ Need help with OIDC configuration?
+
+
+ Developer guides for Confidential/Public clients, redirect URIs,
+ and auth methods.
+
+
+
+
+
+
+
+
+
Docs & Examples
+
+ Includes PKCE, client_secret_basic, redirect URI validation
+ tips.
+
+
+
+ View guides
+
+
+
+
+
+ Owner
+ Tenant admin on-call
+
+
+
+
+
+ AR
+
+
+
AI Admin Bot
+
admin@brsw.kr
+
+
+
+
+ Role: Tenant Admin
+ Scope: TENANT-12
+
+
+
+
+
+ );
+}
+
+export default ClientsPage;
diff --git a/devfront/src/features/dashboard/DashboardPage.tsx b/devfront/src/features/dashboard/DashboardPage.tsx
new file mode 100644
index 00000000..a42f99c7
--- /dev/null
+++ b/devfront/src/features/dashboard/DashboardPage.tsx
@@ -0,0 +1,215 @@
+import {
+ Activity,
+ ArrowRight,
+ BarChart3,
+ CheckCircle2,
+ Database,
+ KeyRound,
+ ShieldCheck,
+ Sparkles,
+} from "lucide-react";
+
+const guardHighlights = [
+ {
+ title: "RP 정책 통제",
+ body: "Relying Party 상태를 활성/비활성으로 관리하고 정책 변경을 기록합니다.",
+ metric: "Policy",
+ },
+ {
+ title: "Consent 흐름",
+ body: "사용자 Consent를 조회하고 필요 시 회수해 리스크를 제어합니다.",
+ metric: "Consent",
+ },
+ {
+ title: "Hydra Admin",
+ body: "Hydra Admin API를 통해 RP 등록 현황을 동기화합니다.",
+ metric: "Hydra",
+ },
+];
+
+const stackReadiness = [
+ "React 19 + Vite 7, strict TS, Router v6 data router.",
+ "TanStack Query 5로 RP/Consent 데이터를 캐시합니다.",
+ "Axios 클라이언트에서 Bearer + 테넌트 헤더를 주입합니다.",
+ "Tailwind + shadcn/ui로 devfront 톤을 맞춥니다.",
+ "Hydra Admin API 연동을 위한 프록시 엔드포인트 준비.",
+];
+
+const nextSteps = [
+ "RP 등록/수정/삭제 워크플로우 추가",
+ "Consent 검색 필터 고도화 및 CSV 내보내기",
+ "권한 가드 및 감사 로그 연동",
+];
+
+function DashboardPage() {
+ return (
+
+
+
+
+
+
+
+ devfront ready
+
+
+ RP 등록 현황과 Consent 상태를
+ 하나의 화면
+ 에서 관리합니다.
+
+
+ Hydra Admin API와 동기화된 RP 목록, 상태 토글, Consent 회수까지
+ devfront에서 처리하도록 준비합니다.
+
+
+
+ RP registry synced
+
+
+ Consent guard ready
+
+
+ Policy toggle enabled
+
+
+
+
+
+
+ RP 정책은 dev scope에서만 적용
+
+
+
+ Consent 회수는 감사 로그와 연계
+
+
+
+ Hydra Admin 상태 체크 준비
+
+
+
+
+
+
+ {guardHighlights.map((item) => (
+
+
+
+
+ {item.metric}
+
+
+ active
+
+
+
+
{item.title}
+
{item.body}
+
+
+ ))}
+
+
+
+
+
+
+
+ Stack readiness
+
+
Devfront baseline
+
+
+ Setup notes
+
+
+
+
+ {stackReadiness.map((item) => (
+
+ ))}
+
+
+
+
+
+ Next actions
+
+
Ship the RP controls
+
+ {nextSteps.map((item, idx) => (
+
+
+ {idx + 1}
+
+
{item}
+
+ ))}
+
+
+
+
+
+
+
+
+ Ops board
+
+
현재 관측
+
+
+
+ Consent grants
+
+
+ RP status
+
+
+
+
+
+
+
+ RP 요청 추이
+
+
준비 중
+
+
+
+
+
+
+ );
+}
+
+export default DashboardPage;
diff --git a/devfront/src/index.css b/devfront/src/index.css
new file mode 100644
index 00000000..20ef470f
--- /dev/null
+++ b/devfront/src/index.css
@@ -0,0 +1,83 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 210 25% 6%;
+ --foreground: 210 35% 96%;
+ --card: 215 32% 9%;
+ --card-foreground: 210 35% 96%;
+ --popover: 215 32% 9%;
+ --popover-foreground: 210 35% 96%;
+ --primary: 209 79% 52%;
+ --primary-foreground: 210 35% 96%;
+ --secondary: 215 25% 16%;
+ --secondary-foreground: 210 35% 96%;
+ --muted: 215 15% 65%;
+ --muted-foreground: 215 15% 65%;
+ --accent: 42 95% 57%;
+ --accent-foreground: 215 25% 10%;
+ --destructive: 0 84% 60%;
+ --destructive-foreground: 210 35% 96%;
+ --border: 215 25% 24%;
+ --input: 215 25% 24%;
+ --ring: 209 79% 52%;
+ --radius: 0.75rem;
+ }
+
+ .light {
+ --background: 0 0% 98%;
+ --foreground: 223 25% 12%;
+ --card: 0 0% 100%;
+ --card-foreground: 223 25% 12%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 223 25% 12%;
+ --primary: 209 79% 52%;
+ --primary-foreground: 0 0% 100%;
+ --secondary: 220 17% 94%;
+ --secondary-foreground: 223 25% 20%;
+ --muted: 223 15% 45%;
+ --muted-foreground: 223 15% 45%;
+ --accent: 40 96% 62%;
+ --accent-foreground: 223 25% 12%;
+ --destructive: 0 84% 60%;
+ --destructive-foreground: 0 0% 100%;
+ --border: 220 17% 90%;
+ --input: 220 17% 90%;
+ --ring: 209 79% 52%;
+ }
+
+ * {
+ @apply border-border;
+ }
+
+ body {
+ @apply min-h-screen bg-background font-sans text-foreground antialiased;
+ background-image: radial-gradient(
+ circle at 10% 18%,
+ rgba(54, 211, 153, 0.16),
+ transparent 28%
+ ),
+ radial-gradient(
+ circle at 78% 4%,
+ rgba(249, 168, 38, 0.14),
+ transparent 24%
+ ),
+ radial-gradient(
+ circle at 50% 90%,
+ rgba(54, 211, 153, 0.08),
+ transparent 30%
+ );
+ }
+
+ a {
+ @apply text-inherit no-underline;
+ }
+}
+
+@layer components {
+ .glass-panel {
+ @apply rounded-2xl border border-border bg-card/85 shadow-card backdrop-blur;
+ }
+}
diff --git a/devfront/src/lib/apiClient.ts b/devfront/src/lib/apiClient.ts
new file mode 100644
index 00000000..80927a89
--- /dev/null
+++ b/devfront/src/lib/apiClient.ts
@@ -0,0 +1,31 @@
+import axios from "axios";
+
+const apiClient = axios.create({
+ baseURL: import.meta.env.VITE_ADMIN_API_BASE ?? "/api/admin",
+});
+
+apiClient.interceptors.request.use((config) => {
+ // TODO: IdP 중립 Auth 레이어 연동 시 세션 토큰을 주입한다.
+ const sessionToken = window.localStorage.getItem("admin_session");
+ if (sessionToken) {
+ config.headers.Authorization = `Bearer ${sessionToken}`;
+ }
+
+ // TODO: 테넌트 선택 값을 보관하고 헤더로 전달한다.
+ const tenantId = window.localStorage.getItem("admin_tenant");
+ if (tenantId) {
+ config.headers["X-Tenant-ID"] = tenantId;
+ }
+
+ return config;
+});
+
+apiClient.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ // TODO: 401/403 응답 시 로그인/재인증 플로우로 리다이렉션한다.
+ return Promise.reject(error);
+ },
+);
+
+export default apiClient;
diff --git a/devfront/src/lib/devApi.ts b/devfront/src/lib/devApi.ts
new file mode 100644
index 00000000..72656221
--- /dev/null
+++ b/devfront/src/lib/devApi.ts
@@ -0,0 +1,89 @@
+import apiClient from "./apiClient";
+
+export type ClientStatus = "active" | "inactive";
+export type ClientType = "confidential" | "public";
+
+export type ClientSummary = {
+ id: string;
+ name: string;
+ type: ClientType;
+ status: ClientStatus;
+ createdAt?: string;
+ redirectUris: string[];
+ scopes: string[];
+};
+
+export type ClientListResponse = {
+ items: ClientSummary[];
+ limit: number;
+ offset: number;
+};
+
+export type ClientEndpoints = {
+ discovery: string;
+ issuer: string;
+ authorization: string;
+ token: string;
+ userinfo: string;
+};
+
+export type ClientDetailResponse = {
+ client: ClientSummary & {
+ metadata?: Record;
+ };
+ endpoints: ClientEndpoints;
+};
+
+export type ConsentSummary = {
+ subject: string;
+ clientId: string;
+ clientName?: string;
+ grantedScopes: string[];
+ authenticatedAt?: string;
+};
+
+export type ConsentListResponse = {
+ items: ConsentSummary[];
+};
+
+export async function fetchClients() {
+ const { data } = await apiClient.get("/clients");
+ return data;
+}
+
+export async function fetchClient(clientId: string) {
+ const { data } = await apiClient.get(
+ `/clients/${clientId}`,
+ );
+ return data;
+}
+
+export async function updateClientStatus(
+ clientId: string,
+ status: ClientStatus,
+) {
+ const { data } = await apiClient.patch(
+ `/clients/${clientId}/status`,
+ { status },
+ );
+ return data;
+}
+
+export async function fetchConsents(subject: string, clientId?: string) {
+ const params: Record = { subject };
+ if (clientId) {
+ params.client_id = clientId;
+ }
+ const { data } = await apiClient.get("/consents", {
+ params,
+ });
+ return data;
+}
+
+export async function revokeConsent(subject: string, clientId?: string) {
+ const params: Record = { subject };
+ if (clientId) {
+ params.client_id = clientId;
+ }
+ await apiClient.delete("/consents", { params });
+}
diff --git a/devfront/src/lib/utils.ts b/devfront/src/lib/utils.ts
new file mode 100644
index 00000000..365058ce
--- /dev/null
+++ b/devfront/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/devfront/src/main.tsx b/devfront/src/main.tsx
new file mode 100644
index 00000000..5bc28ec7
--- /dev/null
+++ b/devfront/src/main.tsx
@@ -0,0 +1,21 @@
+import { QueryClientProvider } from "@tanstack/react-query";
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { RouterProvider } from "react-router-dom";
+import { queryClient } from "./app/queryClient";
+import { router } from "./app/routes";
+import "./index.css";
+
+const rootElement = document.getElementById("root");
+
+if (!rootElement) {
+ throw new Error("Root element not found");
+}
+
+createRoot(rootElement).render(
+
+
+
+
+ ,
+);
diff --git a/devfront/tailwind.config.ts b/devfront/tailwind.config.ts
new file mode 100644
index 00000000..dfb77657
--- /dev/null
+++ b/devfront/tailwind.config.ts
@@ -0,0 +1,67 @@
+import type { Config } from "tailwindcss";
+import { fontFamily } from "tailwindcss/defaultTheme";
+
+const config: Config = {
+ darkMode: ["class"],
+ content: ["./index.html", "./src/**/*.{ts,tsx}"],
+ theme: {
+ container: {
+ center: true,
+ padding: "1.5rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ fontFamily: {
+ sans: ["Space Grotesk", "Pretendard Variable", ...fontFamily.sans],
+ },
+ boxShadow: {
+ card: "0 12px 40px rgba(7, 15, 26, 0.25)",
+ },
+ },
+ },
+ plugins: [require("tailwindcss-animate")],
+};
+
+export default config;
diff --git a/devfront/tests/clients.spec.ts b/devfront/tests/clients.spec.ts
new file mode 100644
index 00000000..a48c16fa
--- /dev/null
+++ b/devfront/tests/clients.spec.ts
@@ -0,0 +1,19 @@
+import { expect, test } from "@playwright/test";
+
+test("clients page loads correctly", async ({ page }) => {
+ await page.goto("/clients");
+
+ // 타이틀 확인
+ await expect(page).toHaveTitle(/바론 개발자 서비스/);
+
+ // 페이지 내 주요 텍스트 확인
+ await expect(page.getByText("클라이언트 목록")).toBeVisible();
+
+ // 테이블 헤더 확인
+ await expect(
+ page.getByRole("columnheader", { name: "애플리케이션" }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole("columnheader", { name: "Client ID" }),
+ ).toBeVisible();
+});
diff --git a/devfront/tests/example.spec.ts b/devfront/tests/example.spec.ts
new file mode 100644
index 00000000..53b304ae
--- /dev/null
+++ b/devfront/tests/example.spec.ts
@@ -0,0 +1,8 @@
+import { expect, test } from "@playwright/test";
+
+test("has title", async ({ page }) => {
+ await page.goto("/");
+
+ // Expect a title "to contain" a substring.
+ await expect(page).toHaveTitle(/바론 개발자 서비스/);
+});
diff --git a/devfront/tsconfig.app.json b/devfront/tsconfig.app.json
new file mode 100644
index 00000000..a9b5a59c
--- /dev/null
+++ b/devfront/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/devfront/tsconfig.json b/devfront/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/devfront/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/devfront/tsconfig.node.json b/devfront/tsconfig.node.json
new file mode 100644
index 00000000..8a67f62f
--- /dev/null
+++ b/devfront/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "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 */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/devfront/vite.config.ts b/devfront/vite.config.ts
new file mode 100644
index 00000000..fdf61f82
--- /dev/null
+++ b/devfront/vite.config.ts
@@ -0,0 +1,19 @@
+import react from "@vitejs/plugin-react";
+import { defineConfig } from "vite";
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ host: "0.0.0.0", // Ensure binding to all interfaces
+ proxy: {
+ "/api": {
+ target: process.env.API_PROXY_TARGET || "http://baron_backend:3000",
+ changeOrigin: true,
+ },
+ },
+ },
+ esbuild: {
+ drop: process.env.APP_ENV === "production" ? ["console", "debugger"] : [],
+ },
+});
diff --git a/docker-compose.yaml b/docker-compose.yaml
index e0ba67ce..9cb73a97 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -21,6 +21,7 @@ services:
- IDP_PROVIDER=${IDP_PROVIDER:-ory,descope}
- KRATOS_ADMIN_URL=${KRATOS_ADMIN_URL:-http://kratos:4434}
- HYDRA_ADMIN_URL=${HYDRA_ADMIN_URL:-http://hydra:4445}
+ - HYDRA_PUBLIC_URL=${HYDRA_PUBLIC_URL:-http://hydra:4444}
- DB_HOST=postgres
- CLICKHOUSE_HOST=clickhouse
- CLICKHOUSE_PORT=${CLICKHOUSE_PORT_NATIVE:-9000}
@@ -44,6 +45,42 @@ services:
retries: 3
start_period: 10s
+ adminfront:
+ build:
+ context: ./adminfront
+ dockerfile: Dockerfile
+ container_name: baron_adminfront
+ env_file:
+ - .env
+ environment:
+ - APP_ENV=${APP_ENV:-development}
+ - API_PROXY_TARGET=http://baron_backend:3000
+ ports:
+ - "${ADMIN_PORT:-5173}:5173"
+ volumes:
+ - ./adminfront:/app
+ - /app/node_modules
+ networks:
+ - baron_net
+
+ devfront:
+ build:
+ context: ./devfront
+ dockerfile: Dockerfile
+ container_name: baron_devfront
+ env_file:
+ - .env
+ environment:
+ - APP_ENV=${APP_ENV:-development}
+ - API_PROXY_TARGET=http://baron_backend:3000
+ ports:
+ - "${DEVFRONT_PORT:-5174}:5173"
+ volumes:
+ - ./devfront:/app
+ - /app/node_modules
+ networks:
+ - baron_net
+
userfront:
build:
context: ./userfront
diff --git a/docker/ory/hydra/hydra.yml b/docker/ory/hydra/hydra.yml
index b2a9d75e..f6e03c2c 100644
--- a/docker/ory/hydra/hydra.yml
+++ b/docker/ory/hydra/hydra.yml
@@ -3,6 +3,36 @@ dsn: memory
serve:
cookies:
same_site_mode: Lax
+ admin:
+ cors:
+ enabled: true
+ allowed_origins:
+ - "*"
+ allowed_methods:
+ - POST
+ - GET
+ - PUT
+ - PATCH
+ - DELETE
+ - CONNECT
+ - HEAD
+ - OPTIONS
+ - TRACE
+ allowed_headers:
+ - Authorization
+ - Accept
+ - Content-Type
+ - Content-Length
+ - Accept-Language
+ - Content-Language
+ exposed_headers:
+ - Content-Type
+ - Cache-Control
+ - Expires
+ - Last-Modified
+ - Pragma
+ - Content-Length
+ - Content-Language
public:
cors:
enabled: true
@@ -14,11 +44,25 @@ serve:
- PUT
- PATCH
- DELETE
+ - CONNECT
+ - HEAD
+ - OPTIONS
+ - TRACE
allowed_headers:
- Authorization
+ - Accept
- Content-Type
+ - Content-Length
+ - Accept-Language
+ - Content-Language
exposed_headers:
- Content-Type
+ - Cache-Control
+ - Expires
+ - Last-Modified
+ - Pragma
+ - Content-Length
+ - Content-Language
allow_credentials: true
urls:
@@ -27,11 +71,18 @@ urls:
consent: http://127.0.0.1:3000/consent
login: http://127.0.0.1:3000/login
logout: http://127.0.0.1:3000/logout
+ device:
+ verification: http://127.0.0.1:3000/device/verify
+ success: http://127.0.0.1:3000/device/success
secrets:
system:
- youReallyNeedToChangeThis
+webfinger:
+ oidc_discovery:
+ client_registration_url: http://127.0.0.1:4444/oauth2/register
+
oidc:
subject_identifiers:
supported_types:
@@ -39,3 +90,5 @@ oidc:
- public
pairwise:
salt: youReallyNeedToChangeThis
+ dynamic_client_registration:
+ enabled: true
diff --git a/docs/API_DESIGN_POLICY.md b/docs/API_DESIGN_POLICY.md
new file mode 100644
index 00000000..4e038bcf
--- /dev/null
+++ b/docs/API_DESIGN_POLICY.md
@@ -0,0 +1,119 @@
+# Baron SSO API Design Policy
+
+## 1. 개요 (Overview)
+본 문서는 Baron SSO 시스템의 백엔드 API 설계 원칙과 규약을 정의합니다. 모든 API 개발은 이 문서를 따름으로써 시스템의 일관성, 가독성, 유지보수성을 확보해야 합니다.
+
+## 2. API 버전 관리 및 URL 구조 (Versioning & URI)
+
+### 2.1 URL 구조
+모든 API는 아래의 기본 구조를 따릅니다.
+
+`[GET|POST|...] /api/{version}/{namespace}/{resource}[/{id}][/{action}]`
+
+* **Version**: `v1`, `v2` 등 메이저 버전 단위로 관리합니다.
+* **Namespace**: API의 사용 목적과 권한 범위를 구분합니다.
+ * `/auth`: 인증, 로그인, 비밀번호 찾기 등 (Public/User context)
+ * `/user`: 사용자 마이페이지, 프로필 수정 (Self-service, User context)
+ * `/admin`: 시스템 관리자 기능 (Admin context, Tenant aware)
+ * `/dev`: 개발자 포털 기능 (Developer context, RP management)
+* **Resource**: 리소스명은 **복수형(Plural)** 명사를 사용합니다. (예: `clients`, `audit-logs`)
+
+### 2.2 명명 규칙 (Naming Conventions)
+* **URL Path**: **kebab-case** (소문자, 하이픈 사용)
+ * `GET /api/v1/audit-logs` (O)
+ * `GET /api/v1/auditLogs` (X)
+* **Query Parameters**: **snake_case** 또는 **camelCase**를 허용하되, **camelCase**를 권장합니다.
+ * `GET /clients?clientId=...`
+* **JSON Fields**: **camelCase**를 엄격히 준수합니다. (프론트엔드 JS/TS/Dart 표준 준수)
+ * `{ "clientId": "...", "createdAt": "..." }` (O)
+ * `{ "client_id": "...", "created_at": "..." }` (X)
+ * *예외:* DB 모델을 직접 반환해야 하는 불가피한 레거시(ClickHouse 로그 등)는 예외를 두되, 가급적 DTO 변환을 권장합니다.
+
+## 3. HTTP 메서드 (HTTP Methods)
+
+리소스에 대한 행위는 HTTP 메서드로 표현합니다.
+
+| 메서드 | 용도 | 멱등성(Idempotent) | 예시 |
+| :--- | :--- | :--- | :--- |
+| **GET** | 리소스 조회 | O | `GET /clients` (목록), `GET /clients/:id` (상세) |
+| **POST** | 리소스 생성, 또는 복잡한 액션 | X | `POST /clients` (생성), `POST /auth/login` (액션) |
+| **PUT** | 리소스 전체 수정 (대체) | O | `PUT /users/:id` |
+| **PATCH** | 리소스 일부 수정 | O (권장) | `PATCH /clients/:id/status` |
+| **DELETE**| 리소스 삭제 | O | `DELETE /clients/:id` |
+
+## 4. 요청 및 응답 형식 (Request & Response)
+
+### 4.1 목록 조회 (List Response)
+목록 조회 시 반드시 페이지네이션 메타데이터를 포함해야 합니다.
+
+```json
+{
+ "items": [
+ { "id": "1", "name": "Resource A" },
+ { "id": "2", "name": "Resource B" }
+ ],
+ "limit": 50,
+ "offset": 0,
+ "total": 120 // 선택적 (성능 이슈 시 제외 가능)
+}
+```
+
+### 4.2 단건 조회/생성/수정 (Single Resource Response)
+데이터를 바로 반환하거나, 필요 시 래핑할 수 있습니다. 일관성을 위해 루트 객체로 반환하는 것을 권장합니다.
+
+```json
+{
+ "id": "1",
+ "name": "Resource A",
+ "status": "active"
+}
+```
+
+### 4.3 에러 응답 (Error Response)
+모든 에러는 일관된 포맷을 유지해야 합니다. 프로덕션 환경에서는 내부 스택 트레이스를 노출하지 않아야 합니다.
+
+**HTTP Status Code 활용:**
+* `400 Bad Request`: 입력값 검증 실패
+* `401 Unauthorized`: 인증 토큰 없음/만료
+* `403 Forbidden`: 권한 부족 (토큰은 있으나 접근 불가)
+* `404 Not Found`: 리소스 없음
+* `409 Conflict`: 데이터 충돌 (중복 생성 등)
+* `429 Too Many Requests`: 레이트 리밋 초과
+* `500 Internal Server Error`: 서버 내부 오류 (상세 내용 마스킹)
+* `503 Service Unavailable`: 외부 의존성(Hydra, DB 등) 연결 실패
+
+**JSON Body:**
+```json
+{
+ "error": "사람이 읽을 수 있는 에러 메시지",
+ "code": "MACHINE_READABLE_CODE", // 선택적 (예: USER_NOT_FOUND, HYDRA_CONN_ERR)
+ "details": { ... } // 선택적 (Validation error 필드별 상세 등)
+}
+```
+
+## 5. 헤더 및 보안 (Headers & Security)
+
+### 5.1 인증 (Authentication)
+* **Authorization**: `Bearer {token}` 형식을 사용합니다.
+ * Backend는 Gateway 또는 Middleware에서 토큰을 검증하고 `User Context`를 생성해야 합니다.
+
+### 5.2 테넌트 격리 (Multi-tenancy)
+* **X-Tenant-ID**: 관리자 API(`admin`) 호출 시, 대상 테넌트를 식별하기 위해 필수적으로 사용합니다.
+ * 슈퍼 어드민이 아닐 경우, 요청자의 권한과 헤더의 테넌트가 일치하는지 검증해야 합니다.
+
+### 5.3 요청 추적 (Tracing)
+* **X-Request-ID**: 모든 요청/응답에 고유 ID를 포함하여 로그 추적성을 확보합니다. 클라이언트가 보내지 않으면 서버가 생성합니다.
+
+## 6. 개발 가이드라인 (Implementation Guidelines)
+
+### 6.1 DTO 사용
+* DB 모델(Gorm, ClickHouse struct)을 그대로 API 응답으로 내보내지 마십시오.
+* 반드시 **Response DTO** 구조체를 별도로 정의하여 `json` 태그를 통해 명명 규칙(camelCase)을 적용하고, 민감한 정보(비밀번호 해시, 내부 ID 등)를 필터링해야 합니다.
+
+### 6.2 핸들러 구조
+* 핸들러는 `Service` 레이어를 호출하고, HTTP 요청/응답 처리(파싱, 상태 코드 매핑)에만 집중해야 합니다.
+* 비즈니스 로직은 `Handler`가 아닌 `Service` 또는 `Domain` 레이어에 위치해야 합니다.
+
+### 6.3 로깅 정책
+* 요청/응답 로그는 미들웨어 레벨에서 처리합니다.
+* 에러 발생 시 `slog.Error`를 통해 스택 트레이스와 컨텍스트를 남기고, 클라이언트에게는 정제된 메시지만 전달합니다.
diff --git a/mcp/hydra-mcp/Dockerfile b/mcp/hydra-mcp/Dockerfile
new file mode 100644
index 00000000..3314add6
--- /dev/null
+++ b/mcp/hydra-mcp/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:20-alpine
+
+WORKDIR /app
+
+COPY package.json ./
+RUN npm install --omit=dev
+
+COPY src ./src
+
+ENV NODE_ENV=production
+
+CMD ["node", "./src/index.js"]
diff --git a/mcp/hydra-mcp/package.json b/mcp/hydra-mcp/package.json
new file mode 100644
index 00000000..9c7275c6
--- /dev/null
+++ b/mcp/hydra-mcp/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "mcp-ory-hydra",
+ "version": "0.1.0",
+ "description": "MCP server for Ory Hydra Admin/Public APIs",
+ "type": "module",
+ "bin": {
+ "mcp-ory-hydra": "./src/runner.js"
+ },
+ "scripts": {
+ "start": "node ./src/index.js"
+ },
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.25.0",
+ "zod": "^3.25.0"
+ }
+}
diff --git a/mcp/hydra-mcp/src/index.js b/mcp/hydra-mcp/src/index.js
new file mode 100755
index 00000000..5484d679
--- /dev/null
+++ b/mcp/hydra-mcp/src/index.js
@@ -0,0 +1,324 @@
+#!/usr/bin/env node
+import { createRequire } from "node:module";
+import path from "node:path";
+import { pathToFileURL } from "node:url";
+
+const modulesBase = process.env.MCP_MODULES_DIR;
+const requireFromModules = createRequire(
+ modulesBase ? path.join(modulesBase, "package.json") : import.meta.url,
+);
+
+const mcpModule = await import(resolveModule("@modelcontextprotocol/sdk/server/mcp.js"));
+const stdioModule = await import(resolveModule("@modelcontextprotocol/sdk/server/stdio.js"));
+const zodModule = await import(resolveModule("zod"));
+
+const { McpServer } = mcpModule;
+const { StdioServerTransport } = stdioModule;
+const { z } = zodModule;
+
+const hydraPublicUrl = process.env.HYDRA_PUBLIC_URL ?? "http://127.0.0.1:4444";
+const hydraAdminUrl = process.env.HYDRA_ADMIN_URL ?? "http://127.0.0.1:4445";
+const adminApiToken = process.env.HYDRA_ADMIN_API_TOKEN;
+const publicApiToken = process.env.HYDRA_PUBLIC_API_TOKEN;
+const timeoutMs = Number.parseInt(process.env.HYDRA_HTTP_TIMEOUT_MS ?? "15000", 10);
+
+class HttpError extends Error {
+ constructor(message, status, body, url) {
+ super(message);
+ this.name = "HttpError";
+ this.status = status;
+ this.body = body;
+ this.url = url;
+ }
+}
+
+function resolveModule(specifier) {
+ const resolvedPath = requireFromModules.resolve(specifier);
+ return pathToFileURL(resolvedPath).href;
+}
+
+function buildUrl(base, path, query) {
+ const url = new URL(path, base);
+ if (query) {
+ for (const [key, value] of Object.entries(query)) {
+ if (value === undefined || value === null || value === "") {
+ continue;
+ }
+ url.searchParams.set(key, String(value));
+ }
+ }
+ return url.toString();
+}
+
+async function requestJson(url, { method = "GET", headers, body } = {}, token) {
+ const controller = new AbortController();
+ const timeoutId = Number.isFinite(timeoutMs)
+ ? setTimeout(() => controller.abort(), timeoutMs)
+ : null;
+
+ const requestHeaders = {
+ accept: "application/json",
+ ...headers,
+ };
+ if (token) {
+ requestHeaders.authorization = `Bearer ${token}`;
+ }
+
+ try {
+ const response = await fetch(url, {
+ method,
+ headers: requestHeaders,
+ body,
+ signal: controller.signal,
+ });
+
+ const contentType = response.headers.get("content-type") ?? "";
+ const text = await response.text();
+ const data = text
+ ? contentType.includes("application/json")
+ ? safeJsonParse(text)
+ : text
+ : null;
+
+ if (!response.ok) {
+ throw new HttpError(`HTTP ${response.status} ${response.statusText}`, response.status, data, url);
+ }
+
+ return {
+ status: response.status,
+ data,
+ };
+ } finally {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ }
+}
+
+function safeJsonParse(text) {
+ try {
+ return JSON.parse(text);
+ } catch {
+ return text;
+ }
+}
+
+function formatToolResult(payload) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: JSON.stringify(payload, null, 2),
+ },
+ ],
+ };
+}
+
+function formatErrorResult(error) {
+ if (error instanceof HttpError) {
+ return formatToolResult({
+ error: {
+ message: error.message,
+ status: error.status,
+ url: error.url,
+ body: error.body,
+ },
+ });
+ }
+
+ return formatToolResult({
+ error: {
+ message: error instanceof Error ? error.message : String(error),
+ },
+ });
+}
+
+const HealthInputSchema = z.object({
+ service: z.enum(["public", "admin"]).optional(),
+ probe: z.enum(["alive", "ready"]).optional(),
+});
+
+const ListClientsInputSchema = z.object({
+ limit: z.number().int().positive().max(500).optional(),
+ offset: z.number().int().min(0).optional(),
+ page_size: z.number().int().positive().max(500).optional(),
+ page_token: z.string().min(1).optional(),
+});
+
+const ClientIdInputSchema = z.object({
+ client_id: z.string().min(1),
+});
+
+const ClientPayloadInputSchema = z.object({
+ client_id: z.string().min(1),
+ payload: z.record(z.unknown()),
+});
+
+const RegisterClientInputSchema = z.object({
+ payload: z.record(z.unknown()),
+});
+
+async function main() {
+ const server = new McpServer({
+ name: "mcp-ory-hydra",
+ version: "0.1.0",
+ });
+
+ server.tool(
+ "hydra_health",
+ "Check Hydra health using /health/alive or /health/ready on public/admin ports.",
+ HealthInputSchema.shape,
+ async (input) => {
+ const service = input.service ?? "admin";
+ const probe = input.probe ?? "ready";
+ const base = service === "public" ? hydraPublicUrl : hydraAdminUrl;
+ const url = buildUrl(base, `/health/${probe}`);
+
+ try {
+ const result = await requestJson(url, {}, service === "public" ? publicApiToken : adminApiToken);
+ return formatToolResult({
+ service,
+ probe,
+ status: result.status,
+ data: result.data,
+ });
+ } catch (error) {
+ return formatErrorResult(error);
+ }
+ },
+ );
+
+ server.tool(
+ "hydra_list_clients",
+ "List OAuth2 clients from Hydra Admin API.",
+ ListClientsInputSchema.shape,
+ async (input) => {
+ const url = buildUrl(hydraAdminUrl, "/clients", {
+ limit: input.limit,
+ offset: input.offset,
+ page_size: input.page_size,
+ page_token: input.page_token,
+ });
+
+ try {
+ const result = await requestJson(url, {}, adminApiToken);
+ return formatToolResult({
+ status: result.status,
+ data: result.data,
+ });
+ } catch (error) {
+ return formatErrorResult(error);
+ }
+ },
+ );
+
+ server.tool(
+ "hydra_get_client",
+ "Get an OAuth2 client by client_id from Hydra Admin API.",
+ ClientIdInputSchema.shape,
+ async (input) => {
+ const url = buildUrl(hydraAdminUrl, `/clients/${encodeURIComponent(input.client_id)}`);
+
+ try {
+ const result = await requestJson(url, {}, adminApiToken);
+ return formatToolResult({
+ status: result.status,
+ data: result.data,
+ });
+ } catch (error) {
+ return formatErrorResult(error);
+ }
+ },
+ );
+
+ server.tool(
+ "hydra_register_client",
+ "Register an OAuth2 client via Hydra public dynamic client registration endpoint.",
+ RegisterClientInputSchema.shape,
+ async (input) => {
+ const url = buildUrl(hydraPublicUrl, "/oauth2/register");
+
+ try {
+ const result = await requestJson(
+ url,
+ {
+ method: "POST",
+ headers: {
+ "content-type": "application/json",
+ },
+ body: JSON.stringify(input.payload ?? {}),
+ },
+ publicApiToken,
+ );
+ return formatToolResult({
+ status: result.status,
+ data: result.data,
+ });
+ } catch (error) {
+ return formatErrorResult(error);
+ }
+ },
+ );
+
+ server.tool(
+ "hydra_update_client",
+ "Update an OAuth2 client via Hydra Admin API.",
+ ClientPayloadInputSchema.shape,
+ async (input) => {
+ const url = buildUrl(hydraAdminUrl, `/clients/${encodeURIComponent(input.client_id)}`);
+
+ try {
+ const result = await requestJson(
+ url,
+ {
+ method: "PUT",
+ headers: {
+ "content-type": "application/json",
+ },
+ body: JSON.stringify(input.payload ?? {}),
+ },
+ adminApiToken,
+ );
+ return formatToolResult({
+ status: result.status,
+ data: result.data,
+ });
+ } catch (error) {
+ return formatErrorResult(error);
+ }
+ },
+ );
+
+ server.tool(
+ "hydra_delete_client",
+ "Delete an OAuth2 client via Hydra Admin API.",
+ ClientIdInputSchema.shape,
+ async (input) => {
+ const url = buildUrl(hydraAdminUrl, `/clients/${encodeURIComponent(input.client_id)}`);
+
+ try {
+ const result = await requestJson(
+ url,
+ {
+ method: "DELETE",
+ },
+ adminApiToken,
+ );
+ return formatToolResult({
+ status: result.status,
+ data: result.data,
+ });
+ } catch (error) {
+ return formatErrorResult(error);
+ }
+ },
+ );
+
+ const transport = new StdioServerTransport();
+ await server.connect(transport);
+}
+
+main().catch((error) => {
+ console.error(error instanceof Error ? error.message : String(error));
+ process.exit(1);
+});
diff --git a/mcp/hydra-mcp/src/runner.js b/mcp/hydra-mcp/src/runner.js
new file mode 100755
index 00000000..5e68503c
--- /dev/null
+++ b/mcp/hydra-mcp/src/runner.js
@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+import { spawn, spawnSync } from "node:child_process";
+import { existsSync, mkdirSync } from "node:fs";
+import { homedir } from "node:os";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const sdkVersion = "^1.25.0";
+const zodVersion = "^3.25.0";
+const cacheRoot = resolveCacheRoot();
+const sdkMarker = path.join(cacheRoot, "node_modules", "@modelcontextprotocol", "sdk", "package.json");
+
+if (!existsSync(sdkMarker)) {
+ mkdirSync(cacheRoot, { recursive: true });
+
+ const installResult = spawnSync(
+ "npm",
+ [
+ "install",
+ "--no-audit",
+ "--no-fund",
+ "--prefix",
+ cacheRoot,
+ `@modelcontextprotocol/sdk@${sdkVersion}`,
+ `zod@${zodVersion}`,
+ ],
+ {
+ encoding: "utf-8",
+ env: {
+ ...process.env,
+ npm_config_loglevel: "error",
+ },
+ },
+ );
+
+ if (installResult.stdout) {
+ process.stderr.write(installResult.stdout);
+ }
+ if (installResult.stderr) {
+ process.stderr.write(installResult.stderr);
+ }
+
+ if (installResult.status !== 0) {
+ process.exit(installResult.status ?? 1);
+ }
+}
+
+const env = {
+ ...process.env,
+ MCP_MODULES_DIR: cacheRoot,
+};
+
+const entryPath = fileURLToPath(new URL("./index.js", import.meta.url));
+const child = spawn(process.execPath, [entryPath], {
+ stdio: "inherit",
+ env,
+});
+
+child.on("exit", (code) => {
+ process.exit(code ?? 0);
+});
+
+function resolveCacheRoot() {
+ if (process.env.MCP_ORY_HYDRA_CACHE_DIR) {
+ return process.env.MCP_ORY_HYDRA_CACHE_DIR;
+ }
+
+ const baseCache = process.env.XDG_CACHE_HOME ?? path.join(homedir(), ".cache");
+ return path.join(baseCache, "mcp-ory-hydra");
+}
diff --git a/mcp/kratos-mcp/Dockerfile b/mcp/kratos-mcp/Dockerfile
new file mode 100644
index 00000000..491af763
--- /dev/null
+++ b/mcp/kratos-mcp/Dockerfile
@@ -0,0 +1,9 @@
+FROM node:20-alpine
+
+WORKDIR /app
+
+RUN npm install -g mcp-ory-kratos
+
+ENV NODE_ENV=production
+
+CMD ["mcp-ory-kratos"]