From 544a2f34284c384c9751793eeafda992bb9b542e Mon Sep 17 00:00:00 2001 From: Eric Zheng Date: Thu, 23 Apr 2026 18:58:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96web?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 8 + .../next_全量对接_go_api_e862d400.plan.md | 568 ++ .../oauth2_pkce_单体设计_62bcf65b.plan.md | 232 + .gitignore | 43 + cmd/server/main.go | 45 + cmd/server/wire.go | 27 + configs/config.yml.example | 39 + docs/auth-api.md | 66 + docs/cors-https-checklist.md | 13 + docs/oauth-v2.md | 2413 +++++ go.mod | 69 + go.sum | 164 + internal/auth/handler/login.go | 189 + internal/auth/http_register.go | 38 + internal/auth/middleware/bearer.go | 41 + internal/auth/middleware/ratelimit.go | 57 + internal/auth/oauth2/errors.go | 10 + internal/auth/oauth2/handler.go | 28 + internal/auth/oauth2/model.go | 64 + internal/auth/oauth2/pkce.go | 23 + internal/auth/oauth2/service.go | 341 + internal/auth/oauth2/store.go | 265 + internal/auth/scope/scope.go | 44 + internal/auth/session/store.go | 76 + internal/auth/wire_provider.go | 43 + internal/data/provider.go | 9 + internal/iam/entity/dept.go | 24 + internal/iam/entity/menu.go | 32 + internal/iam/entity/role.go | 43 + internal/iam/entity/tenant.go | 25 + internal/iam/entity/user.go | 53 + internal/iam/handler/dept_handler.go | 95 + internal/iam/handler/helpers.go | 53 + internal/iam/handler/menu_handler.go | 141 + internal/iam/handler/role_handler.go | 117 + internal/iam/handler/tenant_handler.go | 98 + internal/iam/handler/user_handler.go | 123 + internal/iam/http_register.go | 87 + internal/iam/repository/dept_repository.go | 93 + internal/iam/repository/errors.go | 10 + internal/iam/repository/menu_repository.go | 111 + internal/iam/repository/role_repository.go | 147 + internal/iam/repository/tenant_repository.go | 109 + internal/iam/repository/user_repository.go | 179 + internal/iam/service/constants.go | 4 + internal/iam/service/dept_service.go | 326 + internal/iam/service/menu_service.go | 319 + internal/iam/service/role_service.go | 223 + internal/iam/service/scope.go | 17 + internal/iam/service/tenant_service.go | 261 + internal/iam/service/user_service.go | 240 + internal/iam/wire_provider.go | 45 + internal/server/cors.go | 32 + internal/server/grpc.go | 26 + internal/server/http.go | 67 + internal/server/server.go | 25 + internal/system/entity/param_entity.go | 96 + internal/system/handler/param_handler.go | 177 + internal/system/http_register.go | 33 + .../system/repository/param_repository.go | 139 + internal/system/service/param_service.go | 271 + internal/system/wire_provider.go | 32 + migrations/postgres/001_iam.sql | 132 + migrations/postgres/002_system.sql | 25 + .../postgres/003_seed_platform_tenant.sql | 17 + .../postgres/004_seed_platform_builtin.sql | 122 + migrations/postgres/005_oauth.sql | 66 + .../postgres/006_seed_iam_menu_full.sql | 209 + migrations/postgres/README.md | 27 + pkg/cache/cache.go | 184 + pkg/config/config.go | 84 + pkg/db/db.go | 27 + pkg/db/mysql_db.go | 1 + pkg/db/pg_db.go | 18 + pkg/security/token.go | 15 + pkg/utils/codec/bcrypt.go | 19 + pkg/utils/id/id.go | 21 + pkg/utils/id/uuid_generator.go | 15 + web/.env.example | 7 + web/.gitignore | 42 + web/.prettierignore | 6 + web/.prettierrc | 6 + web/README.md | 19 + .../(main)/(iam)/(devops)/resource/page.tsx | 22 + web/app/(main)/(iam)/(devops)/tenant/page.tsx | 46 + web/app/(main)/(iam)/dept/page.tsx | 19 + web/app/(main)/(iam)/role/page.tsx | 43 + web/app/(main)/(iam)/user/page.tsx | 43 + web/app/(main)/dashboard/account/page.tsx | 8 + web/app/(main)/dashboard/page.tsx | 8 + web/app/(main)/layout.tsx | 9 + web/app/favicon.ico | Bin 0 -> 25931 bytes web/app/globals.css | 55 + web/app/layout.tsx | 33 + web/app/login/page.tsx | 128 + web/app/oauth/callback/page.tsx | 67 + web/app/page.tsx | 23 + web/components/auth/LoginModal.tsx | 118 + web/components/feedback/ToastHost.tsx | 42 + web/components/iam/DeptTreeView.tsx | 23 + web/components/iam/IamSectionCard.tsx | 15 + web/components/iam/MenuTreeView.tsx | 29 + web/components/layout/AppChrome.tsx | 179 + web/components/layout/AuthenticatedLayout.tsx | 22 + .../layout/ClassicCascadeFlyout.tsx | 214 + .../layout/ClassicCollapsedSidebar.tsx | 190 + web/components/layout/IconSidebarLayout.tsx | 180 + web/components/layout/NavHeader.tsx | 18 + web/components/layout/RequireAuth.tsx | 44 + web/components/layout/SidebarNav.tsx | 68 + web/components/layout/TabStrip.tsx | 99 + web/components/layout/TenantSwitcher.tsx | 101 + web/components/layout/UserMenu.tsx | 127 + web/components/layout/nav-shared.tsx | 264 + web/components/layout/nav-tooltip.tsx | 37 + web/components/providers/AppProviders.tsx | 47 + web/components/providers/ErrorBoundary.tsx | 49 + web/eslint.config.mjs | 21 + web/lib/api/auth.ts | 184 + web/lib/api/client.ts | 111 + web/lib/api/iam.ts | 99 + web/lib/api/paths.ts | 2 + web/lib/api/refresh-flight.ts | 33 + web/lib/api/system-param.ts | 27 + web/lib/api/types.ts | 17 + web/lib/api/types/dept.ts | 18 + web/lib/api/types/menu.ts | 15 + web/lib/api/types/role.ts | 13 + web/lib/api/types/system-param.ts | 13 + web/lib/api/types/tenant.ts | 11 + web/lib/api/types/user.ts | 13 + web/lib/env.ts | 30 + web/lib/hooks/use-api.ts | 60 + web/lib/hooks/use-flyout-state.ts | 129 + web/lib/hooks/use-menu-navigation.ts | 14 + web/lib/hooks/use-nav-menu.ts | 70 + web/lib/hooks/use-user-profile.ts | 97 + web/lib/iam/list-helpers.ts | 9 + web/lib/navigation/safe-return.ts | 11 + web/lib/notify/auth-events.ts | 26 + web/lib/oauth/browser.ts | 39 + web/lib/oauth/pkce.ts | 19 + web/lib/sync/logout-broadcast.ts | 58 + web/next.config.ts | 7 + web/package-lock.json | 8064 +++++++++++++++++ web/package.json | 60 + web/pnpm-lock.yaml | 5412 +++++++++++ web/postcss.config.mjs | 5 + web/public/file.svg | 1 + web/public/globe.svg | 1 + web/public/next.svg | 1 + web/public/vercel.svg | 1 + web/public/window.svg | 1 + web/stores/auth-store.ts | 78 + web/stores/auth-ui-store.ts | 17 + web/stores/layout-store.ts | 84 + web/stores/tab-store.ts | 119 + web/stores/tenant-store.ts | 59 + web/stores/toast-store.ts | 31 + web/tsconfig.json | 27 + 160 files changed, 27327 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .cursor/plans/next_全量对接_go_api_e862d400.plan.md create mode 100644 .cursor/plans/oauth2_pkce_单体设计_62bcf65b.plan.md create mode 100644 .gitignore create mode 100644 cmd/server/main.go create mode 100644 cmd/server/wire.go create mode 100644 configs/config.yml.example create mode 100644 docs/auth-api.md create mode 100644 docs/cors-https-checklist.md create mode 100644 docs/oauth-v2.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/auth/handler/login.go create mode 100644 internal/auth/http_register.go create mode 100644 internal/auth/middleware/bearer.go create mode 100644 internal/auth/middleware/ratelimit.go create mode 100644 internal/auth/oauth2/errors.go create mode 100644 internal/auth/oauth2/handler.go create mode 100644 internal/auth/oauth2/model.go create mode 100644 internal/auth/oauth2/pkce.go create mode 100644 internal/auth/oauth2/service.go create mode 100644 internal/auth/oauth2/store.go create mode 100644 internal/auth/scope/scope.go create mode 100644 internal/auth/session/store.go create mode 100644 internal/auth/wire_provider.go create mode 100644 internal/data/provider.go create mode 100644 internal/iam/entity/dept.go create mode 100644 internal/iam/entity/menu.go create mode 100644 internal/iam/entity/role.go create mode 100644 internal/iam/entity/tenant.go create mode 100644 internal/iam/entity/user.go create mode 100644 internal/iam/handler/dept_handler.go create mode 100644 internal/iam/handler/helpers.go create mode 100644 internal/iam/handler/menu_handler.go create mode 100644 internal/iam/handler/role_handler.go create mode 100644 internal/iam/handler/tenant_handler.go create mode 100644 internal/iam/handler/user_handler.go create mode 100644 internal/iam/http_register.go create mode 100644 internal/iam/repository/dept_repository.go create mode 100644 internal/iam/repository/errors.go create mode 100644 internal/iam/repository/menu_repository.go create mode 100644 internal/iam/repository/role_repository.go create mode 100644 internal/iam/repository/tenant_repository.go create mode 100644 internal/iam/repository/user_repository.go create mode 100644 internal/iam/service/constants.go create mode 100644 internal/iam/service/dept_service.go create mode 100644 internal/iam/service/menu_service.go create mode 100644 internal/iam/service/role_service.go create mode 100644 internal/iam/service/scope.go create mode 100644 internal/iam/service/tenant_service.go create mode 100644 internal/iam/service/user_service.go create mode 100644 internal/iam/wire_provider.go create mode 100644 internal/server/cors.go create mode 100644 internal/server/grpc.go create mode 100644 internal/server/http.go create mode 100644 internal/server/server.go create mode 100644 internal/system/entity/param_entity.go create mode 100644 internal/system/handler/param_handler.go create mode 100644 internal/system/http_register.go create mode 100644 internal/system/repository/param_repository.go create mode 100644 internal/system/service/param_service.go create mode 100644 internal/system/wire_provider.go create mode 100644 migrations/postgres/001_iam.sql create mode 100644 migrations/postgres/002_system.sql create mode 100644 migrations/postgres/003_seed_platform_tenant.sql create mode 100644 migrations/postgres/004_seed_platform_builtin.sql create mode 100644 migrations/postgres/005_oauth.sql create mode 100644 migrations/postgres/006_seed_iam_menu_full.sql create mode 100644 migrations/postgres/README.md create mode 100644 pkg/cache/cache.go create mode 100644 pkg/config/config.go create mode 100644 pkg/db/db.go create mode 100644 pkg/db/mysql_db.go create mode 100644 pkg/db/pg_db.go create mode 100644 pkg/security/token.go create mode 100644 pkg/utils/codec/bcrypt.go create mode 100644 pkg/utils/id/id.go create mode 100644 pkg/utils/id/uuid_generator.go create mode 100644 web/.env.example create mode 100644 web/.gitignore create mode 100644 web/.prettierignore create mode 100644 web/.prettierrc create mode 100644 web/README.md create mode 100644 web/app/(main)/(iam)/(devops)/resource/page.tsx create mode 100644 web/app/(main)/(iam)/(devops)/tenant/page.tsx create mode 100644 web/app/(main)/(iam)/dept/page.tsx create mode 100644 web/app/(main)/(iam)/role/page.tsx create mode 100644 web/app/(main)/(iam)/user/page.tsx create mode 100644 web/app/(main)/dashboard/account/page.tsx create mode 100644 web/app/(main)/dashboard/page.tsx create mode 100644 web/app/(main)/layout.tsx create mode 100644 web/app/favicon.ico create mode 100644 web/app/globals.css create mode 100644 web/app/layout.tsx create mode 100644 web/app/login/page.tsx create mode 100644 web/app/oauth/callback/page.tsx create mode 100644 web/app/page.tsx create mode 100644 web/components/auth/LoginModal.tsx create mode 100644 web/components/feedback/ToastHost.tsx create mode 100644 web/components/iam/DeptTreeView.tsx create mode 100644 web/components/iam/IamSectionCard.tsx create mode 100644 web/components/iam/MenuTreeView.tsx create mode 100644 web/components/layout/AppChrome.tsx create mode 100644 web/components/layout/AuthenticatedLayout.tsx create mode 100644 web/components/layout/ClassicCascadeFlyout.tsx create mode 100644 web/components/layout/ClassicCollapsedSidebar.tsx create mode 100644 web/components/layout/IconSidebarLayout.tsx create mode 100644 web/components/layout/NavHeader.tsx create mode 100644 web/components/layout/RequireAuth.tsx create mode 100644 web/components/layout/SidebarNav.tsx create mode 100644 web/components/layout/TabStrip.tsx create mode 100644 web/components/layout/TenantSwitcher.tsx create mode 100644 web/components/layout/UserMenu.tsx create mode 100644 web/components/layout/nav-shared.tsx create mode 100644 web/components/layout/nav-tooltip.tsx create mode 100644 web/components/providers/AppProviders.tsx create mode 100644 web/components/providers/ErrorBoundary.tsx create mode 100644 web/eslint.config.mjs create mode 100644 web/lib/api/auth.ts create mode 100644 web/lib/api/client.ts create mode 100644 web/lib/api/iam.ts create mode 100644 web/lib/api/paths.ts create mode 100644 web/lib/api/refresh-flight.ts create mode 100644 web/lib/api/system-param.ts create mode 100644 web/lib/api/types.ts create mode 100644 web/lib/api/types/dept.ts create mode 100644 web/lib/api/types/menu.ts create mode 100644 web/lib/api/types/role.ts create mode 100644 web/lib/api/types/system-param.ts create mode 100644 web/lib/api/types/tenant.ts create mode 100644 web/lib/api/types/user.ts create mode 100644 web/lib/env.ts create mode 100644 web/lib/hooks/use-api.ts create mode 100644 web/lib/hooks/use-flyout-state.ts create mode 100644 web/lib/hooks/use-menu-navigation.ts create mode 100644 web/lib/hooks/use-nav-menu.ts create mode 100644 web/lib/hooks/use-user-profile.ts create mode 100644 web/lib/iam/list-helpers.ts create mode 100644 web/lib/navigation/safe-return.ts create mode 100644 web/lib/notify/auth-events.ts create mode 100644 web/lib/oauth/browser.ts create mode 100644 web/lib/oauth/pkce.ts create mode 100644 web/lib/sync/logout-broadcast.ts create mode 100644 web/next.config.ts create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/pnpm-lock.yaml create mode 100644 web/postcss.config.mjs create mode 100644 web/public/file.svg create mode 100644 web/public/globe.svg create mode 100644 web/public/next.svg create mode 100644 web/public/vercel.svg create mode 100644 web/public/window.svg create mode 100644 web/stores/auth-store.ts create mode 100644 web/stores/auth-ui-store.ts create mode 100644 web/stores/layout-store.ts create mode 100644 web/stores/tab-store.ts create mode 100644 web/stores/tenant-store.ts create mode 100644 web/stores/toast-store.ts create mode 100644 web/tsconfig.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..ac0694e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run *我使用的是 pnpm)", + "Bash(npx next *)" + ] + } +} diff --git a/.cursor/plans/next_全量对接_go_api_e862d400.plan.md b/.cursor/plans/next_全量对接_go_api_e862d400.plan.md new file mode 100644 index 0000000..de87be3 --- /dev/null +++ b/.cursor/plans/next_全量对接_go_api_e862d400.plan.md @@ -0,0 +1,568 @@ +--- +name: Next 全量对接 Go API +overview: 全栈对接 Go API;OAuth2 Code + PKCE;**401/403 与 200+`code` 分工**(**含:密码错=401,只看状态**);JSON 信封;login 的 401 不 refresh;dev 跨域;登出多 Tab;auth/login 改造 §3;租户关 Tabs;路由守卫。 +todos: + - id: init-next-env + content: 初始化 Next+TS+Prettier,配置 NEXT_PUBLIC_API_ORIGIN 与目录结构(api/、stores/) + status: completed + - id: api-client-auth + content: 实现 apiClient(JSON + Bearer;OAuth token 用 form);auth/login 取 code 再 oauth/token;logout;Zustand useAuthStore + status: completed + - id: go-auth-login-pkce + content: Go:改造 POST /api/v1/auth/login 密码通过后签发 PKCE 绑定 code,与 CreateAuthorizationCode+token 复用;文档 oauth-v2 对齐 + status: completed + - id: api-iam-system + content: 按 http_register 补齐 iam(tenant/dept/role/user/menu)与 system/param 全部封装 + status: completed + - id: oauth-pkce-ui + content: 实现 OAuth2 PKCE 授权链接、callback 页、换 token 与 token 刷新策略 + status: completed + - id: cors-cookie + content: 与 Go 对齐 CORS、Cookie SameSite/credentials;生产 HTTPS 检查清单 + status: completed + - id: layout-shell-nav + content: AppShell;顶栏用户下拉;侧栏后端 nav;经典/图标+localStorage;path 映射路由(首版 web 为顶栏+入口,侧栏与双模式待迭代) + status: completed + - id: workbench-tabs-content + content: 主工作区 Tabs+页签状态+内容区(首版 TabStrip 骨架;右键/滚动钮/标准列表/左树右表待迭代) + status: completed +isProject: false +--- + +# Next.js 前端全量对接 Go 后端方案 + +## 后端路由清单(对接范围) + +以下均基于当前仓库中的注册代码([`internal/auth/http_register.go`](internal/auth/http_register.go)、[`internal/iam/http_register.go`](internal/iam/http_register.go)、[`internal/system/http_register.go`](internal/system/http_register.go)、[`internal/server/http.go`](internal/server/http.go))。 + +| 前缀 | 说明 | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `GET /health` | 探活 | +| **根路径 OAuth** | `GET /oauth/authorize`、`POST /oauth/token`、`POST /oauth/introspect`(**无** `/api/v1` 前缀) | +| **`/api/v1`** | 全组挂载 Bearer 中间件(无 token 仍放行,见 [`internal/auth/middleware/bearer.go`](internal/auth/middleware/bearer.go)) | + +**`/api/v1` 下具体接口:** + +- **Auth**:`POST /api/v1/auth/login`、`POST /api/v1/auth/logout` +- **IAM**([`internal/iam/http_register.go`](internal/iam/http_register.go)):`/api/v1/iam/tenant/*`、`/dept/*`、`/role/*`、`/user/*`、`/menu/*`(含 `tree`、`nav`、`perms` 等) +- **System**:`/api/v1/system/param/*`(create/update/delete-batch/get/list) + +前端需要为上述路径提供 **baseURL 配置**(例如 `NEXT_PUBLIC_API_ORIGIN=http://127.0.0.1:8000`),并区分两类调用: + +- **业务 JSON API**:`fetch(\`${origin}/api/v1/...\`)` +- **OAuth**:`fetch(\`${origin}/oauth/token\`, …)`等(表单`application/x-www-form-urlencoded`) + +```mermaid +flowchart LR + subgraph next [Nextjs] + Pages[页面与Radix组件] + Store[Zustand状态] + Client[api客户端层] + end + subgraph go [Go] + Health["/health"] + OAuth["/oauth/*"] + API["/api/v1/*"] + end + Pages --> Store + Store --> Client + Client --> Health + Client --> OAuth + Client --> API +``` + +--- + +## 架构建议 + +### 1. 环境与请求基址 + +- 使用 `NEXT_PUBLIC_API_ORIGIN`(仅 scheme+host+port,**不要**带尾部 `/api/v1`,便于拼 `/api/v1` 与 `/oauth`)。 +- **部署**:**上线后** Next 与 Go **同一站点/同域**(或反代为同源),Cookie(若仍用于 authorize 兼容)与 CSRF 更简单。**开发阶段** 常为 **前端 localhost:3000 + 后端 :8000 跨域**,Go 必须配置 **CORS 白名单**(具体 `http://localhost:3000` 等),`Access-Control-Allow-Origin` **禁止 `*`** 若带 `credentials`;Bearer 主路径下跨域以 **`Authorization`** 为主,但仍需为 **预检 OPTIONS** 与 **错误 JSON** 配好 CORS。 + +### 2. HTTP 客户端层(对接「所有接口」的核心) + +- 封装单一 **`apiClient`**(`fetch` 或 `ky`/`axios` 二选一),职责: + - 统一 `baseURL`、`Content-Type: application/json`(OAuth token 端点单独走 **form-urlencoded**)。 + - 从 Zustand(或内存)读取 **access_token**,对 `/api/v1/**` 自动加 `Authorization: Bearer `。 + - **HTTP 401**:**先 refresh**(见 §3),失败再 **弹窗**;**HTTP 200** 时读 **`body.code`** 判断业务成败(见 §「JSON 统一响应」)。 + - 可选:`X-Tenant-ID` 等与后端 [`internal/iam/handler/helpers.go`](internal/iam/handler/helpers.go) 兼容。 +- **按领域拆模块**(与后端包对齐,便于维护): + - `api/auth.ts` — login/logout + - `api/oauth.ts` — token、(若前端自己做 PKCE)authorize URL 构造 + - `api/iam/tenant.ts`、`dept.ts`、`role.ts`、`user.ts`、`menu.ts` + - `api/system/param.ts` + +这样「对接所有接口」= **模块方法覆盖上表每一条路由**,类型用 TypeScript 手写 DTO(后端暂无统一 OpenAPI 时可从 handler 结构体对齐,后续可加 swagger 生成)。 + +**JSON 统一响应(`/api/v1/**`业务接口,含改造后的`auth/login`)\*\* + +**HTTP 状态码(已定)** + +| HTTP | 含义 | +| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **`401`** | **认证失败**:未登录、token 无效/过期、**以及 `POST /api/v1/auth/login` 账号密码错误(凭据不成立)**等。此类场景 **只看 HTTP 状态即可**;**不必**再依赖响应体 `code` 区分是否密码错误(体可仍带统一信封或极简 JSON,供文案时选用)。 | +| **`403`** | **已认证但无权限访问**该资源或操作(**无全权限/禁止访问**)。 | +| **`200`** | **其余业务层响应**(含非认证类校验失败、参数错误、业务规则不满足等)。**具体失败类型看响应体 `code`**。**认证类失败(含密码错误)不归入此类**,见上 **`401`**。 | +| **`5xx`** | **服务端异常**(可选)仍用 **5xx**,可与信封并存或单独约定;**不**要求强行改成 200。 | + +**响应体信封** + +| 字段 | 说明 | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`code`** | **业务码**:**`200` 表示业务成功**;非 `200` 表示**在 HTTP=200 前提下**的业务失败(参数错、业务规则不满足等)。**凭据/认证问题已用 HTTP `401` 表达时,前端以 HTTP 为准**,可不解析 `code`。 | +| **`msg`** | 文案,供 toast。 | +| **`data`** | 成功时为载荷;失败时 **`null` 或省略**。 | + +- 请求可传 **`state`**,成功时经 **`data` 回显**。 +- **`/oauth/token`**、**`/oauth/introspect`** 等 OAuth 端点可仍按 **RFC**,**以 `docs/oauth-v2.md` 为准**(可与业务信封并存)。 + +**前端 `apiClient`**:先读 **HTTP**;若为 **`401`** → **若请求不是「正在调 `/auth/login`」**则 **先 refresh**,失败再弹窗;**若 401 来自 `POST /auth/login`(含密码错误)** → **不 refresh**,直接 **弹窗或提示**(与 token 无关)。**`403`** → toast。**`200`** → 解析 **`body.code`**。 + +### 3. 登录与鉴权策略 + +**Cookie 登录 vs OAuth2 Code + PKCE(概念)** + +- **Cookie 会话**:`Set-Cookie` 会话 id,后续请求自动带 Cookie。 +- **OAuth2 Code + PKCE**:用 **`code_verifier` / `code_challenge(S256)`** 绑定授权码,再通过 **`POST /oauth/token`**(`grant_type=authorization_code`)换 **opaque `access_token`**;业务请求 **`Authorization: Bearer`**。见 [`docs/oauth-v2.md`](docs/oauth-v2.md)。 + +**现状与目标(后端)** + +- **现状(偏差)**:[`internal/auth/handler/login.go`](internal/auth/handler/login.go) 中 `POST /api/v1/auth/login` 仅校验账号密码并下发 **Session Cookie**,返回 `{"ok":true}`,**未**纳入 PKCE,也**未**与 [`internal/auth/oauth2/service.go`](internal/auth/oauth2/service.go) 的授权码/token 管线统一。 +- **目标(已定需求)**:**`/api/v1/auth/login` 也必须走 OAuth2 Code + PKCE 语义**——即:账号密码验证成功后,**签发可与现有 `/oauth/token` 交换的 authorization_code(且绑定 PKCE)**,前端仍用 **同一套 `POST /oauth/token`** 换 Bearer;**禁止**仅靠 Session Cookie 作为 SPA 主鉴权路径(Session 若保留,仅作与 `/oauth/authorize` 浏览器跳转兼容的可选项,可逐步弱化)。 + +**`client_id` / `redirect_uri` 是什么?为何常和「种子 SPA」绑在一起?** + +- **`client_id`**:OAuth2 里标识 **「哪一个客户端应用」**(例如浏览器里的 SPA)。授权服务器(Go)在库里 **登记过** 的 `client_id` 才合法。 +- **`redirect_uri`**:换发 **`authorization_code` 之后**,用户(或纯 API 流程)**最终要把 `code` 送到的回调地址**。为防止劫持,**必须与该 `client_id` 在服务端登记的允许列表一致**(`ParseRedirectURIs` / `RedirectURIMatch`)。 +- **「种子 SPA」**:指迁移/安装时 **预置的一条 OAuth 客户端**(如 `client_id=spa`,`redirect_uri` 含 `http://localhost:3000/oauth/callback` 与生产 `https://app.example.com/oauth/callback`)。**前端在 dev/prod 可共用同一 `client_id`**,只要 **各环境 `redirect_uri` 都已登记**;若种子只含一条 URI,开发跨域时需在 **后端配置/数据库** 里 **补登记** `localhost` 回调,否则 token 交换会报 `invalid_redirect_uri`。 + +**推荐后端形态(与现有 Token 端复用)** + +1. 扩展 `POST /api/v1/auth/login` 请求体:除 `user_name`、`password`、`tenant_id` 外,携带 **`code_challenge`、`code_challenge_method=S256`、`client_id`、`redirect_uri`**;**可选 `state`**(成功时在响应 `data` 中原样返回)。须通过 **与 `/oauth/authorize` 相同**的 `client_id` / `redirect_uri` 校验。 +2. 校验通过后,写入 **与 authorize 相同**的 authorization_code(同一 `oauth2.Store`、PKCE 绑定)。**HTTP 200**,body:`{ "code": 200, "msg": "操作成功", "data": { "authorization_code": "", "state": "" } }`(**`authorization_code` 与 `/oauth/token` 的 `code` 同义**)。 +3. **`/api/v1/auth/login` 密码错误**:**`HTTP 401`**(认证失败),前端 **只认状态码** 走统一 401 处理链(与 token 失效同类;**不在此用 `HTTP 200` + `body.code` 表示密码错**)。**参数不合法等非认证问题**:**`HTTP 200`** + `code`≠200 等业务码(与上表一致)。 +4. 前端拿到 `authorization_code` 后 **`POST /oauth/token`**(form)换 **access_token / refresh_token**;**前端主路径不依赖 Session Cookie**(`Set-Cookie` 可选保留)。 + +**Go 改造落点(备忘)**:在 `LoginHandler` 或抽取的 OAuth 服务方法中复用 `oauth2.Service` / `Store` 的 **授权码创建**能力;注意 **rate limit**、**redirect_uri 校验**、**client_id** 与种子 SPA 配置一致;更新 [`docs/oauth-v2.md`](docs/oauth-v2.md) 中 **「JSON 登录」** 小节与示例请求/响应。 + +| 步骤 | 前端要点 | +| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 登录 | 生成 PKCE → **`POST /api/v1/auth/login`**(body 含挑战 + client + redirect_uri + 可选 **state**)→ 信封取 **`data.authorization_code`**。 | +| 换 token | **`POST /oauth/token`**(form,`authorization_code` + `code_verifier`)→ **access_token / refresh_token**;与全站 OAuth 流程一致。 | +| 调 API | `/api/v1/**` 带 **`Authorization: Bearer`**。 | +| **静默刷新** | `access_token` 将过期或 API 返回 **401** 时,**先**用 **`refresh_token`** 调 `grant_type=refresh_token` **无感换票**;**仅当 refresh 也失败**(无 refresh、过期、服务端拒绝)再 **弹登录框**(再走 login→code→token)。`apiClient` 内建议 **单飞刷新队列**,避免并发请求重复 refresh。 | + +**401 时的交互(已定)**:**先静默 refresh** → 仍 401 再弹窗;弹窗 **仅账号 + 密码** + PKCE;**`/api/v1/auth/login` → authorization_code → `/oauth/token`**;成功后 **关弹窗并重试原请求**。**用户主动「退出」**:`POST /api/v1/auth/logout`、清本地 token、**多 Tab** 可用 **`BroadcastChannel` / `localStorage` 事件** 同步登出(与 §4 一致)。 + +### 4. Zustand + +- **`useAuthStore`**:`accessToken`、`refreshToken`(存前端或安全存储策略团队定)、`userId`/`tenantId`、`setTokens`、**`logout`**(清状态 + 调 `auth/logout` + 广播多 Tab)。 +- 可选:**`useTenantStore`** 与菜单/权限缓存,与 `iam/menu` 的 `nav`、`perms` 联动。 + +### 5. Next.js 注意点 + +- **App Router**:OAuth **callback**、首次引导授权等放在 `app/(auth)/...`;业务页在 `app/(dashboard)/...`。layout 可对「冷启动无 token」做跳转授权等策略;**业务 API 返回 401 时优先用全局弹窗登录**(见 §3),避免打断用户操作。 +- **服务端组件默认不能带浏览器 Cookie 调浏览器域下的私有 API** unless 使用 **Route Handler** 作 BFF;若希望「全 CSR + 直连 Go」,以 **Client Component + `apiClient`** 为主即可。 +- **Prettier**:与 ESLint 一起在仓库根配置,与现有 Go 仓库分属不同目录时各自一份配置即可。 + +### 6. Radix UI + +- 用于表单、Dialog、Dropdown、**Navigation Menu / Collapsible**、**Tooltip**(图标模式下图标旁展示完整菜单名)等;与业务页面解耦。 + +--- + +## 导航数据:后端驱动(强制约定) + +**目标**:左侧导航(含经典树状 / 图标模式下的浮层或抽屉)所展示的 **层级、顺序、可见项、展示名、图标字段(若后端提供)** 均以 **后端返回的菜单树为准**;前端 **不在生产环境写死业务菜单列表**(开发环境可保留极少量占位路由仅用于 Storybook/演示,与正式壳分离)。 + +**主数据源(与当前 Go 能力对齐)** + +- **`GET /api/v1/iam/menu/nav`**:面向当前用户/租户的 **导航菜单树**(侧栏渲染首选)。 +- 必要时配合 **`GET /api/v1/iam/menu/perms`** 等做 **按钮级/路由级权限**;与 [`iam_menu`](migrations/postgres/001_iam.sql) 及角色绑定一致。 +- 若需全量配置态菜单(管理端「菜单管理」页),可用 **`/api/v1/iam/menu/tree`** 等接口;**运行时侧栏**仍以 **nav** 为主,避免混用两套源。 + +**前端职责** + +- 登录成功且具备 token 后 **拉取 nav**(及 perms),存入 Zustand/React Query 等,带 **SWR/失效策略**(切换租户、重新登录时 **重新请求**)。 +- 将后端节点字段(如 `path`、`component`、`perms`、`children`)**映射到 Next `Link`/`router` 路径**;若后端 `path` 与前端路由表不一致,维护 **一层显式映射表**(仍由后端数据驱动「显示哪些项」,映射只解决 URL 形状)。 +- **经典模式**:直接渲染树组件;**图标模式**:同一棵树做遍历,父节点走浮层/抽屉(见上文「图标模式下的二级、三级导航」)。 + +**反模式(避免)** + +- 侧栏 `MenuItem` 写死在 `layout.tsx` 内且与数据库菜单两套真相。 +- 仅首屏拉一次菜单后永不刷新(租户切换、权限变更会不同步)。 + +--- + +## 整体页面与布局(管理端壳层) + +目标:登录后进入 **统一壳**(**顶栏** + **左侧导航** + **主内容区**),业务模块(租户/部门/角色/用户/菜单/系统参数等)均在主内容区切换;登录/回调页 **不使用** 该壳,避免多余导航。 + +| 区域 | 职责 | +| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **顶栏** | 左侧:Logo/产品名;右侧:**用户信息区**(见下「顶栏右侧:用户信息下拉」,内含侧栏经典/图标切换)、租户等全局摘要(按需);**本阶段不在顶栏提供全局搜索框** | +| **左导航** | 见下文「双模式」;**数据见上一节「导航数据:后端驱动」** | +| **主工作区** | 见下文「主工作区:Tabs 区 + 内容区」;**非简单单页 children**,而是 **多页签 + 卡片式内容区**(列表/表单等在内层渲染) | + +### 顶栏右侧:用户信息下拉(产品约定) + +- **位置**:Header **最右侧**(或紧挨全局操作区)展示当前用户摘要(建议:**头像/占位图 + 显示名或账号**)。 +- **交互**:**鼠标悬停**在用户区域上时,展开 **下拉列表**(可用 Radix `DropdownMenu` 配合 `onOpenChange` / 延迟关闭,或 `HoverCard`+菜单组合;实现时注意 **触控设备无悬停**,需 **点击同样可打开**,并支持 **Esc 关闭、键盘方向键**,避免纯悬停导致无障碍与移动端不可用)。 +- **下拉项(固定项,顺序建议如下)**: + 1. **个人中心** — 跳转前端路由(如 `/account` 或 `/profile`),展示当前用户资料;**若后端暂无专用接口**,可先读已有用户接口(如按 `user_id` `GET`)或占位页,后续与 IAM 对齐。 + 2. **修改密码** — 跳转 `/account/password` 或 **弹窗表单**;提交时调用后端修改密码接口(**若当前 Go 未暴露**,计划中单列为「需补接口」或与 `iam/user` 更新密码能力对齐)。 + 3. **侧栏布局** — **经典模式** / **图标模式** 二选一(可用分段控件、单选行或两项可点菜单项),与 `useShellStore` 的 `sidebarMode` + `localStorage` 一致;**勿再单独放在顶栏**。 + 4. **退出** — 调用 `POST /api/v1/auth/logout`,清除前端 token/状态并跳转登录页。 + +```text ++------------------------------------------------------------------------+ +| [Logo] Smart Admin [租户…] [○ 张三 ▼] ← 悬停/点击展开 | ++------------------------------------------------------------------------+ + +---------------------------+ + | 个人中心 | + | 修改密码 | + | 侧栏:[ 经典 | 图标 ] | + | 退出 | + +---------------------------+ +``` + +--- + +## 主工作区:Tabs 区 + 内容区(产品约定) + +导航栏 **右侧**为 **主工作区**,纵向分为两层:**上部 Tabs 页签区** + **下部内容区**。左侧菜单点击路由时,**优先在 Tabs 中打开/激活对应页签**(与常见「多标签后台」一致;实现可用 Zustand 维护页签列表与当前激活项)。 + +### A. Tabs 页签区 + +| 项 | 约定 | +| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **结构** | 顶部一条 **Tabs 栏**;每个 Tab 对应一个已打开页面(路由 + 关键参数可序列化进 tab id)。 | +| **固定首页签** | **最左侧**固定 **「概览」** 页签(路由如 `/dashboard` 或 `/overview`),**不可关闭**、不可被「关闭全部」关闭。 | +| **右键菜单** | 在 **页签条区域**(建议在 **某个 tab 标签上**)**点击右键**弹出菜单(Radix **Context Menu**),包含:**关闭**、**关闭左侧**、**关闭右侧**、**关闭全部**(关闭全部时保留「概览」)。 | +| **横向溢出** | 当 Tab 过多超出可视宽度时:**左侧、右侧各一个常驻小按钮**(如 `‹` `›`),用于将 Tab 条 **向左/向右滚动**;按钮**始终占位可见**(禁用态亦可,避免布局跳动)。Tab 容器使用 `overflow-x: auto` + `scrollBy` 或等效实现。 | + +**线框示意** + +```text +| ◀ | [ 概览 | 用户管理 × | 角色管理 × | ... ] | ▶ | + ^固定不可关^ ^右键关闭等^ +``` + +### B. 内容区(包裹在 Tabs 下方) + +| 项 | 约定 | +| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **外层边距** | 内容区相对主工作区容器:**左、上、右** 外边距均为 **12px**;外层背景为 **浅灰**(具体色值用设计 token,如 `hsl` 中性灰,勿写死纯黑字对比不足)。 | +| **内层卡片** | 实际承载页面的是 **白色背景** 容器;**内边距 12px**(与外边距统一节奏)。内容很短时,**底部**仍保持 **至少 12px** 外边距(可用 `min-height` + `padding-bottom` 或 flex 布局保证)。 | +| **圆角** | 白色主工作卡片 **四角圆角**(建议 `8px`–`12px` 或 Tailwind `rounded-lg`/`rounded-xl`,全局一致)。 | + +### C. 一般列表页(内容区内「标准模板」) + +适用于租户/用户/角色等 **表格类** 页面,内容区内再分 **上 / 中 / 下** 三块: + +| 区域 | 内容 | 布局 | +| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- | +| **上部 · 查询条** | **左侧:操作区**(常见:**新增**、**批量删除** 等,随业务增减);**右侧:条件查询区**(输入框、下拉等)。左右 **两端对齐**(`justify-content: space-between` 或 grid 两列)。 | +| **中部 · 表格** | 数据表格,列随实体定义;**数据随查询条件变化**(前端驱动请求参数,或由 Zustand/React Query 绑定 filter state)。 | +| **下部 · 分页** | 分页器;**每页条数** 可选 **10 / 20 / 50**(默认可先 20);**允许按业务页单独配置**(通过 props 或页面级常量覆盖默认值)。 | + +**表格行内操作(约定)** + +| 操作 | 说明 | +| -------- | ------------------------------------------------------------- | +| **修改** | 行级入口,跳转编辑页、侧滑/抽屉表单或行内编辑(按模块选型)。 | +| **删除** | 行级删除。 | + +**删除与批量删除(接口约定)** + +- **单条删除**与**批量删除(工具栏)**共用 **同一后端批量删除能力**(例如 `POST .../batch-delete` 或 `DELETE` + body 为 `ids: []`);单条删除时传 **仅含一个 id 的列表**,避免维护两套删除路径与权限点。 +- 前端:表格多选 + 「批量删除」与行内「删除」最终都走上述接口;确认弹窗文案区分「删除所选 N 条」与「删除本条」即可。 + +非列表页(表单页、详情页)可只使用白色卡片容器 + 内边距,不强制三区块,但 **边距与圆角** 与上表保持一致。 + +### D. 内容区布局变体(除「纯表格式列表」外) + +| 形态 | 适用场景 | 结构要点 | +| ------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **左树 + 右表 + 分页** | 数据依赖**树形上下文**的列表(如 **用户管理**:左侧 **部门/组织机构树**,右侧用户表 + 查询条 + 分页) | 左侧树与右侧表 **同一张灰底白卡片容器内**或左右分栏两卡片;**选中树节点**作为查询条件(如 `dept_id` / `org_id`),切换节点时刷新右侧表格与分页回到第 1 页。 | +| **整页树(可带工具栏)** | **层级数据本身即主对象**(如 **菜单管理**、部分 **目录/分类**) | 主区为 **可编辑树**(拖拽排序、增删改节点等按后端能力);若需与列表混用,可树下方再挂从表(按产品)。 | + +**线框示意(左树右表,如用户管理)** + +```text + 灰底 12px ++-----------------------------------------------------------------------+ +| +-- 白卡片 ----------------------------------------------------------------+ | +| | +------------------+ +------------------------------------------------+ | | +| | | 部门 / 组织树 | | [+新增] [批量删除] [条件…] [查询] [重置] | | | +| | | ▾ 公司 | +------------------------------------------------+ | | +| | | ▾ 研发部 | | 表头 | 操作(修改/删除) | … | | | +| | | ▾ 市场部 | | 数据 | [改][删] | … | | | +| | | … | +------------------------------------------------+ | | +| | | (可选宽 240–280) | | 共 N 条 [10|20|50/页] < 1 2 3 > | | | +| | +------------------+ +------------------------------------------------+ | | +| +-------------------------------------------------------------------------+ | ++-----------------------------------------------------------------------+ +``` + +### E. 组织机构与用户管理是否合一(计划约定) + +- **常见做法(推荐默认)**:**组织机构(部门树)**作为 **用户管理** 的左侧上下文,与 **用户列表** 放在 **同一功能页 / 同一菜单入口**(左树右表);用户的新增/编辑表单里 **归属部门** 与树联动。这样避免「组织」与「用户」两处维护同一棵部门树时的割裂感。 +- **何时拆页**:若产品要求 **组织机构** 单独做 **编制、合并、禁用** 等重操作,且与用户列表 **强解耦**,可另开 **「部门管理」** 子菜单;数据上仍与用户的 `dept_id` 同源,前端避免重复实现两套树数据源(宜共用 hook / 同一 `dept` API)。 +- **结论**:计划层面 **默认采用「用户管理 = 部门树 + 用户表」一体化**;是否再单列「组织机构」顶级菜单由产品命名决定(可做成 **同路由别名** 或 **子 Tab:用户 | 部门**),实现上不强制两套壳。 + +**推荐实现(一种可落地方案)** + +| 层次 | 做法 | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **路由** | **一个主路由**承载左树右表,例如 `/iam/users`(与后端菜单 `path` 对齐)。部门相关「重操作」优先做 **同页 Modal / 右侧抽屉 / 全屏子路由**(如 `/iam/users/dept`),避免先复制一套独立壳再维护两份树状态。 | +| **部门树数据源** | 封装 **`useDeptTree`**(React Query:`queryKey: ['dept', 'tree', tenantId]`),全应用 **仅此一处拉树**;用户页、(若有的)部门管理页、用户表单里的部门选择器 **都复用同一 query**,树更新后 **`invalidateQueries(['dept', 'tree', …])` 一处失效即可**。 | +| **选中部门** | 用 **`useState` + 可选 URL `?deptId=`**(`nuqs` 或 Next `useSearchParams`):刷新、分享链接可恢复上下文;切换节点时 **用户列表页码重置为 1**。 | +| **用户列表** | **`useUserList({ deptId, page, pageSize, …filters })`**,`deptId` 来自选中节点(根节点可表示「全部」或 `undefined` 由后端约定);查询条与树筛选 **合并为同一请求参数**。 | +| **行内 / 批量删** | 与上文约定一致,走 **同一 batch-delete**;表格 `rowSelection` 与批量按钮共用 `ids`。 | +| **用户表单与树联动** | 新增/编辑用 **受控的部门选择**(下拉树、`TreeSelect` 或内嵌窄树);`dept_id` 初始值 = 当前左侧选中节点或行数据;若用户在表单里改部门,**不必**自动改左侧选中(避免抢焦点),保存成功后 **刷新列表** 即可。 | +| **部门 CRUD(轻)** | 在树工具条放「新增子部门」「编辑」「禁用」→ **Modal**;成功后 **invalidate 部门树 + 用户列表**(若影响可见范围)。 | +| **部门 CRUD(重)** | 若合并、批量迁移等交互很重,可 **另开菜单** 指向 **独立页面**,该页仍 import **同一套 `features/dept` 模块**(hooks + api),禁止复制 `DeptTree` 组件实现。 | + +**小结**:用 **「单一路由 + 单一 dept tree query + 可选 searchParam 记录选中节点」** 最省事;重功能用 **子路由或抽屉** 消化,数据层仍 **一套 dept API、一套 React Query key 前缀**。 + +**计划采纳**:上述 **「推荐实现」表格 + 小结** 为本方案 **用户/部门一体化页的默认实现**;后续除非产品另有要求,按此执行。 + +**线框示意(列表页)** + +```text + 灰色背景 (外层 12px 边距) ++------------------------------------------------------------------+ +| +-- 白色圆角卡片 (padding 12px) --------------------------------+ | +| | [+新增][批量删除] [条件A][条件B][查询][重置] | | +| | ---------------------------------------------------------------- | | +| | | 表头 | 表头 | ... | | +| | | 数据 | 数据 | ... | | +| | ---------------------------------------------------------------- | | +| | 共 N 条 [10|20|50/页] < 1 2 3 > | | +| +----------------------------------------------------------------+ | ++------------------------------------------------------------------+ +``` + +### 整页综合线框图(Header + 左导航 + Tabs + 内容区标准列表) + +```text ++======================================================================================+ +|| Header || +|| [Logo] Smart [租户…] [○ 用户 ▼] ← 展开含侧栏经典/图标、退出等 || ++===+==================================================================================+ +|| || Tabs 栏(右键:关闭 / 关左 / 关右 / 关全部;概览不可关) || +|| || +---+ +--------------------------------------------------------------------+ || +|| || | ◀ | | 概览 | 用户× | 角色× | 租户× | …overflow… | ▶ | || +|| || +---+ +--------------------------------------------------------------------+ || +|| || ^固定^ ^滚动钮常显^ || +|| || +-----------------------------------------------------------------------------+| +|| || | 灰底 12px 边距(左/上/右;底同) | || +|| || | +-------------------------------------------------------------------------+ | || +|| || | | 白底圆角卡片 (内边距 12px) | | || +|| || | | [+新增] [批量删除] [条件…] [查询] [重置] ←上:左右对齐 | | || +|| || | |-------------------------------------------------------------------------| | || +|| || | | | 列1 | 列2 | 列3 | ... ←中:表格 | | || +|| || | | | 数据| 数据| 数据| | | || +|| || | |-------------------------------------------------------------------------| | || +|| || | | 共 N 条 每页 [10▼] [20] [50] < 1 2 3 > ←下:分页 | | || +|| || | +-------------------------------------------------------------------------+ | || +|| || +-----------------------------------------------------------------------------+| +|| || || +||左|| || +||侧|| || +||导|| || +||航|| || +|| || || +||树|| || +||状|| || +|| || || ++===+==================================================================================+ +``` + +**实现提示**:Tabs 状态与路由 **可双向同步**(新开 tab 推 history 或仅内存,按团队选择);右键菜单项需 **禁用态**(例如当前 tab 左侧无 tab 时「关闭左侧」禁用)。 + +--- + +## 左导航:经典模式 vs 图标模式(约定含义) + +业内常见两种叫法,与你描述的「经典 / 图标」一般对应如下(若你希望另一种交互,可再改一版文案): + +| 模式 | 典型形态 | 体验要点 | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | +| **经典模式** | 侧栏 **较宽**(约 220–260px),每项 **图标 + 文字** 并排展示;多级菜单可展开/折叠,**当前选中态**清晰 | 信息密度高,适合首次使用与菜单项较多的管理后台 | +| **图标模式** | 侧栏 **收窄**(约 56–72px),**仅显示图标**;文字通过 **Tooltip** 或 **悬停浮层** 展示;子菜单可用 **Popover / 侧滑面板** 或点击展开窄条下的二级 | 主内容区更宽,适合熟练用户;类似 VS Code 活动栏、许多 SaaS 的「收起侧边栏」 | + +**实现要点(计划内约定):** + +- 使用 **Zustand**(如 `useShellStore`)保存 `sidebarMode: 'classic' | 'icon'`,并用 **`localStorage` 持久化**(键名如 `smart_sidebar_mode`),刷新后保持用户选择。 +- 布局用 **CSS 变量或 Tailwind** 控制侧栏宽度;`transition` 做宽度切换动画(可选)。 +- **无障碍**:图标模式下每个图标按钮必须带 **`aria-label`**,与 Tooltip 文案一致。 +- 与 **Radix**:`Tooltip` + `NavigationMenu` 或自研侧栏;避免纯 div 堆叠导致键盘无法操作。 + +```mermaid +flowchart TB + subgraph shell [AppShell] + Top[顶栏] + subgraph left [左导航] + Classic[经典: 宽栏+图文] + IconOnly[图标: 窄栏+Tooltip] + end + Main[内容区] + end + Top --> left + left --> Main +``` + +### 线框图(ASCII) + +**经典模式(宽侧栏:图标 + 文字)** + +```text ++----------------------------------------------------------------------------------+ +| [Logo] Smart Admin [租户: 平台] [用户 ▼] [退出] ←「用户」下拉内含侧栏经典/图标 | ++----------+-----------------------------------------------------------------------+ +| [i] 首页 | | +| [i] 系统 | 主内容区(列表 / 表单 / 详情) | +| 参数 | | +| [i] IAM | +---------------------------------------------------------------+ | +| 用户 | | 表格 / 卡片 / 步骤条 … | | +| 角色 | +---------------------------------------------------------------+ | +| ... | | +| | | ++----------+-----------------------------------------------------------------------+ + ^约 220–260px^ +``` + +**图标模式(窄侧栏:仅图标,文字用 Tooltip)** + +```text ++----------------------------------------------------------------------------------+ +| [Logo] Smart Admin [租户] [用户 ▼] [退出] ←「用户」下拉内含侧栏经典/图标 | ++--+-------------------------------------------------------------------------------+ +|[]| | +|[]| 主内容区(更宽) | +|[]| | +|[]| +---------------------------------------------------------------+ | +|[]| | | | +|[]| +---------------------------------------------------------------+ | +| | | ++--+-------------------------------------------------------------------------------+ + ^约 56–72px^ + 悬停图标 → Tooltip「用户管理」;子菜单可 Popover 或右侧滑出 +``` + +**两种模式对比(同一壳,仅侧栏宽度与是否显示标签变化)** + +```text +经典 图标 ++--------------------+ +--+---------------+ +| [≡] 用户管理 | |👤| 用户列表… | +| [≡] 角色管理 | ↔ |👥| | +| [≡] … | |⚙ | (Tooltip) | ++--------------------+ +--+---------------+ +``` + +### 图标模式下的二级、三级导航(常见交互) + +窄栏里**无法像经典模式那样纵向展开整棵树**,多级菜单通常用下面几类方式之一(可混用,按菜单深度与数量选型): + +| 方案 | 行为 | 适用 | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| **A. 右侧飞出级联面板** | 点击一级图标 → 在侧栏右侧弹出 **浮层 1** 列出二级;在某项上悬停或点击 → 再向右弹出 **浮层 2** 列三级(「手风琴式」级联,类似旧版 Windows 开始菜单多级) | 二、三级都多、需要快速扫视 | +| **B. 单层面板 + 树/分组** | 点击一级图标 → 一个较宽的 **Popover / DropdownMenu** 内用 **可折叠分组** 或 **缩进树** 展示二、三级(可滚动) | 总项数中等、希望少移动鼠标 | +| **C. 抽屉** | 点击带子女的图标 → 从左侧 **滑出抽屉**,内部为完整树(与经典侧栏同结构,只是按需出现) | 层级很深或名称很长 | +| **D. 临时加宽** | 点击某父级后,侧栏在图标列旁 **临时展开一条「迷你文字列」** 仅显示该支路的二、三级 | 想兼顾窄栏与可读性 | + +**线框示意(方案 A:向右级联)** + +```text +侧栏(窄) 浮层1(二级) 浮层2(三级) ++--+ +-----------+ +-----------+ +|👤| 点击 → | 用户管理 | | 列表用户 | +| | | 角色管理 | hover| 导入用户 | +| | | 部门 ─────┼────→| 导出 | ++--+ +-----------+ +-----------+ +``` + +**线框示意(方案 B:单 Popover 内多级)** + +```text ++--+ +|👤| 点击 → +-------------------------+ ++--+ | ▼ IAM | + | 用户管理 | + | 角色管理 | + | ▼ 系统 | + | 参数配置 ← 三级作为子项 | + +-------------------------+ +``` + +**计划约定**:实现时 **同一套菜单树数据**(如 `menu/nav`)驱动经典树与图标模式;图标模式需为 **带 `children` 的节点** 绑定上述一种交互,并在设计稿中统一 **键盘操作**(Esc 关闭、方向键在级联中移动,可与 Radix DropdownMenu / NavigationMenu 能力对齐)。 + +--- + +## 实施顺序建议 + +1. 初始化 Next 项目(TS + Prettier),配置 `NEXT_PUBLIC_API_ORIGIN`。 +2. **(Go)** 按 §3 改造 **`POST /api/v1/auth/login`**:密码通过后签发 **PKCE 绑定 `code`**,与 **`/oauth/token`** 复用;同步更新 **`docs/oauth-v2.md`**。 +3. 实现 **`apiClient` + `api/auth`**,跑通 **login → code → token**、logout 与一条 IAM 只读接口(如 `menu/nav`)。 +4. 搭建 **`app/(dashboard)/layout`**(或等价)实现 **AppShell + 左导航双模式** 与顶栏,菜单对接 `menu/nav`。 +5. 按模块把 **IAM、System** 全部方法补齐(与 [`http_register`](internal/iam/http_register.go) 路径一一对应),页面放入内容区。 +6. 接入 **OAuth2**(PKCE + callback + token 存储),与现有 Go 的 `client_id`/`redirect_uri` 配置一致(种子 `spa` 与 [`configs/local.yml`](configs/local.yml) 中 `frontend_login_url`)。 +7. Go 侧确认 **CORS + Cookie 策略** 与生产 HTTPS。 +8. (可选)后端增加 OpenAPI/Swagger 后,用 codegen 替换手写 DTO。 + +--- + +## 产品口径与待确认项 + +### 已定口径(按当前共识写入计划) + +| 项 | 决定 | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **鉴权** | **仅 OAuth2 Code + PKCE + Bearer**;**`/api/v1/auth/login` 须改造为签发 PKCE 绑定 `code` 并与 `/oauth/token` 同一套逻辑**(当前 Go 仅为 Session,属待改偏差)。 | +| **租户切换** | **必须关闭所有 Tabs**(页签状态清空),并 **重新拉** `menu/nav`、`dept/tree` 及当前内容区数据(与 `tenantId` / token 声明一致)。 | +| **权限 · 第一期** | **仅做路由级守卫**(未登录或整页无权限 → 拦截/提示);**不做**表格内每颗按钮与 `perms` 的细粒度联动(后续迭代再加)。 | +| **批量删除** | 与 Go 路由对齐;若某资源 **没有 batch 接口**,**实现侧先通知负责人,由负责人决定**补接口或临时方案,**不擅自拍板**。 | +| **HTTP 与 body.code** | **`401`** = 认证失败(**含登录密码错误**),**前端以 HTTP 为准**;**`403`** = 无权限;**其余业务** **`HTTP 200`** + 体 `code`。**密码错误不单独用 `200`+业务码表达**。 | +| **JSON 与 state** | 信封 **`{ code, msg, data }`**;**`auth/login` 可带 `state`**,成功 **`data` 回显**。 | +| **401 / refresh** | **一般 API 的 401** → **先 refresh**,失败再弹窗;**`POST /auth/login` 的 401**(密码错等)→ **不 refresh**,直接按需弹窗(§3)。 | +| **403** | **HTTP 403** → **toast**,留在当前页,**不弹登录框**。 | +| **环境** | **生产同域**;**开发跨域** → Go **CORS 白名单** dev origin;种子 SPA 的 **`redirect_uri` 须含各环境回调**(见 §3 `client_id` 说明)。 | +| **登出** | **`POST /auth/logout`** + 清前端 token;**多 Tab** 用 **BroadcastChannel / storage 事件** 同步登出。 | + +### 术语说明(白话) + +**部门树:根节点 =「全部」** + +- 含义:左侧树**最顶层**(或「未选中具体部门」)表示 **不按部门过滤**,用户列表查 **「全部」**(具体 query 与后端约定)。 +- **是否含停用**:部门可能被 **停用**,需约定树里 **是否仍显示**这些节点(显示则可选中;不显示则界面更干净)。 +- **是否懒加载**:**懒加载** = 先只拉根,**展开再拉子节点**;**非懒加载** = **一次拉整棵树**。部门特别多时常用懒加载;树小可一次拉齐。 + +**Tabs 要不要写进 URL** + +- **只存在内存**:开了多个页签,**一刷新浏览器页签全没**——可接受则实现简单。 +- **写进 URL**:刷新或 **分享链接** 能恢复多页签状态;实现更复杂,第一期**不强制**。 + +**部门「轻」vs「重」/ 独立页** + +- **轻**:在用户管理页用 **Modal** 做 **加子部门、改名** 等简单维护。 +- **重**:**整页**做 **合并部门、批量迁移、复杂拖拽** 等,才需要 **单独「部门管理」菜单**。**MVP 建议先轻后重**,复用同一套 `dept` API。 + +### 仍待与后端核对 + +| 项 | 说明 | +| ------------------------- | ------------------------------------------------------ | +| **批量删除契约** | 路径、`ids`、软删以 Gin 为准;**缺接口时通知负责人**。 | +| **部门树 · 停用与懒加载** | 与后端行为对齐;未约定则联调时定一版。 | +| **Tabs 是否进 URL** | 可第一期 **仅内存**,后续再加。 | + +--- + +## 风险与约束 + +- **「所有接口」** 以当前 Gin 注册为准;若后续新增路由,前端需同步增加 `api/*` 方法。 +- Bearer 中间件对无 token 请求**不 401**;**业务上需登录**由路由守卫与 **`apiClient`:先 refresh、失败再弹登录框** 协同处理。 +- 工作区若与 Go 仓库分离,请用 **同一文档** 维护 baseURL 与 `client_id`/redirect 列表。 diff --git a/.cursor/plans/oauth2_pkce_单体设计_62bcf65b.plan.md b/.cursor/plans/oauth2_pkce_单体设计_62bcf65b.plan.md new file mode 100644 index 0000000..9d08cda --- /dev/null +++ b/.cursor/plans/oauth2_pkce_单体设计_62bcf65b.plan.md @@ -0,0 +1,232 @@ +--- +name: OAuth2 PKCE 单体设计 +overview: 在单体部署中同时承担 AS 与 RS:逻辑分层 + **opaque access token**(随机串)存 PostgreSQL/Redis,RS 中间件按 token 查元数据完成校验;可选暴露 RFC 7662 introspection 供未来独立资源服务。前后端分离下后端不渲染 HTML,仅 JSON 登录与 302;OAuth 2.1 + 强制 PKCE,与现有 IAM 对齐。 +todos: + - id: schema-oauth + content: 设计并迁移 oauth_client、opaque access_token / refresh_token 元数据表(或 Redis)、authorization code 存储策略 + status: completed + - id: session-login + content: JSON 登录 API + HttpOnly Cookie 与 Redis 会话;未登录访问 /oauth/authorize 时 302 到配置的前端 URL(携带完整回跳 authorize 链接或 request_id),无后端 HTML + status: completed + - id: auth-code-pkce + content: 实现 /oauth/authorize 与 /oauth/token(authorization_code + S256 PKCE + state) + status: completed + - id: opaque-token-mw + content: 签发 opaque access token(高熵随机串,元数据存库)、Gin Bearer 中间件内联查库/Redis 解析用户与 scope;可选 POST /oauth/introspect(RFC 7662) + status: completed + - id: refresh-scope + content: refresh_token 轮换、scope 与租户/权限映射、限流与审计 + status: completed +isProject: false +--- + +# OAuth2 Authorization Code + PKCE(单体 AS+RS)设计 + +## 可行性结论 + +**可以这样做,而且是合理选择。** 单体同时作为 AS 与 RS 并不意味着违反 OAuth 模型:OAuth 描述的是**角色与端点**,不要求物理上分属不同进程。同一服务内: + +- **授权服务器**:提供 `/oauth/authorize`、`/oauth/token`(以及可选的授权服务器元数据、**令牌自省**、撤销端点)。 +- **资源服务器**:现有 [`/api/v1/...`](internal/server/http.go) 等业务 API,通过 `Authorization: Bearer` 校验访问令牌。 + +部署形态为**单体**时,AS 签发 **opaque** 令牌并写入存储,RS 与 AS **同进程**:中间件直接 **查库/Redis** 解析 `sub`(用户 id)、租户、`scope`、`exp`,**无需** JWT 验签,也**不必**为 RS 单独 HTTP 调 introspection(除非你愿意统一走自省接口以保持代码路径单一)。若未来 RS 独立部署,再启用 **RFC 7662 introspection** 或共享 Redis/DB 读副本。 + +**选型说明(已定)**:**Access token 采用 opaque**——撤销可即时生效(删行或标 `revoked`)、不暴露声明给客户端解析;代价是每次 API 请求多一次存储读取(可用 Redis 缓存 + 短 TTL 与 DB 一致)。 + +与仓库内 [docs/oauth-v2.md](docs/oauth-v2.md)(OAuth 2.1 草案)对齐的要点: + +- **Authorization Code** 为主流程;**PKCE**(`code_challenge` / `code_verifier`,推荐 S256)对各类客户端(含机密客户端)可防范授权码注入,草案要求除例外情况外**强制使用**(见文档中 4.1 与 7.5 节相关段落)。 +- **Redirect URI** 须严格字符串匹配(2.1 比 2.0 更严)。 +- **Implicit** 等 grant 在 2.1 中不再规定,**不要**实现隐式授权。 +- 生产环境 **HTTPS**、**短期 access token**、**scope/audience 约束**为最佳实践。 + +--- + +## 前后端分离约束(后端不返回 HTML) + +本方案与「后端只提供 API」**兼容**:OAuth 规范里授权端点本来就是 **HTTP 重定向** 与浏览器导航,不要求授权服务器返回登录表单 HTML。 + +- **登录页、同意页**:由**前端 SPA** 渲染(独立域名或 `/login` 路由均可)。 +- **后端**提供:`POST /api/v1/.../login`(或 `auth/login`)等 **JSON** 接口校验 [`iam_user`](internal/iam/entity/user.go) 凭据,成功后通过 **`Set-Cookie`**(HttpOnly、Secure、`SameSite` 按跨站需求选择)建立会话,**响应体不写 HTML**。 +- **`GET /oauth/authorize`**:仅返回 **302**(已登录则带 `code` 重定向到 client;未登录则重定向到**配置的前端登录 URL**),或 **302 到错误 redirect_uri**(OAuth 标准错误响应)。禁止用 HTML 表单作为唯一交互方式。 + +**未登录时的典型串联**(浏览器内): + +1. 用户浏览器访问 AS 的 `/oauth/authorize?...`(含 PKCE、`state`)。 +2. AS 发现无会话:**302** 到 `FRONTEND_LOGIN_URL`,并在 query 中携带「回到 authorize 所需信息」——常见两种做法(二选一或组合): + - **回跳链接**:`next` / `return_to` = `urlencode(完整 authorize URL)`,前端登录成功后执行 `location.href = decode(next)` 再次命中 authorize(此时带 Cookie)。 + - **短期票据**:`login_challenge` / `request_id` 指向 Redis 中暂存的 authorize 查询参数,前端登录成功后调用后端「完成登录并继续授权」或仍用回跳 authorize URL(实现简单优先前者需额外端点)。 +3. 用户在 SPA 输入账号密码 → **`POST` JSON 登录** → Cookie 写入。 +4. 浏览器再次 **`GET` 同一 `/oauth/authorize?...`**(PKCE 参数必须与首次一致,故回跳必须带齐原始 query),AS 有会话则签发 `code` 并 **302** 到第三方/首方应用的 `redirect_uri`。 + +**跨域 Cookie**:若 API 与 SPA **不同站点**,需 `SameSite=None; Secure` 且 CORS 对登录与 authorize 请求配置 `credentials`,并在配置中固定 `Cookie` 的 `Domain`/`Path`;若同站不同路径则可用 `Lax`。此项在 [`pkg/config`](pkg/config) 中显式配置并写入计划实施阶段。 + +**同意(consent)**:若需要显式授权,同样 **302 到前端同意页**(带 `consent_challenge` 或回跳 authorize),用户提交后由前端再导航回 authorize 或调用完成端点——**仍无后端 HTML**。 + +--- + +## 目标架构(逻辑视图) + +```mermaid +flowchart LR + subgraph client [OAuth客户端] + Browser[浏览器/SPA] + Native[原生应用] + end + subgraph monolith [单体后端] + AuthZ["/oauth/authorize"] + LoginAPI["POST JSON 登录"] + Token["/oauth/token"] + RS["/api/v1 资源API"] + Intro["可选 /oauth/introspect"] + end + subgraph spa [前端应用] + LoginUI[登录页UI] + end + subgraph store [持久化] + PG[(PostgreSQL)] + Redis[(Redis可选)] + end + Browser --> AuthZ + AuthZ -->|未登录302| LoginUI + LoginUI --> LoginAPI + LoginAPI -->|Set-Cookie| Browser + Browser --> AuthZ + AuthZ -->|302 code| Browser + Browser --> Token + Token -->|opaque| Browser + Browser --> RS + RS -->|查 token 元数据| PG + RS -.->|独立 RS 时| Intro + Token --> PG + LoginAPI --> Redis +``` + +--- + +## 核心组件设计 + +### 1. OAuth 客户端注册(Client Registry) + +需要能校验 `client_id`、允许的 **redirect_uri 列表**、客户端类型(public / confidential)。 + +- **首方应用**(自有前端):可用**配置 + 种子数据**或表 `oauth_client`(`client_id`、哈希后的 `client_secret`(仅 confidential)、`redirect_uris[]`、`grant_types`、`token_endpoint_auth_method`)。 +- **第三方**:同样走表结构,由管理 API 维护(可后续迭代)。 + +OAuth 2.1 对 **public client** 不在 token 端点使用 secret;**confidential client** 使用 `client_secret`(或 `private_key_jwt`,可二期)。 + +### 2. 授权端点 `GET /oauth/authorize` + +**查询参数**(RFC/2.1):`response_type=code`、`client_id`、`redirect_uri`、`scope`、`state`(**强烈建议必填**,防 CSRF)、`code_challenge`、`code_challenge_method=S256`。 + +**服务端行为**: + +1. 校验 client、redirect_uri、PKCE 参数完整且 method 支持。 +2. 若用户**未登录**:**302** 到配置项 **`FRONTEND_LOGIN_URL`**(或等价路径),并携带**完整回跳**到本端点所需的参数(见上文「前后端分离约束」:`next`/`return_to` 或 `request_id`)。**不返回 HTML**。登录成功依赖后续 **`POST` JSON 登录** + **HttpOnly Cookie** + **Redis 会话**(与 [`internal/server/http.go`](internal/server/http.go) 中间件顺序一致)。 +3. 已登录用户:若需同意且未静默授权:**302** 到 **`FRONTEND_CONSENT_URL`**(同样仅重定向,无 HTML);首方可配置静默同意跳过。 +4. 生成 **authorization code**(一次性、短 TTL,如 60–120s),**绑定**:`client_id`、`redirect_uri`、`code_challenge`/`method`、**资源所有者 id**(即 [`iam_user`](internal/iam/entity/user.go) 主键)、**租户**(`tenant_id`)、请求的 `scope`。存 PostgreSQL 或 Redis(Redis 更适合极短 TTL)。 +5. 302 到 `redirect_uri?code=...&state=...`。 + +### 3. 令牌端点 `POST /oauth/token` + +支持: + +- `grant_type=authorization_code`:`code`、`redirect_uri`、`client_id`、**`code_verifier`**(PKCE);confidential 客户端另加 `client_secret` 或 HTTP Basic。 +- `grant_type=refresh_token`:**refresh token 轮换**(发新废旧,旧 token 入库标记撤销)推荐。 + +**校验**: + +- 校验 code 未使用、未过期;`code_verifier` 与当时存的 `code_challenge` 按 S256 比对(与 [docs/oauth-v2.md](docs/oauth-v2.md) 一致)。 +- 颁发 **access_token**(**opaque**,高熵随机串)与 **refresh_token**(opaque);**存库字段建议**:`token` 或仅存 **哈希**(防 DB 泄露即泄露明文)、`user_id`、`tenant_id`、`client_id`、`scope`、`expires_at`、`revoked_at` 等。 + +错误响应遵循 OAuth 2.1 的 `application/json` 错误体约定。 + +### 4. Opaque Access Token 与资源服务器校验(默认方案) + +**Access token** 为不可解析的随机串(opaque),**不**使用 JWT 自包含声明。 + +**存储与撤销**:表(或 Redis)保存令牌元数据;撤销时删除或标记 `revoked`,**立即生效**。 + +**RS 中间件**(挂在 [`apiGroup`](internal/server/http.go) 上需保护的子树): + +- 从 `Authorization: Bearer` 取出 token,**查存储**(优先 Redis 缓存命中,miss 回源 PG)得到 `user_id`、`tenant_id`、`scope`、`exp`。 +- 校验未过期、未撤销;按需校验 `scope`。 +- 将 `user_id`、`tenant_id`、`scope` 写入 `gin.Context`,替代当前仅靠 [`X-User-ID` / `X-Tenant-ID`](internal/iam/handler/helpers.go) 的占位方式;对内网遗留 header 仅过渡且需防伪造。 + +**可选 `POST /oauth/introspect`**([RFC 7662](https://www.rfc-editor.org/rfc/rfc7662)):与上述查表语义一致,供**未来独立 RS** 或统一审计;单体同进程内可直接复用同一套存储访问逻辑,不必强制每次 API 都走 HTTP 自省。 + +**与 JWT 对比**:本方案不暴露 `/.well-known/jwks.json`(除非日后为 **id_token** 引入 JWT);若将来要支持纯离线验签再另议。 + +### 5. Scope 与现有 IAM 的映射 + +- 最小集:`openid`(若上 OIDC)、`profile`、`email`(按需,OIDC 层再扩展)。 +- API 资源:`api` 或细分 `user.read`、`admin` 等,与 [`iam_menu.perms`](migrations/postgres/001_iam.sql) / 角色体系映射:opaque 行内可存 **scope 字符串**;细粒度权限可在中间件加载后按 `user_id`+`tenant_id` **再查**角色/菜单(与「快照进 token」二选一,避免存储行过大)。 + +### 6. 安全与运维要点 + +- **HTTPS** 强制(开发可用本地证书或反向代理)。 +- **Redirect URI** 精确匹配;禁止 open redirect。 +- **Authorization code** 单次使用;建议**绑定 PKCE**(已实现则满足 2.1 核心要求)。 +- **Refresh token**:哈希存储、轮换、可撤销。 +- **限流**:`/oauth/token`、登录接口按 IP/client 限流。 +- **审计**:登录、发 token、撤销记日志。 + +--- + +## 与现有代码库的衔接点 + +| 区域 | 作用 | +| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| [`internal/server/http.go`](internal/server/http.go) | 注册 `/oauth/*`、可选 `/.well-known/oauth-authorization-server`、`/oauth/introspect`,并为 `/api/v1` 挂 Bearer 中间件 | +| [`internal/iam`](internal/iam) | 复用用户校验(密码可用 [`pkg/utils/codec`](pkg/utils/codec/bcypt.go))、租户与用户查询 | +| [`migrations/postgres`](migrations/postgres) | `oauth_client`、`oauth_access_token`(或统一 `oauth_token`)、`oauth_refresh_token`、authorization code 表或 Redis | +| [`pkg/config`](pkg/config) | issuer URL、access/refresh TTL、`FRONTEND_LOGIN_URL` / `FRONTEND_CONSENT_URL`、跨域 Cookie 与 CORS | + +依赖:OAuth 协议可自研或选用 **Fosite** / **go-oauth2**(需核对 opaque + 2.1 PKCE);**JWT 库仅在** 日后 OIDC `id_token` 时再引入。 + +--- + +## 建议代码落点(包结构) + +与现有 [`internal/iam`](internal/iam)(租户/用户 CRUD)**并列**,新增独立边界 **`internal/auth`**:作为**各类认证方式的统一挂载点**(OAuth2、日后 SAML、LDAP、API Key 等),避免与 IAM 领域模型混包。**首期 OAuth2 Code + PKCE** 放在 **`internal/auth/oauth2/`** 子树(Go 子包 `oauth2`),与「账号密码会话」等可并列拆分,互不污染。 + +| 路径 | 内容 | +| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **`internal/auth/oauth2/handler`** | `authorize`、`token`、`introspect`、可选 `revocation` | +| **`internal/auth/oauth2/service`** | 授权码、PKCE、opaque 签发/校验、refresh 轮换 | +| **`internal/auth/oauth2/repository`** | `oauth_client`、token、authorization code 持久化 | +| **`internal/auth/oauth2/entity`** | OAuth 表对应模型(关联 `iam_user`,用户主数据仍在 IAM) | +| **`internal/auth/session/`**(可选子包) | JSON 登录、Cookie/Redis 会话(供 authorize 识别用户;命名避免与 `net/http` 概念混淆时可叫 `websession`) | +| **`internal/auth/middleware`** 或 **`internal/server`** | **Bearer 查库中间件**(协议无关:只要是 opaque 就查存储);或放 `internal/server/auth.go` | +| **`internal/auth/http_register.go`** | 聚合注册:实现 [`HttpRoutes`](internal/server/http.go),内部委托 `oauth2` 等子路由(与 [`internal/iam/http_register.go`](internal/iam/http_register.go) 同级注入) | +| **`internal/auth/wire_provider.go`** | `ProviderSet`,在 [`cmd/server/wire.go`](cmd/server/wire.go) 注入 | + +**命名说明**:顶层用 **`auth`** 而非 **`oauth`**,便于扩展;**HTTP 路径**仍可保持标准 **`/oauth/authorize`**、**`/oauth/token`**(协议规定,与包名无关)。 + +**JSON 登录**:建议 **`internal/auth/session`**(或 `handler/login.go` 于 `auth` 根下),与 **`internal/auth/oauth2`** 并列;**密码校验**复用 [`internal/iam/service`](internal/iam/service) + [`pkg/utils/codec`](pkg/utils/codec/bcypt.go)。 + +**配置**:[`pkg/config`](pkg/config) 增加 `Auth` 段(内含 `OAuth2`、`Session`、`CORS` 等子段)。 + +**迁移**:[`migrations/postgres`](migrations/postgres) 新文件如 `005_oauth.sql`(编号按仓库顺延)。 + +**入口组装**:[`internal/server/http.go`](internal/server/http.go) 的 `NewHttpRouteRegistrars` 增加 **`authRoutes`**;全局 Bearer 中间件在 `NewHttpEngine` 或路由组上按需挂载。 + +**纯工具**:PKCE SHA256、高熵随机串可放 **`pkg/security/token`**(或 `pkg/authutil`),避免 `internal/auth` 根包臃肿。 + +--- + +## 可选扩展(非首期必选) + +- **OpenID Connect**:在 OAuth2 之上增加 `openid` scope、`/oauth/userinfo`;`id_token` 一般为 **JWT**(与 opaque **access_token** 并存不冲突)。 +- **RFC 7662**:独立资源服务时 RS 调 introspection;单体已用查表则可选暴露端点以保持对外契约一致。 +- **RP-Initiated Logout**:前后端分离时的统一登出。 + +--- + +## 实施阶段建议 + +1. **数据模型 + 配置**:客户端表、**opaque token 表/索引**(按 token 哈希查找)、TTL、**前端回跳 URL**、迁移脚本。 +2. **JSON 登录 + 会话**:`POST` 登录接口、HttpOnly Cookie + Redis,供 `authorize` 识别用户;与前端约定 `next`/`return_to` 回跳完整 authorize URL。 +3. **authorize + token + PKCE**:浏览器串联「authorize → 302 前端登录 → 再 authorize」;端到端打通 Code 换 **opaque** Token。 +4. **Bearer 中间件(查库/缓存)**:保护 `/api/v1`,上下文注入 `user_id`/`tenant_id`;可选 `introspect` 端点。 +5. **Refresh 轮换与撤销**、scope 与 IAM 权限对齐、集成测试(含跨域 Cookie 场景)。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6424cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Reference https://github.com/github/gitignore/blob/master/Go.gitignore +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Go workspace file +go.work + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# OS General +Thumbs.db +.DS_Store + +# project +*.cert +*.key +*.log +bin/ + +# Develop tools +.vscode/ +.idea/ +*.swp + +# Configs +configs/local.yml +wire_gen.go +__debug_bin.* \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..b2e180f --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "flag" + "log" + "os" + "os/signal" + "syscall" + + "giter.top/smart/pkg/config" +) + +var configPath string +func init() { + flag.StringVar(&configPath, "conf", "configs/local.yml", "config file path") +} +func main() { + flag.Parse() + // load config + config, err := config.Load(configPath) + if err != nil { + log.Fatalf("load config failed: %v", err) + } + // initialize server + servers, err := InitializeServer(config) + if err != nil || len(servers) == 0 { + panic(err) + } + // 启动 + for _, srv := range servers { + s := srv // 避免闭包问题,若需要 + go func() { + if err := s.Run(); err != nil { + log.Printf("server stopped: %v", err) + } + }() + } + + // 阻塞直到收到退出信号 + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + +// 再依次 Stop / Shutdown +} \ No newline at end of file diff --git a/cmd/server/wire.go b/cmd/server/wire.go new file mode 100644 index 0000000..89f7086 --- /dev/null +++ b/cmd/server/wire.go @@ -0,0 +1,27 @@ +//go:build wireinject +// +build wireinject + +// The build tag makes sure the stub is not built in the final build. + +package main + +import ( + "giter.top/smart/internal/auth" + "giter.top/smart/internal/data" + "giter.top/smart/internal/iam" + "giter.top/smart/internal/server" + "giter.top/smart/internal/system" + "giter.top/smart/pkg/config" + "github.com/google/wire" +) + +func InitializeServer(config *config.Config) ([]server.Server, error) { + panic(wire.Build( + server.ProviderSet, + data.ProviderSet, + auth.ProviderSet, + system.ProviderSet, + iam.ProviderSet, + ), + ) +} \ No newline at end of file diff --git a/configs/config.yml.example b/configs/config.yml.example new file mode 100644 index 0000000..7b5afc8 --- /dev/null +++ b/configs/config.yml.example @@ -0,0 +1,39 @@ +server: + http: + addr: "0.0.0.0:8000" + timeout: 10s + grpc: + addr: "0.0.0.0:9000" + timeout: 10s + +data: + database: + driver: postgres # mysql, postgres, sqlite + dsn: postgres://root:123456@127.0.0.1:5432/smart?sslmode=disable + redis: + # standalone | sentinel | cluster + mode: standalone + addrs: + - 127.0.0.1:6379 + password: machine03 + db: 3 + pool_size: 100 + idle_timeout: 10s + max_retries: 3 + retry_delay: 1s + retry_max_delay: 10s + # 哨兵示例(mode: sentinel,addrs 为 Sentinel 地址列表): + # mode: sentinel + # master_name: mymaster + # addrs: + # - 127.0.0.1:26379 + # - 127.0.0.1:26380 + # password: "" + # db: 0 + # 集群示例(mode: cluster,addrs 为若干节点种子地址): + # mode: cluster + # addrs: + # - 127.0.0.1:7000 + # - 127.0.0.1:7001 + # - 127.0.0.1:7002 + # password: "" diff --git a/docs/auth-api.md b/docs/auth-api.md new file mode 100644 index 0000000..fe1ac15 --- /dev/null +++ b/docs/auth-api.md @@ -0,0 +1,66 @@ +# 认证 HTTP 约定(Smart Go) + +与 OAuth2 授权码 + PKCE 对齐,见仓库内 PKCE 校验实现:`internal/auth/oauth2`。 + +## 统一 JSON 信封(`/api/v1/auth/*`) + +| HTTP | 说明 | +|------|------| +| `401` | 认证失败(如用户名或密码错误) | +| `403` | 已认证但无权限(如用户已禁用) | +| `200` | 其余:业务成功或 **OAuth 客户端/PKCE 元数据** 类错误(看 `code`) | +| `5xx` | 服务端异常 | + +响应体: + +```json +{ "code": 200, "msg": "操作成功", "data": { } } +``` + +- 业务成功时 `code` 为 `200`。 +- 凭据错误以 **HTTP 401** 为准,可不依赖 `body.code` 做分支。 + +## `POST /api/v1/auth/login` + +在校验 **用户名 + 密码** 通过后,签发与 `GET /oauth/authorize` **同一存储**的 **authorization_code**(绑定 PKCE),并 **Set-Cookie** 会话(便于浏览器再走 `/oauth/authorize`)。 + +### 请求体(JSON) + +| 字段 | 必填 | 说明 | +|------|------|------| +| `user_name` | 是 | 登录名 | +| `password` | 是 | 密码 | +| `tenant_id` | 否 | 缺省为平台租户 | +| `client_id` | 是 | 已注册的 OAuth 客户端 | +| `redirect_uri` | 是 | 须在该客户端允许列表内 | +| `code_challenge` | 是 | PKCE S256 | +| `code_challenge_method` | 是 | 须为 `S256`(大小写不敏感) | +| `state` | 否 | 成功时在 `data.state` 回显 | +| `scope` | 否 | 缺省 `openid` | + +### 成功 `HTTP 200` + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "authorization_code": "", + "state": "<若请求携带>" + } +} +``` + +前端用本地保存的 `code_verifier` 请求: + +`POST /oauth/token`,`grant_type=authorization_code`,参数与标准授权码换 token 一致(`code`、`redirect_uri`、`client_id`、`code_verifier`)。 + +### 失败示例 + +- 用户名或密码错误:**HTTP 401**,`msg` 提示凭据错误。 +- 用户禁用:**HTTP 403**。 +- `client_id` / `redirect_uri` / PKCE 不合法:**HTTP 200**,`code`≠`200`,`msg` 说明原因。 + +## `POST /api/v1/auth/logout` + +清除会话 Cookie;响应为信封 `code: 200`。 diff --git a/docs/cors-https-checklist.md b/docs/cors-https-checklist.md new file mode 100644 index 0000000..f387380 --- /dev/null +++ b/docs/cors-https-checklist.md @@ -0,0 +1,13 @@ +# CORS / HTTPS 清单(开发 → 生产) + +## 开发(当前) + +- Go 已加 [`internal/server/cors.go`](../internal/server/cors.go):仅 **Origin 为 `http://localhost:*` / `http://127.0.0.1:*` / `http://[::1]:*`** 时反射 `Access-Control-Allow-Origin` 并允许 **Credentials**。 +- 前端 `web/.env.local` 设置 `NEXT_PUBLIC_API_ORIGIN=http://127.0.0.1:8000`(与 Go 监听一致)。 +- OAuth `redirect_uri` 须与 Go /oauth 客户端种子登记一致(如 `http://localhost:3000/oauth/callback`)。 + +## 生产 + +- **同源**:Next 与 Go 同站点 / 反代为同一 Origin 时,浏览器 **简单请求无 CORS**;请关闭或收紧对公网暴露的 CORS。 +- **HTTPS**:Cookie `Secure`、`SameSite` 与 OAuth 重定向须用 **HTTPS**;参阅 `configs` 里 Session 配置。 +- 多环境 `redirect_uri`:在 OAuth 客户端表中登记各环境回调 URL。 diff --git a/docs/oauth-v2.md b/docs/oauth-v2.md new file mode 100644 index 0000000..3c722c8 --- /dev/null +++ b/docs/oauth-v2.md @@ -0,0 +1,2413 @@ +OAuth Working Group +draft-ietf-oauth-v2-1-15 +2 March 2026 +Standards Track +3 September 2026 +D. Hardt +Hellō +A. Parecki +Okta +T. Lodderstedt +SPRIND +The OAuth 2.1 Authorization Framework +Abstract +The OAuth 2.1 authorization framework enables an application to obtain limited access to a protected resource, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and an authorization service, or by allowing the application to obtain access on its own behalf. This specification replaces and obsoletes the OAuth 2.0 Authorization Framework described in RFC 6749 and the Bearer Token Usage in RFC 6750. + +Discussion Venues +This note is to be removed before publishing as an RFC. + +Discussion of this document takes place on the OAuth Working Group mailing list (oauth@ietf.org), which is archived at https://mailarchive.ietf.org/arch/browse/oauth/. + +Source for this draft and an issue tracker can be found at https://github.com/oauth-wg/oauth-v2-1. + +Status of This Memo +This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79. + +Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/. + +Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress." + +This Internet-Draft will expire on 3 September 2026. + +Copyright Notice +Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved. + +This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License. + +Table of Contents + +1. Introduction + +1.1. Roles + +1.2. Protocol Flow + +1.3. Authorization Grant + +1.3.1. Authorization Code + +1.3.2. Refresh Token + +1.3.3. Client Credentials + +1.4. Access Token + +1.4.1. Access Token Scope + +1.4.2. Bearer Tokens + +1.4.3. Sender-Constrained Access Tokens + +1.5. Communication security + +1.6. HTTP Redirections + +1.7. Interoperability + +1.8. Compatibility with OAuth 2.0 + +1.9. Notational Conventions + +2. Client Registration + +2.1. Client Types + +2.2. Client Identifier + +2.3. Client Redirection Endpoint + +2.3.1. Registration Requirements + +2.3.2. Multiple Redirect URIs + +2.3.3. Preventing CSRF Attacks + +2.3.4. Preventing Mix-Up Attacks + +2.3.5. Invalid Endpoint + +2.3.6. Endpoint Content + +2.4. Client Authentication + +2.4.1. Client Secret + +2.4.2. Other Authentication Methods + +2.5. Unregistered Clients + +3. Protocol Endpoints + +3.1. Authorization Endpoint + +3.2. Token Endpoint + +3.2.1. Client Authentication + +3.2.2. Token Endpoint Request + +3.2.3. Token Endpoint Response + +3.2.4. Token Endpoint Error Response + +4. Grant Types + +4.1. Authorization Code Grant + +4.1.1. Authorization Request + +4.1.2. Authorization Response + +4.1.3. Token Endpoint Extension + +4.2. Client Credentials Grant + +4.2.1. Token Endpoint Extension + +4.3. Refresh Token Grant + +4.3.1. Token Endpoint Extension + +4.3.2. Refresh Token Response + +4.3.3. Refresh Token Recommendations + +4.4. Extension Grants + +5. Resource Requests + +5.1. Bearer Token Requests + +5.1.1. Authorization Request Header Field + +5.1.2. Form-Encoded Content Parameter + +5.2. Access Token Validation + +5.3. Error Response + +5.3.1. The WWW-Authenticate Response Header Field + +5.3.2. Error Codes + +6. Extensibility + +6.1. Defining Access Token Types + +6.1.1. Registered Access Token Types + +6.1.2. Vendor-Specific Access Token Types + +6.2. Defining New Endpoint Parameters + +6.3. Defining New Authorization Grant Types + +6.4. Defining New Authorization Endpoint Response Types + +6.5. Defining Additional Error Codes + +7. Security Considerations + +7.1. Access Token Security Considerations + +7.1.1. Security Threats + +7.1.2. Threat Mitigation + +7.1.3. Summary of Recommendations + +7.1.4. Access Token Privilege Restriction + +7.2. Client Authentication + +7.3. Client Impersonation + +7.3.1. Impersonation of Native Apps + +7.3.2. Access Token Privilege Restriction + +7.4. Client Impersonating Resource Owner + +7.5. Authorization Code Security Considerations + +7.5.1. Authorization Code Injection + +7.5.2. Reuse of Authorization Codes + +7.5.3. HTTP 307 Redirect + +7.6. Ensuring Endpoint Authenticity + +7.7. Credentials-Guessing Attacks + +7.8. Phishing Attacks + +7.9. Cross-Site Request Forgery + +7.10. Clickjacking + +7.11. Injection and Input Validation + +7.12. Open Redirection + +7.12.1. Client as Open Redirector + +7.12.2. Authorization Server as Open Redirector + +7.13. Transport Security + +7.14. Authorization Server Mix-Up Mitigation + +7.14.1. Mix-Up Defense via Issuer Identification + +7.14.2. Mix-Up Defense via Distinct Redirect URIs + +8. Native Applications + +8.1. Client Authentication of Native Apps + +8.1.1. Registration of Native App Clients + +8.1.2. Native App Attestation + +8.2. Using Inter-App URI Communication for OAuth in Native Apps + +8.3. Initiating the Authorization Request from a Native App + +8.4. Receiving the Authorization Response in a Native App + +8.4.1. Claimed "https" Scheme URI Redirection + +8.4.2. Loopback Interface Redirection + +8.4.3. Private-Use URI Scheme Redirection + +8.5. Security Considerations in Native Apps + +8.5.1. Embedded User Agents in Native Apps + +8.5.2. Fake External User-Agents in Native Apps + +8.5.3. Malicious External User-Agents in Native Apps + +8.5.4. Loopback Redirect Considerations in Native Apps + +9. Browser-Based Apps + +10. Differences from OAuth 2.0 + +10.1. Removal of the OAuth 2.0 Implicit grant + +10.2. Redirect URI Parameter in Token Request + +11. IANA Considerations + +12. References + +12.1. Normative References + +12.2. Informative References + +Appendix A. Augmented Backus-Naur Form (ABNF) Syntax + +A.1. "client_id" Syntax + +A.2. "client_secret" Syntax + +A.3. "response_type" Syntax + +A.4. "scope" Syntax + +A.5. "state" Syntax + +A.6. "redirect_uri" Syntax + +A.7. "error" Syntax + +A.8. "error_description" Syntax + +A.9. "error_uri" Syntax + +A.10. "grant_type" Syntax + +A.11. "code" Syntax + +A.12. "access_token" Syntax + +A.13. "token_type" Syntax + +A.14. "expires_in" Syntax + +A.15. "refresh_token" Syntax + +A.16. Endpoint Parameter Syntax + +A.17. "code_verifier" Syntax + +A.18. "code_challenge" Syntax + +Appendix B. Use of application/x-www-form-urlencoded Media Type + +Appendix C. Serializations + +C.1. Query String Serialization + +C.2. Form-Encoded Serialization + +C.3. JSON Serialization + +Appendix D. Extensions + +Appendix E. Acknowledgements + +Appendix F. Document History + +Authors' Addresses + +1. Introduction + OAuth introduces an authorization layer to the client-server authentication model by separating the role of the client from that of the resource owner. In OAuth, the client requests access to resources controlled by the resource owner and hosted by the resource server. Instead of using the resource owner's credentials to access protected resources, the client obtains an access token - a credential representing a specific set of access attributes such as scope and lifetime. Access tokens are issued to clients by an authorization server with the approval of the resource owner. The client uses the access token to access the protected resources hosted by the resource server. + +In the older, more limited client-server authentication model, the client requests an access-restricted resource (protected resource) on the server by authenticating to the server using the resource owner's credentials. In order to provide applications access to restricted resources, the resource owner shares their credentials with the application. This creates several problems and limitations: + +Applications are required to store the resource owner's credentials for future use, typically a password in clear-text. + +Servers are required to support password authentication, despite the security weaknesses inherent in passwords. + +Applications gain overly broad access to the resource owner's protected resources, leaving resource owners without any ability to restrict duration or access to a limited subset of resources. + +Resource owners often reuse passwords with other unrelated services, despite best security practices. This password reuse means a vulnerability or exposure in one service may have security implications in completely unrelated services. + +Resource owners cannot revoke access to an individual application without revoking access to all third parties, and must do so by changing their password. + +Compromise of any application results in compromise of the end-user's password and all of the data protected by that password. + +An example where OAuth is used is where an end user (resource owner) grants a financial management service (client) access to their sensitive transaction history stored at a banking service (resource server), without sharing their username and password with the financial management service. Instead, they authenticate directly with their financial institution's server (authorization server), which issues the financial management service delegation-specific credentials (access token). + +This separation of concerns also provides the ability to use more advanced user authentication methods such as multi-factor authentication and even passwordless authentication, without any modification to the applications. With all user authentication logic handled by the authorization server, applications don't need to be concerned with the specifics of implementing any particular authentication mechanism. This provides the ability for the authorization server to manage the user authentication policies and even change them in the future without coordinating the changes with applications. + +The authorization layer can also simplify how a resource server determines if a request is authorized. Traditionally, after authenticating the client, each resource server would evaluate policies to compute if the client is authorized on each API call. In a distributed system, the policies need to be synchronized to all the resource servers, or the resource server must call a central policy server to process each request. In OAuth, evaluation of the policies is performed only when a new access token is created by the authorization server. If the authorized access is represented in the access token, the resource server no longer needs to evaluate the policies, and only needs to validate the access token. This simplification applies when the application is acting on behalf of a resource owner, or on behalf of itself. + +OAuth is an authorization protocol, not an authentication protocol, as OAuth does not define the necessary components to achieve user authentication. An authentication protocol is necessary if the goal is to authenticate users. An example is OpenID Connect [OpenID.Connect], which builds on OAuth to provide the security characteristics and necessary components required of an authentication protocol. + +The access token represents the authorization granted to the client. It is a common practice for the client to present the access token to a proprietary API which returns a user identifier for the resource owner, and then using the result of the API as a proxy for authenticating the user. This practice is not part of the OAuth standard or security considerations, and may not have been considered by the resource owner. Implementors should carefully consult the documentation of the resource server before adopting this practice. + +This specification is designed for use with HTTP [RFC9110]. The use of OAuth over any protocol other than HTTP is out of scope. + +Since the publication of the OAuth 2.0 Authorization Framework [RFC6749] in October 2012, it has been updated by OAuth 2.0 for Native Apps [RFC8252], OAuth Security Best Current Practice [RFC9700], and OAuth 2.0 for Browser-Based Apps [I-D.ietf-oauth-browser-based-apps]. The OAuth 2.0 Authorization Framework: Bearer Token Usage [RFC6750] has also been updated with [RFC9700]. This Standards Track specification consolidates the information in all of these documents and removes features that have been found to be insecure in [RFC9700]. + +1.1. Roles +OAuth defines four roles: + +"resource owner": +An entity capable of granting access to a protected resource. When the resource owner is a person, it is referred to as an end user. This is sometimes abbreviated as "RO". + +"resource server": +The server hosting the protected resources, capable of accepting and responding to protected resource requests using access tokens. The resource server is often accessible via an API. This is sometimes abbreviated as "RS". + +"client": +An application making protected resource requests on behalf of the resource owner and with its authorization. The term "client" does not imply any particular implementation characteristics (e.g., whether the application executes on a server, a desktop, or other devices). + +"authorization server": +The server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization. This is sometimes abbreviated as "AS". + +Most of this specification defines the interaction between the client and the authorization server, as well as between the client and resource server. + +The interaction between the authorization server and resource server is beyond the scope of this specification, however several extensions have been defined to provide an option for interoperability between resource servers and authorization servers. The authorization server may be the same server as the resource server or a separate entity. A single authorization server may issue access tokens accepted by multiple resource servers. + +The interaction between the resource owner and authorization server (e.g. how the end user authenticates themselves at the authorization server) is also out of scope of this specification, with some exceptions, such as security considerations around prompting the end user for consent. + +When the resource owner is the end user, the user will interact with the client. When the client is a web-based application, the user will interact with the client through a user agent (as described in Section 3.5 of [RFC9110]). When the client is a native application, the user will interact with the client directly through the operating system. See Section 2.1 for further details. + +1.2. Protocol Flow ++--------+ +---------------+ +| |--(1)- Authorization Request ->| Resource | +| | | Owner | +| |<-(2)-- Authorization Grant ---| | +| | +---------------+ +| | +| | +---------------+ +| |--(3)-- Authorization Grant -->| Authorization | +| Client | | Server | +| |<-(4)----- Access Token -------| | +| | +---------------+ +| | +| | +---------------+ +| |--(5)----- Access Token ------>| Resource | +| | | Server | +| |<-(6)--- Protected Resource ---| | ++--------+ +---------------+ +Figure 1: Abstract Protocol Flow +The abstract OAuth 2.1 flow illustrated in Figure 1 describes the interaction between the four roles and includes the following steps: + +The client requests authorization from the resource owner. The authorization request can be made directly to the resource owner (as shown), or preferably indirectly via the authorization server as an intermediary. + +The client receives an authorization grant, which is a credential representing the resource owner's authorization, expressed using one of the authorization grant types defined in this specification or using an extension grant type. The authorization grant type depends on the method used by the client to request authorization and the types supported by the authorization server. + +The client requests an access token by authenticating with the authorization server and presenting the authorization grant. + +The authorization server authenticates the client and validates the authorization grant, and if valid, issues an access token. + +The client requests the protected resource from the resource server and authenticates by presenting the access token. + +The resource server validates the access token, and if valid, serves the request. + +The preferred method for the client to obtain an authorization grant from the resource owner (depicted in steps (1) and (2)) is to use the authorization server as an intermediary, which is illustrated in Figure 3 in Section 4.1. + +1.3. Authorization Grant +An authorization grant represents the resource owner's authorization (to access its protected resources) used by the client to obtain an access token. This specification defines three grant types -- authorization code, refresh token, and client credentials -- as well as an extensibility mechanism for defining additional types. + +1.3.1. Authorization Code +An authorization code is a temporary credential used to obtain an access token. Instead of the client requesting authorization directly from the resource owner, the client directs the resource owner to an authorization server (via its user agent) which in turn directs the resource owner back to the client with the authorization code. The client can then exchange the authorization code for an access token. + +Before directing the resource owner back to the client with the authorization code, the authorization server authenticates the resource owner, and may request the resource owner's consent or otherwise inform them of the client's request. Because the resource owner only authenticates with the authorization server, the resource owner's credentials are never shared with the client, and the client does not need to have knowledge of any additional authentication steps such as multi-factor authentication or delegated accounts. + +The authorization code provides a few important security benefits, such as the ability to authenticate the client, as well as the transmission of the access token directly to the client without passing it through the resource owner's user agent and potentially exposing it to others, including the resource owner. + +1.3.2. Refresh Token +Refresh tokens are credentials used to obtain access tokens. Refresh tokens may be issued to the client by the authorization server and are used to obtain a new access token when the current access token becomes invalid or expires, or to obtain additional access tokens with identical or narrower scope (access tokens may have a shorter lifetime and fewer privileges than authorized by the resource owner). Issuing a refresh token is optional at the discretion of the authorization server, and may be issued based on properties of the client, properties of the request, policies within the authorization server, or any other criteria. If the authorization server issues a refresh token, it is included when issuing an access token (i.e., step (2) in Figure 2). The lifetime of the refresh token is also at the discretion of the authorization server. + +A refresh token is a string representing the authorization granted to the client by the resource owner. The string is considered opaque to the client. The refresh token may be an identifier used to retrieve the authorization information or may encode this information into the string itself. Unlike access tokens, refresh tokens are intended for use only with authorization servers and are never sent to resource servers. + ++--------+ +---------------+ +| |--(1)------- Authorization Grant --------->| | +| | | | +| |<-(2)----------- Access Token -------------| | +| | & Refresh Token | | +| | | | +| | +----------+ | | +| |--(3)---- Access Token ---->| | | | +| | | | | | +| |<-(4)- Protected Resource --| Resource | | Authorization | +| Client | | Server | | Server | +| |--(5)---- Access Token ---->| | | | +| | | | | | +| |<-(6)- Invalid Token Error -| | | | +| | +----------+ | | +| | | | +| |--(7)----------- Refresh Token ----------->| | +| | | | +| |<-(8)----------- Access Token -------------| | ++--------+ & Optional Refresh Token +---------------+ +Figure 2: Refreshing an Expired Access Token +The flow illustrated in Figure 2 includes the following steps: + +The client requests an access token by authenticating with the authorization server and presenting an authorization grant. + +The authorization server authenticates the client and validates the authorization grant, and if valid, issues an access token and optionally a refresh token. + +The client makes a protected resource request to the resource server by presenting the access token. + +The resource server validates the access token, and if valid, serves the request. + +Steps (3) and (4) repeat until the access token expires. If the client knows the access token expired, it skips to step (7); otherwise, it makes another protected resource request. + +Since the access token is invalid, the resource server returns an invalid token error. + +The client requests a new access token by presenting the refresh token and providing client authentication if it has been issued credentials. The client authentication requirements are based on the client type and on the authorization server policies. + +The authorization server authenticates the client and validates the refresh token, and if valid, issues a new access token (and, optionally, a new refresh token). + +Note that there is no need to communicate the lifetime of the refresh token to the client, because the client can't do anything different with the knowledge of the lifetime. Additionally, the authorization server might choose to use dynamic lifetimes (e.g. the refresh token expiry is extended as long as the refresh token is used at least once every 7 days), or the authorization server might revoke the refresh token before its scheduled expiration date for any reason, such as if the user revokes the application's access. This means the client already has to handle the case of a refresh token expiring at an arbitrary time. + +Regardless of why or when the refresh token expires, the client has only one path to obtain new tokens, which is to start a new OAuth flow from the beginning. For that reason, there is no property defined to communicate the expiration of a refresh token to the client. + +1.3.3. Client Credentials +The client credentials or other forms of client authentication (e.g., a private key used to sign a JWT, as described in [RFC7523] and its update [I-D.ietf-oauth-rfc7523bis]) can be used as an authorization grant when the authorization scope is limited to the protected resources under the control of the client, or to protected resources previously arranged with the authorization server. Client credentials are used when the client is requesting access to protected resources based on an authorization previously arranged with the authorization server. + +1.4. Access Token +Access tokens are credentials used to access protected resources. An access token is a string representing an authorization issued to the client. + +The string is considered opaque to the client, even if it has a structure. The client MUST NOT expect to be able to parse the access token value. The authorization server is not required to use a consistent access token encoding or format other than what is expected by the resource server. + +The access granted by the resource owner to the client is represented by the Access Token created by the authorization server. Access Tokens are short lived to reduce the blast radius of a leaked Access Token. The expiration of the Access Token is set by the authorization server. + +Depending on the authorization server implementation, the token string may be used by the resource server to retrieve the authorization information, or the token may self-contain the authorization information in a verifiable manner (i.e., a token string consisting of a signed data payload). One example of a token retrieval mechanism is Token Introspection [RFC7662], in which the RS calls an endpoint on the AS to validate the token presented by the client. One example of a structured token format is JWT Profile for Access Tokens [RFC9068], a method of encoding and signing access token data as a JSON Web Token [RFC7519]. + +Additional authentication credentials, which are beyond the scope of this specification, may be required in order for the client to use an access token. This is typically referred to as a sender-constrained access token, such as DPoP [RFC9449] and Mutual TLS Certificate-Bound Access Tokens [RFC8705]. + +The access token provides an abstraction layer, replacing different authorization constructs (e.g., username and password) with a single token understood by the resource server. This abstraction enables issuing access tokens more restrictive than the authorization grant used to obtain them, as well as removing the resource server's need to understand a wide range of authentication methods. + +Access tokens can have different formats, structures, and methods of utilization (e.g., cryptographic properties) based on the resource server security requirements. Access token attributes and the methods used to access protected resources may be extended beyond what is described in this specification. + +Access tokens (as well as any confidential access token attributes) MUST be kept confidential in transit and storage, and only shared among the authorization server, the resource servers the access token is valid for, and the client to which the access token is issued. + +The authorization server MUST ensure that access tokens cannot be generated, modified, or guessed to produce valid access tokens by unauthorized parties. + +1.4.1. Access Token Scope +Access tokens are intended to be issued to clients with less privileges than the user granting the access has. This is known as a limited "scope" access token. The authorization server and resource server can use this scope mechanism to limit what types of resources or level of access a particular client can have. + +For example, a client may only need "read" access to a user's resources, but doesn't need to update resources, so the client can request the read-only scope defined by the authorization server, and obtain an access token that cannot be used to update resources. This requires coordination between the authorization server, resource server, and client. The authorization server provides the client the ability to request specific scopes, and associates those scopes with the access token issued to the client. The resource server is then responsible for enforcing scopes when presented with a limited-scope access token. + +OAuth does not define any scope values, instead scopes are defined by the authorization server or by extensions or profiles of OAuth. One such extension that defines scopes is [OpenID.Connect], which defines a set of scopes that provide granular access to a user's profile information. It is recommended to avoid defining custom scopes that conflict with scopes from known extensions. + +To request a limited-scope access token, the client uses the scope request parameter at the authorization or token endpoints, depending on the grant type used. In turn, the authorization server uses the scope response parameter to inform the client of the scope of the access token issued. + +The value of the scope parameter is expressed as a space- delimited list of case-sensitive strings. The strings are defined by the authorization server. If the value contains multiple space-delimited strings, their order does not matter, and each string adds an additional access range to the requested scope. + + scope = scope-token *( SP scope-token ) + scope-token = 1*( %x21 / %x23-5B / %x5D-7E ) + +The authorization server MAY fully or partially ignore the scope requested by the client, based on the authorization server policy or the resource owner's instructions. If the issued access token scope is different from the one requested by the client, the authorization server MUST include the scope response parameter in the token response (Section 3.2.3) to inform the client of the actual scope granted. + +If the client omits the scope parameter when requesting authorization, the authorization server MUST either process the request using a pre-defined default value or fail the request indicating an invalid scope. The authorization server SHOULD document its scope requirements and default value (if defined). + +1.4.2. Bearer Tokens +A Bearer Token is a security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can. Using a Bearer Token does not require a bearer to prove possession of cryptographic key material (proof-of-possession). + +Bearer Tokens may be enhanced with proof-of-possession specifications such as DPoP [RFC9449] and mTLS [RFC8705] to provide proof-of-possession characteristics. + +To protect against access token disclosure, the communication interaction between the client and the resource server MUST utilize confidentiality and integrity protection as described in Section 1.5. + +There is no requirement on the particular structure or format of a bearer token. If a bearer token is a reference to authorization information, such references MUST be infeasible for an attacker to guess, such as using a sufficiently long cryptographically random string. If a bearer token uses an encoding mechanism to contain the authorization information in the token itself, the access token MUST use integrity protection sufficient to prevent the token from being modified. One example of an encoding and signing mechanism for access tokens is described in JSON Web Token Profile for Access Tokens [RFC9068]. + +1.4.3. Sender-Constrained Access Tokens +A sender-constrained access token binds the use of an access token to a specific sender. This sender is obliged to demonstrate knowledge of a certain secret as prerequisite for the acceptance of that access token at the recipient (e.g., a resource server). + +Authorization and resource servers SHOULD use mechanisms for sender-constraining access tokens, such as OAuth Demonstration of Proof of Possession (DPoP) [RFC9449] or Mutual TLS for OAuth 2.0 [RFC8705]. See Section 4.10.1 of [RFC9700] to prevent misuse of stolen and leaked access tokens. + +It is RECOMMENDED to use end-to-end TLS between the client and the resource server. If TLS traffic needs to be terminated at an intermediary, refer to Section 4.13 of [RFC9700] for further security advice. + +1.5. Communication security +Implementations MUST use a mechanism to provide communication authentication, integrity and confidentiality such as Transport-Layer Security [RFC8446], to protect the exchange of clear-text credentials and tokens either in the content or in header fields from eavesdropping which enables replay (e.g., see Section 2.4.1, Section 7.5.1, Section 3.2, and Section 1.4.2). + +All the OAuth protocol URLs (URLs exposed by the AS, RS and Client) MUST use the https scheme except for loopback interface redirect URIs, which MAY use the http scheme. When using https, TLS certificates MUST be checked according to Section 4.3.4 of [RFC9110]. At the time of this writing, TLS version 1.3 [RFC8446] is the most recent version. + +Implementations MAY also support additional transport-layer security mechanisms that meet their security requirements. + +The identification of the TLS versions and algorithms is outside the scope of this specification. Refer to [BCP195] for up to date recommendations on transport layer security, and to the relevant specifications for certificate validation and other security considerations. + +1.6. HTTP Redirections +This specification makes extensive use of HTTP redirections, in which the client or the authorization server directs the resource owner's user agent to another destination. While the examples in this specification show the use of the HTTP 302 status code, any other method available via the user agent to accomplish this redirection, with the exception of HTTP 307, is allowed and is considered to be an implementation detail. See Section 7.5.3 for details. + +1.7. Interoperability +OAuth 2.1 provides a rich authorization framework with well-defined security properties. + +This specification leaves a few required components partially or fully undefined (e.g., client registration, authorization server capabilities, endpoint discovery). Some of these behaviors are defined in optional extensions which implementations can choose to use, such as: + +[RFC8414]: Authorization Server Metadata, defining an endpoint clients can use to look up the information needed to interact with a particular OAuth server + +[RFC7591]: Dynamic Client Registration, providing a mechanism for programmatically registering clients with an authorization server + +[RFC7592]: Dynamic Client Management, providing a mechanism for updating dynamically registered client information + +[RFC7662]: Token Introspection, defining a mechanism for resource servers to obtain information about access tokens + +Please refer to Appendix D for a list of current known extensions at the time of this publication. + +1.8. Compatibility with OAuth 2.0 +OAuth 2.1 is compatible with OAuth 2.0 with the extensions and restrictions from known best current practices applied. Specifically, features not specified in OAuth 2.0 core, such as PKCE, are required in OAuth 2.1. Additionally, some features available in OAuth 2.0, such as the Implicit or Resource Owner Credentials grant types, are not specified in OAuth 2.1. Furthermore, some behaviors allowed in OAuth 2.0 are restricted in OAuth 2.1, such as the strict string matching of redirect URIs required by OAuth 2.1. + +See Section 10 for more details on the differences from OAuth 2.0. + +1.9. Notational Conventions +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. + +This specification uses the Augmented Backus-Naur Form (ABNF) notation of [RFC5234]. Additionally, the rule URI-reference is included from "Uniform Resource Identifier (URI): Generic Syntax" [RFC3986]. + +Certain security-related terms are to be understood in the sense defined in [RFC4949]. These terms include, but are not limited to, "attack", "authentication", "authorization", "certificate", "confidentiality", "credential", "encryption", "identity", "sign", "signature", "trust", "validate", and "verify". + +The term "content" is to be interpreted as described in Section 6.4 of [RFC9110]. + +The term "user agent" is to be interpreted as described in Section 3.5 of [RFC9110]. + +Unless otherwise noted, all the protocol parameter names and values are case sensitive. + +2. Client Registration + Before initiating the protocol, the client must have established an identifier (Section 2.2) at the authorization server. The means through which the client identifier is established with the authorization server are beyond the scope of this specification, but typically involve the client developer manually registering the client at the authorization server's website (after creating an account and agreeing to the service's Terms of Service), or by using Dynamic Client Registration [RFC7591]. Extensions may also define other programmatic methods of establishing client registration. + +Client registration does not require a direct interaction between the client and the authorization server. When supported by the authorization server, registration can rely on other means for establishing trust and obtaining the required client properties (e.g., redirect URI, client type). For example, registration can be accomplished using a self-issued or third-party-issued assertion, or by the authorization server performing client discovery using a trusted channel. + +Client registration MUST include: + +the client type as described in Section 2.1, + +client details needed by the grant type in use, such as redirect URIs as described in Section 2.3, and + +any other information required by the authorization server (e.g., application name, website, description, logo image, the acceptance of legal terms). + +Dynamic Client Registration [RFC7591] defines a common general data model for clients that may be used even with manual client registration. + +2.1. Client Types +OAuth 2.1 defines two client types based on their ability to authenticate securely with the authorization server. + +"confidential": +Clients that have credentials with the AS are designated as "confidential clients" + +"public": +Clients without credentials are called "public clients" + +Any clients with credentials MUST take precautions to prevent leakage and abuse of their credentials. + +Client authentication allows an Authorization Server to ensure it is interacting with a certain client (identified by its client_id) in an OAuth flow. The Authorization Server might make policy decisions about things such as whether to prompt the user for consent on every authorization or only the first based on the confidence that the Authorization Server is actually communicating with the legitimate client. + +Whether and how an Authorization Server validates the identity of a client or the party providing/operating this client is out of scope of this specification. Authorization servers SHOULD consider the level of confidence in a client's identity when deciding whether they allow a client access to more sensitive resources and operations such as the Client Credentials grant type and how often to prompt the user for consent. + +There is no requirement that an Authorization Server supports a particular client type. + +A single client_id SHOULD NOT be treated as more than one type of client. + +This specification has been designed around the following client profiles: + +"web application": +A web application is a client running on a web server. Resource owners access the client via an HTML user interface rendered in a user agent on the device used by the resource owner. The client credentials as well as any access tokens issued to the client are stored on the web server and are not exposed to or accessible by the resource owner. + +"browser-based application": +A browser-based application is a client in which the client code is downloaded from a web server and executes within a user agent (e.g., web browser) on the device used by the resource owner. Protocol data and credentials are easily accessible (and often visible) to the resource owner. If such applications wish to use client credentials, it is recommended to utilize the backend for frontend pattern. Since such applications reside within the user agent, they can make seamless use of the user agent capabilities when requesting authorization. + +"native application": +A native application is a client installed and executed on the device used by the resource owner. Protocol data and credentials are accessible to the resource owner. It is assumed that any client authentication credentials included in the application can be extracted. Dynamically issued access tokens and refresh tokens can receive an acceptable level of protection. On some platforms, these credentials are protected from other applications residing on the same device. If such applications wish to use client credentials, it is recommended to utilize the backend for frontend pattern, or issue the credentials at runtime using Dynamic Client Registration [RFC7591]. + +2.2. Client Identifier +Every client is identified in the context of an authorization server by a client identifier -- a unique string representing the registration information provided by the client. While the Authorization Server typically issues the client identifier itself, it may also serve clients whose client identifier was created by a party other than the Authorization Server. The client identifier is not a secret; it is exposed to the resource owner and MUST NOT be used alone for client authentication. The client identifier is unique in the context of an authorization server. + +The client identifier is an opaque string whose size is left undefined by this specification. The client should avoid making assumptions about the identifier size. The authorization server SHOULD document the size of any identifier it issues. + +If the authorization server supports clients with client identifiers issued by parties other than the authorization server, the authorization server SHOULD take precautions to avoid clients impersonating resource owners as described in Section 7.4. + +2.3. Client Redirection Endpoint +The client redirection endpoint (also referred to as "redirect endpoint") is the URI of the client that the authorization server redirects the user agent back to after completing its interaction with the resource owner. + +The authorization server redirects the user agent to one of the client's redirection endpoints previously established with the authorization server during the client registration process. + +The redirect URI MUST be an absolute URI as defined by Section 4.3 of [RFC3986]. The redirect URI MAY include an query string component (Appendix C.1), which MUST be retained when adding additional query parameters. The redirect URI MUST NOT include a fragment component. + +2.3.1. Registration Requirements +Authorization servers MUST require clients to register their complete redirect URI (including the path component). Authorization servers MUST reject authorization requests that specify a redirect URI that doesn't exactly match one that was registered, with an exception for loopback redirects, where an exact match is required except for the port URI component, see Section 4.1.1 for details. + +The authorization server MAY allow the client to register multiple redirect URIs. + +Registration may happen out of band, such as a manual step of configuring the client information at the authorization server, or may happen at runtime, such as in the initial POST in Pushed Authorization Requests [RFC9126]. + +For private-use URI scheme-based redirect URIs, authorization servers SHOULD enforce the requirement in Section 8.4.3 that clients use schemes that are reverse domain name based. At a minimum, any private-use URI scheme that doesn't contain a period character (.) SHOULD be rejected. + +In addition to the collision-resistant properties, this can help to prove ownership in the event of a dispute where two apps claim the same private-use URI scheme (where one app is acting maliciously). For example, if two apps claimed com.example.app, the owner of example.com could petition the app store operator to remove the counterfeit app. Such a petition is harder to prove if a generic URI scheme was used. + +Clients MUST NOT expose URLs that forward the user's browser to arbitrary URIs obtained from a query parameter ("open redirector"), as described in Section 7.12. Open redirectors can enable exfiltration of authorization codes and access tokens. + +The client MAY use the state request parameter to achieve per-request customization if needed rather than varying the redirect URI per request. + +Without requiring registration of redirect URIs, attackers can use the authorization endpoint as an open redirector as described in Section 7.12. + +2.3.2. Multiple Redirect URIs +If multiple redirect URIs have been registered to a client, the client MUST include a redirect URI with the authorization request using the redirect_uri request parameter (Section 4.1.1). If only a single redirect URI has been registered to a client, the redirect_uri request parameter is optional. + +2.3.3. Preventing CSRF Attacks +Clients MUST prevent Cross-Site Request Forgery (CSRF) attacks. In this context, CSRF refers to requests to the redirection endpoint that do not originate at the authorization server, but a malicious third party (see Section 4.4.1.8 of [RFC6819] for details). Clients that have ensured that the authorization server supports the code_challenge parameter MAY rely on the CSRF protection provided by that mechanism. In OpenID Connect flows, validating the nonce parameter provides CSRF protection. Otherwise, one-time use CSRF tokens carried in the state parameter that are securely bound to the user agent MUST be used for CSRF protection (see Section 7.9). + +2.3.4. Preventing Mix-Up Attacks +When an OAuth client can only interact with one authorization server, a mix-up defense is not required. In scenarios where an OAuth client interacts with two or more authorization servers, however, clients MUST prevent mix-up attacks. In order to prevent mix-up attacks, clients MUST only process redirect responses of the issuer they sent the respective request to and from the same user agent this authorization request was initiated with. + +See Section 7.14 for a detailed description of two different defenses against mix-up attacks. + +2.3.5. Invalid Endpoint +If an authorization request fails validation due to a missing, invalid, or mismatching redirect URI, the authorization server SHOULD inform the resource owner of the error and MUST NOT automatically redirect the user agent to the invalid redirect URI. + +2.3.6. Endpoint Content +The redirection request to the client's endpoint typically results in an HTML document response, processed by the user agent. If the HTML response is served directly as the result of the redirection request, any script included in the HTML document will execute with full access to the redirect URI and the artifacts (e.g., authorization code) it contains. Additionally, the request URL containing the authorization code may be sent in the HTTP Referer header to any embedded images, stylesheets and other elements loaded in the page. + +The client SHOULD NOT include any third-party scripts (e.g., third- party analytics, social plug-ins, ad networks) in the redirect URI endpoint response. Instead, it SHOULD extract the artifacts from the URI and redirect the user agent again to another endpoint without exposing the artifacts (in the URI or elsewhere). If third-party scripts are included, the client MUST ensure that its own scripts (used to extract and remove the credentials from the URI) will execute first. + +2.4. Client Authentication +The authorization server MUST only rely on client authentication if the process of issuance/registration and distribution of the underlying credentials ensures their confidentiality. + +For confidential clients, the authorization server MAY accept any form of client authentication meeting its security requirements (e.g., client secret, public/private key pair). + +It is RECOMMENDED to use asymmetric (public-key based) methods for client authentication such as mTLS [RFC8705] or using signed JWTs ("Private Key JWT") in accordance with [RFC7521], [RFC7523], and their update [I-D.ietf-oauth-rfc7523bis] (defined in [OpenID.Connect] as the client authentication method private_key_jwt). When such methods for client authentication are used, authorization servers do not need to store sensitive symmetric keys, making these methods more robust against a number of attacks, and enables clients to manage their own keys and key rotation. + +When using JWT-based client authentication, clients and authorization servers MUST follow the updated guidance around aud values in [I-D.ietf-oauth-rfc7523bis]. + +When client authentication is not possible, the authorization server SHOULD employ other means to validate the client's identity -- for example, by requiring the registration of the client redirect URI or enlisting the resource owner to confirm identity. A valid redirect URI is not sufficient to verify the client's identity when asking for resource owner authorization but can be used to prevent delivering credentials to a counterfeit client after obtaining resource owner authorization. + +The client MUST NOT use more than one authentication method in each request to prevent a conflict of which authentication mechanism is authoritative for the request. + +The authorization server MUST consider the security implications of interacting with unauthenticated clients and take measures to limit the potential exposure of tokens issued to such clients, (e.g., limiting the lifetime of refresh tokens). + +The privileges an authorization server associates with a certain client identity MUST depend on the assessment of the overall process for client identification and client credential lifecycle management. See Section 7.2 for additional details. + +2.4.1. Client Secret +To support confidential clients in possession of a client secret, the authorization server MUST support the client including the client credentials in the request body content using the following parameters: + +"client_id": +REQUIRED. The client identifier issued to the client during the registration process described by Section 2.2. + +"client_secret": +REQUIRED. The client secret. + +The parameters can only be transmitted in the request content and MUST NOT be included in the request URI. + +This is also known as client_secret_post as defined in Section 2 of [RFC7591]. + +For example, a request to refresh an access token (Section 4.3) using the content parameters (with extra line breaks for display purposes only): + +POST /token HTTP/1.1 +Host: server.example.com +Content-Type: application/x-www-form-urlencoded + +grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA +&client_id=s6BhdRkqt3&client_secret=7Fjfp0ZBr1KtDRbnfVdmIw +The authorization server MAY support the HTTP Basic authentication scheme for authenticating clients that were issued a client secret. + +When using the HTTP Basic authentication scheme as defined in Section 11 of [RFC9110] to authenticate with the authorization server, the client identifier is encoded using the application/x-www-form-urlencoded encoding algorithm per Appendix B, and the encoded value is used as the username; the client secret is encoded using the same algorithm and used as the password. + +This is also known as client_secret_basic as defined in Section 2 of [RFC7591]. + +For example (with extra line breaks for display purposes only): + +Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3 +Note: This method of initially form-encoding the client identifier and secret, and then using the encoded values as the HTTP Basic authentication username and password, has led to many interoperability problems in the past. Some implementations have missed the encoding step, or decided to only encode certain characters, or ignored the encoding requirement when validating the credentials, leading to clients having to special-case how they present the credentials to individual authorization servers. Including the credentials in the request body content avoids the encoding issues and leads to more interoperable implementations. + +Since the client secret authentication method involves a password, the authorization server MUST protect any endpoint utilizing it against brute force attacks. + +2.4.2. Other Authentication Methods +The authorization server MAY support any suitable authentication scheme matching its security requirements. When using other authentication methods, the authorization server MUST define a mapping between the client identifier (registration record) and authentication scheme. + +Some additional authentication methods such as mTLS [RFC8705] and Private Key JWT ([RFC7523], [I-D.ietf-oauth-rfc7523bis]) are defined in the "OAuth Token Endpoint Authentication Methods" registry, and may be useful as generic client authentication methods beyond the specific use of protecting the token endpoint. + +2.5. Unregistered Clients +This specification does not require that clients be registered with the authorization server. However, the use of unregistered clients is beyond the scope of this specification and requires additional security analysis and review of its interoperability impact. + +3. Protocol Endpoints + The authorization process utilizes two authorization server endpoints (HTTP resources): + +Authorization endpoint - used by the client to obtain authorization from the resource owner via user agent redirection. + +Token endpoint - used by the client to exchange an authorization grant for an access token, typically with client authentication. + +As well as one client endpoint: + +Redirection endpoint - used by the authorization server to return responses containing authorization credentials to the client via the resource owner user agent. + +Not every authorization grant type utilizes both endpoints. Extension grant types MAY define additional endpoints as needed. + +3.1. Authorization Endpoint +The authorization endpoint is used to interact with the resource owner and obtain an authorization grant. The authorization server MUST first authenticate the resource owner. The way in which the authorization server authenticates the resource owner (e.g., username and password login, passkey, federated login, or by using an established session) is beyond the scope of this specification. + +The means through which the client obtains the URL of the authorization endpoint are beyond the scope of this specification, but the URL is typically provided in the service documentation, or in the authorization server's metadata document [RFC8414]. + +The authorization endpoint URL MUST NOT include a fragment component, and MAY include a query string component Appendix C.1, which MUST be retained when adding additional query parameters. + +The authorization server MUST support the use of the HTTP GET method Section 9.3.1 of [RFC9110] for the authorization endpoint and MAY support the POST method (Section 9.3.3 of [RFC9110]) as well. + +The authorization server MUST ignore unrecognized request parameters sent to the authorization endpoint. + +Request and response parameters defined by this specification MUST NOT be included more than once. This requirement also applies to parameters defined by extensions unless the extension explicitly defines otherwise for a specific parameter. Parameters sent without a value MUST be treated as if they were omitted from the request. + +An authorization server that redirects a request potentially containing user credentials MUST avoid forwarding these user credentials accidentally (see Section 7.5.3 for details). + +Cross-Origin Resource Sharing [WHATWG.CORS] MUST NOT be supported at the Authorization Endpoint as the client does not access this endpoint directly, instead the client redirects the user agent to it. + +3.2. Token Endpoint +The token endpoint is used by the client to obtain an access token using a grant such as those described in Section 4 and Section 4.3. + +The means through which the client obtains the URL of the token endpoint are beyond the scope of this specification, but the URL is typically provided in the service documentation and configured during development of the client, or provided in the authorization server's metadata document [RFC8414] and fetched programmatically at runtime. + +The token endpoint URL MUST NOT include a fragment component, and MAY include a query string component Appendix C.1. + +The client MUST use the HTTP POST method when making requests to the token endpoint. + +The authorization server MUST ignore unrecognized request parameters sent to the token endpoint. + +Parameters sent without a value MUST be treated as if they were omitted from the request. Request and response parameters defined by this specification MUST NOT be included more than once. This requirement also applies to parameters defined by extensions unless the extension explicitly defines otherwise for a specific parameter. + +Authorization servers that wish to support browser-based applications (for example, applications running exclusively in client-side JavaScript without access to a supporting backend server) will need to ensure the token endpoint supports the necessary CORS [WHATWG.CORS] headers to allow the responses to be visible to the application. If the authorization server provides additional endpoints to the application, such as metadata URLs, dynamic client registration, revocation, introspection, discovery or user info endpoints, these endpoints may also be accessed by the browser-based application, and will also need to have the CORS headers defined to allow access. See [I-D.ietf-oauth-browser-based-apps] for further details. + +3.2.1. Client Authentication +Confidential clients MUST authenticate with the authorization server as described in Section 2.4 when making requests to the token endpoint. + +Client authentication is used for: + +Enforcing the binding of refresh tokens and authorization codes to the client they were issued to. Client authentication adds an additional layer of security when an authorization code is transmitted to the redirection endpoint over an insecure channel. + +Recovering from a compromised client by disabling the client or changing its credentials, thus preventing an attacker from abusing stolen refresh tokens. Changing a single set of client credentials is significantly faster than revoking an entire set of refresh tokens. + +Implementing authentication management best practices, which require periodic credential rotation. Rotation of an entire set of refresh tokens can be challenging, while rotation of a single set of client credentials is significantly easier. + +3.2.2. Token Endpoint Request +The client makes a request to the token endpoint by sending the following parameters using the form-encoded serialization format per Appendix C.2 with a character encoding of UTF-8 in the HTTP request content: + +"grant_type": +REQUIRED. Identifier of the grant type the client uses with the particular token request. This specification defines the values authorization_code, refresh_token, and client_credentials. The grant type determines the further parameters required or supported by the token request. The details of those grant types are defined below. + +"client_id": +OPTIONAL. The client identifier is needed when a form of client authentication that relies on the parameter is used, or the grant_type requires identification of public clients. + +Confidential clients MUST authenticate with the authorization server as described in Section 3.2.1. + +For example, the client makes the following HTTPS request (with extra line breaks for display purposes only): + +POST /token HTTP/1.1 +Host: server.example.com +Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW +Content-Type: application/x-www-form-urlencoded + +grant_type=authorization_code +&code=SplxlOBeZQQYbYS6WxSbIA +&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb +&code_verifier=3641a2d12d66101249cdf7a79c000c1f8c05d2aafcf14bf146497bed +The authorization server MUST: + +require client authentication for confidential clients (or clients with other authentication requirements), + +authenticate the client if client authentication is included + +Further grant type specific processing rules apply and are specified with the respective grant type. + +3.2.3. Token Endpoint Response +If the access token request is valid and authorized, the authorization server issues an access token and optional refresh token. + +If the client authentication failed or is invalid, the authorization server returns an error response as described in Section 3.2.4. + +The authorization server issues an access token and optional refresh token by creating an HTTP response according to Appendix C.3, using the application/json media type as defined by [RFC8259], with the following parameters and an HTTP 200 (OK) status code: + +"access_token": +REQUIRED. The access token issued by the authorization server. + +"token_type": +REQUIRED. The type of the access token issued as described in Section 1.4. Value is case insensitive. + +"expires_in": +RECOMMENDED. A JSON number that represents the lifetime in seconds of the access token. For example, the value 3600 denotes that the access token will expire in one hour from the time the response was generated. If omitted, the authorization server SHOULD provide the lifetime via other means or document the default value. Note that the authorization server may prematurely expire an access token and clients MUST NOT expect an access token to be valid for the provided lifetime. + +"scope": +RECOMMENDED, if identical to the scope requested by the client; otherwise, REQUIRED. The scope of the access token as described by Section 1.4.1. + +"refresh_token": +OPTIONAL. The refresh token, which can be used to obtain new access tokens based on the grant passed in the corresponding token request. + +Authorization servers SHOULD determine, based on a risk assessment and their own policies, whether to issue refresh tokens to a certain client. If the authorization server decides not to issue refresh tokens, the client MAY obtain new access tokens by starting the OAuth flow over, for example initiating a new authorization code request. In such a case, the authorization server may utilize cookies and persistent grants to optimize the user experience. + +If refresh tokens are issued, those refresh tokens MUST be bound to the scope and resource servers as consented by the resource owner. This is to prevent privilege escalation by the legitimate client and reduce the impact of refresh token leakage. + +The parameters are serialized into a JavaScript Object Notation (JSON) structure as described in Appendix C.3. + +The authorization server MUST include the HTTP Cache-Control response header field (see Section 5.2 of [RFC9111]) with a value of no-store in any response containing tokens, credentials, or other sensitive information. + +For example: + +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-store + +{ +"access_token": "2YotnFZFEjr1zCsicMWpAA", +"token_type": "Bearer", +"expires_in": 3600, +"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", +"example_parameter": "example_value" +} +The client MUST ignore unrecognized value names in the response. The sizes of tokens and other values received from the authorization server are left undefined. The client should avoid making assumptions about value sizes. The authorization server SHOULD document the size of any value it issues. + +3.2.4. Token Endpoint Error Response +The authorization server responds with an HTTP 400 (Bad Request) status code (unless specified otherwise) and includes the following parameters with the response: + +"error": +REQUIRED. A single ASCII [USASCII] error code from the following: + +"invalid_request": +The request is missing a required parameter, includes an unsupported parameter value (other than grant type), repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, contains a code_verifier although no code_challenge was sent in the authorization request, or is otherwise malformed. + +"invalid_client": +Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The authorization server MAY return an HTTP 401 (Unauthorized) status code to indicate which HTTP authentication schemes are supported. If the client attempted to authenticate via the Authorization request header field, the authorization server MUST respond with an HTTP 401 (Unauthorized) status code and include the WWW-Authenticate response header field matching the authentication scheme used by the client. + +"invalid_grant": +The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirect URI used in the authorization request, or was issued to another client. + +"unauthorized_client": +The authenticated client is not authorized to use this authorization grant type. + +"unsupported_grant_type": +The authorization grant type is not supported by the authorization server. + +"invalid_scope": +The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner. + +Values for the error parameter MUST NOT include characters outside the set %x20-21 / %x23-5B / %x5D-7E. + +"error_description": +OPTIONAL. Human-readable ASCII [USASCII] text providing additional information, used to assist the client developer in understanding the error that occurred. Values for the error_description parameter MUST NOT include characters outside the set %x20-21 / %x23-5B / %x5D-7E. + +"error_uri": +OPTIONAL. A URI identifying a human-readable web page with information about the error, used to provide the client developer with additional information about the error. Values for the error_uri parameter MUST conform to the URI-reference syntax and thus MUST NOT include characters outside the set %x21 / %x23-5B / %x5D-7E. + +The parameters are included in the content of the HTTP response using the application/json media type as defined in Appendix C.3. + +For example: + +HTTP/1.1 400 Bad Request +Content-Type: application/json +Cache-Control: no-store + +{ +"error": "invalid_request" +} 4. Grant Types +To request an access token, the client obtains authorization from the resource owner. This specification defines the following authorization grant types: + +authorization code + +client credentials, and + +refresh token + +It also provides an extension mechanism for defining additional grant types. + +4.1. Authorization Code Grant +The authorization code grant type is used to obtain both access tokens and refresh tokens. + +The grant type uses the additional authorization endpoint to let the authorization server interact with the resource owner in order to get consent for resource access. + +Since this is a redirect-based flow, the client must be capable of initiating the flow with the resource owner's user agent (typically a web browser) and capable of being redirected back to from the authorization server. + ++----------+ +| Resource | +| Owner | ++----------+ +^ +| +| ++-----|----+ Client Identifier +---------------+ +| .---+---------(1)-- & Redirect URI ------->| | +| | | | | | +| | '---------(2)-- User authenticates --->| | +| | User- | | Authorization | +| | Agent | | Server | +| | | | | +| | .--------(3)-- Authorization Code ---<| | ++-|----|---+ +---------------+ +| | ^ v +| | | | +^ v | | ++---------+ | | +| |>---(4)-- Authorization Code ---------' | +| Client | & Redirect URI | +| | | +| |<---(5)----- Access Token -------------------' ++---------+ (w/ Optional Refresh Token) +Figure 3: Authorization Code Flow +The flow illustrated in Figure 3 includes the following steps: + +(1) The client initiates the flow by directing the resource owner's user agent to the authorization endpoint. The client includes its client identifier, code challenge (derived from a generated code verifier), optional requested scope, optional local state, and a redirect URI to which the authorization server will send the user agent back once access is granted (or denied). + +(2) The authorization server authenticates the resource owner (via the user agent) and establishes whether the resource owner grants or denies the client's access request. + +(3) Assuming the resource owner grants access, the authorization server redirects the user agent back to the client using the redirect URI provided earlier (in the request or during client registration). The redirect URI includes an authorization code and any local state provided by the client earlier. + +(4) The client requests an access token from the authorization server's token endpoint by including the authorization code received in the previous step, and including its code verifier. When making the request, the client authenticates with the authorization server if it can. The client includes the redirect URI used to obtain the authorization code for verification. + +(5) The authorization server authenticates the client when possible, validates the authorization code, validates the code verifier, and ensures that the redirect URI received matches the URI used to redirect the user agent to the client in step (3). If valid, the authorization server responds back with an access token and, optionally, a refresh token. + +4.1.1. Authorization Request +To begin the authorization request, the client builds the authorization request URI by adding parameters to the authorization server's authorization endpoint URI. The client will eventually redirect the user agent to this URI to initiate the request. + +Clients use a unique secret, called the "code verifier", per authorization request to protect against authorization code injection and CSRF attacks. The client first generates the code verifier, then derives the "code challenge" to include in the authorization request. The client uses the code verifier when exchanging the authorization code at the token endpoint to prove that the client using the authorization code is the same client that requested it. + +The client constructs the request URI by adding the following parameters to the query component of the authorization endpoint URI as described by Appendix C.1: + +"response_type": +REQUIRED. The authorization endpoint supports different sets of request and response parameters. The client determines the type of flow by using a certain response_type value. This specification defines the value code, which must be used to signal that the client wants to use the authorization code flow. + +Extension response types MAY contain a space-delimited (%x20) list of values, where the order of values does not matter (e.g., response type a b is the same as b a). The meaning of such composite response types is defined by their respective specifications. + +Some extension response types are defined by [OpenID.Connect]. + +If an authorization request is missing the response_type parameter, or if the response type is not understood, the authorization server MUST return an error response as described in Section 4.1.2.1. + +"client_id": +REQUIRED. The client identifier as described in Section 2.2. + +"code_challenge": +REQUIRED unless the specific requirements of Section 7.5.1 are met. Code challenge derived from the code verifier. + +"code_challenge_method": +OPTIONAL, defaults to plain if not present in the request. Code verifier transformation method is S256 or plain. + +"redirect_uri": +OPTIONAL if only one redirect URI is registered for this client. REQUIRED if multiple redirect URIs are registered for this client. See Section 2.3.2. + +"scope": +OPTIONAL. The scope of the access request as described by Section 1.4.1. + +"state": +OPTIONAL. An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user agent back to the client. + +The code*verifier is a unique high-entropy cryptographically random string generated for each authorization request, using the unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "*" / "~", with a minimum length of 43 characters and a maximum length of 128 characters. + +The client stores the code_verifier temporarily, and calculates the code_challenge which it uses in the authorization request. + +ABNF for code_verifier is as follows. + +code-verifier = 43\*128unreserved +unreserved = ALPHA / DIGIT / "-" / "." / "\_" / "~" +ALPHA = %x41-5A / %x61-7A +DIGIT = %x30-39 +Clients SHOULD use code challenge methods that do not expose the code_verifier in the authorization request. Otherwise, attackers that can read the authorization request (cf. Attacker A4 in [RFC9700]) can break the security provided by this mechanism. Currently, S256 is the only such method. + +NOTE: The code verifier SHOULD have enough entropy to make it impractical to guess the value. It is RECOMMENDED that the output of a suitable random number generator be used to create a 32-octet sequence. The octet sequence is then base64url-encoded to produce a 43-octet URL-safe string to use as the code verifier. + +The client then creates a code_challenge derived from the code verifier by using one of the following transformations on the code verifier: + +S256 +code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) + +plain +code_challenge = code_verifier +If the client is capable of using S256, it MUST use S256, as S256 is Mandatory To Implement (MTI) on the server. Clients are permitted to use plain only if they cannot support S256 for some technical reason, for example constrained environments that do not have a hashing function available, and know via out-of-band configuration or via Authorization Server Metadata [RFC8414] that the server supports plain. + +ABNF for code_challenge is as follows. + +code-challenge = 43\*128unreserved +unreserved = ALPHA / DIGIT / "-" / "." / "\_" / "~" +ALPHA = %x41-5A / %x61-7A +DIGIT = %x30-39 +The properties code_challenge and code_verifier are adopted from the OAuth 2.0 extension known as "Proof-Key for Code Exchange", or PKCE [RFC7636] where this technique was originally developed. + +Authorization servers MUST support the code_challenge and code_verifier parameters. + +Clients MUST use code_challenge and code_verifier and authorization servers MUST enforce their use except under the conditions described in Section 7.5.1. Even in this case, using and enforcing code_challenge and code_verifier as described above is still RECOMMENDED. + +The state and scope parameters SHOULD NOT include sensitive client or resource owner information in plain text, as they can be transmitted over insecure channels or stored insecurely. + +The client directs the resource owner to the constructed URI using an HTTP redirection, or by other means available to it via the user agent. + +For example, the client directs the user agent to make the following HTTPS request (with extra line breaks for display purposes only): + +GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz +&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb +&code_challenge=6fdkQaPm51l13DSukcAH3Mdx7_ntecHYd1vi3n0hMZY +&code_challenge_method=S256 HTTP/1.1 +Host: server.example.com +The authorization server validates the request to ensure that all required parameters are present and valid. + +In particular, the authorization server MUST validate the redirect_uri in the request if present, ensuring that it matches one of the registered redirect URIs previously established during client registration (Section 2). When comparing the two URIs the authorization server MUST ensure that the two URIs are equal, see Section 6.2.1 of [RFC3986], Simple String Comparison, for details. The only exception is native apps using a localhost URI: In this case, the authorization server MUST allow variable port numbers as described in Section 7.3 of [RFC8252]. + +If the request is valid, the authorization server authenticates the resource owner and obtains an authorization decision (by asking the resource owner or by establishing approval via other means). + +When a decision is established, the authorization server directs the user agent to the provided client redirect URI using an HTTP redirection response, or by other means available to it via the user agent. + +4.1.2. Authorization Response +If the resource owner grants the access request, the authorization server issues an authorization code and delivers it to the client by adding the following parameters to the query component of the redirect URI using the query string serialization described by Appendix C.1, unless specified otherwise by an extension: + +"code": +REQUIRED. The authorization code is generated by the authorization server and opaque to the client. The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks. A maximum authorization code lifetime of 10 minutes is RECOMMENDED. The authorization code is bound to the client identifier, code challenge and redirect URI. + +"state": +REQUIRED if the state parameter was present in the client authorization request. The exact value received from the client. + +"iss": +OPTIONAL. The identifier of the authorization server which the client can use to prevent mix-up attacks, if the client interacts with more than one authorization server. See Section 7.14 and [RFC9207] for additional details on when this parameter is necessary, and how the client can use it to prevent mix-up attacks. + +For example, the authorization server redirects the user agent by sending the following HTTP response: + +HTTP/1.1 302 Found +Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA +&state=xyz&iss=https%3A%2F%2Fauthorization-server.example.com +The client MUST ignore unrecognized response parameters. The authorization code string size is left undefined by this specification. The client should avoid making assumptions about code value sizes. The authorization server SHOULD document the size of any value it issues. + +The authorization server MUST associate the code_challenge and code_challenge_method values with the issued authorization code so the code challenge can be verified later. + +The exact method that the server uses to associate the code_challenge with the issued code is out of scope for this specification. The code challenge could be stored on the server and associated with the code there. The code_challenge and code_challenge_method values may be stored in encrypted form in the code itself, but the server MUST NOT include the code_challenge value in a response parameter in a form that entities other than the AS can extract. + +Clients MUST prevent injection (replay) of authorization codes into the authorization response by attackers. Using code_challenge and code_verifier prevents injection of authorization codes since the authorization server will reject a token request with a mismatched code_verifier. See Section 7.5.1 for more details. + +4.1.2.1. Authorization Error Response +If the request fails due to a missing, invalid, or mismatching redirect URI, or if the client identifier is missing or invalid, the authorization server MUST NOT redirect the user agent to the invalid redirect URI and SHOULD inform the resource owner of the error, for example by displaying a message to the user in their browser. + +An authorization server MUST reject requests without a code_challenge from public clients, and MUST reject such requests from other clients unless there is reasonable assurance that the client mitigates authorization code injection in other ways. See Section 7.5.1 for details. + +If the server does not support the requested code_challenge_method transformation, the authorization endpoint MUST return the authorization error response with error value set to invalid_request. The error_description or the response of error_uri SHOULD explain the nature of error, e.g., transform algorithm not supported. + +If the resource owner denies the access request or if the request fails for reasons other than a missing or invalid redirect URI, the authorization server informs the client by redirecting the user agent to the redirect URI and adding the following parameters to the query component of the redirect URI as described by Appendix C.1: + +"error": +REQUIRED. A single ASCII [USASCII] error code from the following: + +"invalid_request": +The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. + +"unauthorized_client": +The client is not authorized to request an authorization code using this method. + +"access_denied": +The resource owner or authorization server denied the request. + +"unsupported_response_type": +The authorization server does not support obtaining an authorization code using this method. + +"invalid_scope": +The requested scope is invalid, unknown, or malformed. + +"server_error": +The authorization server encountered an unexpected condition that prevented it from fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP status code cannot be returned to the client via an HTTP redirect.) + +"temporarily_unavailable": +The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server. (This error code is needed because a 503 Service Unavailable HTTP status code cannot be returned to the client via an HTTP redirect.) + +Values for the error parameter MUST NOT include characters outside the set %x20-21 / %x23-5B / %x5D-7E. + +"error_description": +OPTIONAL. Human-readable ASCII [USASCII] text providing additional information, used to assist the client developer in understanding the error that occurred. Values for the error_description parameter MUST NOT include characters outside the set %x20-21 / %x23-5B / %x5D-7E. + +"error_uri": +OPTIONAL. A URI identifying a human-readable web page with information about the error, used to provide the client developer with additional information about the error. Values for the error_uri parameter MUST conform to the URI-reference syntax and thus MUST NOT include characters outside the set %x21 / %x23-5B / %x5D-7E. + +"state": +REQUIRED if a state parameter was present in the client authorization request. The exact value received from the client. + +"iss": +OPTIONAL. The identifier of the authorization server. See Section 4.1.2 above for details. + +For example, the authorization server indicates the request was denied by redirecting the user agent with the following HTTP response: + +HTTP/1.1 302 Found +Location: https://client.example.com/cb?error=access_denied +&state=xyz&iss=https%3A%2F%2Fauthorization-server.example.com +4.1.3. Token Endpoint Extension +The authorization grant type is identified at the token endpoint with the grant_type value of authorization_code. + +If this value is set, the following additional token request parameters beyond Section 3.2.2 are supported: + +"code": +REQUIRED. The authorization code received from the authorization server. + +"code_verifier": +REQUIRED, if the code_challenge parameter was included in the authorization request. MUST NOT be used otherwise. The original code verifier string. + +"client_id": +REQUIRED, if the client is not authenticating with the authorization server as described in Section 3.2.1. + +The authorization server MUST return an access token only once for a given authorization code. + +If a second valid token request is made with the same authorization code as a previously successful token request, the authorization server MUST deny the request and SHOULD revoke (when possible) all access tokens and refresh tokens previously issued based on that authorization code. See Section 7.5.2 for further details. + +For example, the client makes the following HTTPS request (with extra line breaks for display purposes only): + +POST /token HTTP/1.1 +Host: server.example.com +Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW +Content-Type: application/x-www-form-urlencoded + +grant_type=authorization_code +&code=SplxlOBeZQQYbYS6WxSbIA +&code_verifier=3641a2d12d66101249cdf7a79c000c1f8c05d2aafcf14bf146497bed +In addition to the processing rules in Section 3.2.2, the authorization server MUST: + +ensure that the authorization code was issued to the authenticated confidential client, or if the client is public, ensure that the code was issued to client_id in the request, + +verify that the authorization code is valid, + +verify that the code_verifier parameter is present if and only if a code_challenge parameter was present in the authorization request, + +if a code_verifier is present, verify the code_verifier by calculating the code challenge from the received code_verifier and comparing it with the previously associated code_challenge, after first transforming it according to the code_challenge_method method specified by the client, and + +If there was no code_challenge in the authorization request associated with the authorization code in the token request, the authorization server MUST reject the token request. + +See Section 10.2 for details on backwards compatibility with OAuth 2.0 clients regarding the redirect_uri parameter in the token request. + +4.2. Client Credentials Grant +The client can request an access token using only its client credentials (or other supported means of authentication) when the client is requesting access to the protected resources under its control, or those of another resource owner that have been previously arranged with the authorization server (the method of which is beyond the scope of this specification). + +The client credentials grant type MUST only be used by confidential clients. + + +---------+ +---------------+ + | | | | + | |>--(1)- Client Authentication --->| Authorization | + | Client | | Server | + | |<--(2)---- Access Token ---------<| | + | | | | + +---------+ +---------------+ + +Figure 4: Client Credentials Grant +The use of the client credentials grant illustrated in Figure 4 includes the following steps: + +(1) The client authenticates with the authorization server and requests an access token from the token endpoint. + +(2) The authorization server authenticates the client, and if valid, issues an access token. + +4.2.1. Token Endpoint Extension +The client credentials grant type is identified at the token endpoint with the grant_type value of client_credentials. + +If this value is set, the following additional token request parameters beyond Section 3.2.2 are supported: + +"scope": +OPTIONAL. The scope of the access request as described by Section 1.4.1. + +For example, the client makes the following HTTP request using transport-layer security (with extra line breaks for display purposes only): + +POST /token HTTP/1.1 +Host: server.example.com +Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW +Content-Type: application/x-www-form-urlencoded + +grant_type=client_credentials +The authorization server MUST authenticate the client. + +4.3. Refresh Token Grant +The refresh token is a credential issued by the authorization server to a client, which can be used to obtain new (fresh) access tokens based on an existing grant. The client uses this option either because the previous access token has expired or the client previously obtained an access token with a scope more narrow than approved by the respective grant and later requires an access token with a different scope under the same grant. + +Refresh tokens MUST be kept confidential in transit and storage, and shared only among the authorization server and the client to whom the refresh tokens were issued. The authorization server MUST maintain the binding between a refresh token and the client to whom it was issued. + +The authorization server MUST verify the binding between the refresh token and client identity whenever the client identity can be authenticated. When client authentication is not possible, the authorization server SHOULD issue sender-constrained refresh tokens or use refresh token rotation as described in Section 4.3.1. + +The authorization server MUST ensure that refresh tokens cannot be generated, modified, or guessed to produce valid refresh tokens by unauthorized parties. + +4.3.1. Token Endpoint Extension +The refresh token grant type is identified at the token endpoint with the grant_type value of refresh_token. + +If this value is set, the following additional parameters beyond Section 3.2.2 are supported: + +"refresh_token": +REQUIRED. The refresh token issued to the client. + +"scope": +OPTIONAL. The scope of the access request as described by Section 1.4.1. The requested scope MUST NOT include any scope not originally granted by the resource owner, and if omitted is treated as equal to the scope originally granted by the resource owner. + +Because refresh tokens are typically long-lasting credentials used to request additional access tokens, the refresh token is bound to the client to which it was issued. Confidential clients MUST authenticate with the authorization server as described in Section 3.2.1. + +For example, the client makes the following HTTP request using transport-layer security (with extra line breaks for display purposes only): + +POST /token HTTP/1.1 +Host: server.example.com +Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW +Content-Type: application/x-www-form-urlencoded + +grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA +In addition to the processing rules in Section 3.2.2, the authorization server MUST: + +if client authentication is included in the request, ensure that the refresh token was issued to the authenticated client, OR if a client_id is included in the request, ensure the refresh token was issued to the matching client + +validate that the grant corresponding to this refresh token is still active + +validate the refresh token + +Authorization servers MUST utilize one of these methods to detect refresh token replay by malicious actors for public clients: + +Sender-constrained refresh tokens: the authorization server cryptographically binds the refresh token to a certain client instance, e.g., by utilizing DPoP [RFC9449] or mTLS [RFC8705]. + +Refresh token rotation: the authorization server issues a new refresh token with every access token refresh response. The previous refresh token is invalidated but information about the relationship is retained by the authorization server. If a refresh token is compromised and subsequently used by both the attacker and the legitimate client, one of them will present an invalidated refresh token, which will inform the authorization server of the breach. The authorization server cannot determine which party submitted the invalid refresh token, but it will revoke the active refresh token as well as the access authorization grant associated with it. This stops the attack at the cost of forcing the legitimate client to obtain a fresh authorization grant. + +Implementation note: the grant to which a refresh token belongs may be encoded into the refresh token itself. This can enable an authorization server to efficiently determine the grant to which a refresh token belongs, and by extension, all refresh tokens that need to be revoked. Authorization servers MUST ensure the integrity of the refresh token value in this case, for example, using signatures. + +4.3.2. Refresh Token Response +If valid and authorized, the authorization server issues an access token as described in Section 3.2.3. + +The authorization server MAY issue a new refresh token, in which case the client MUST discard the old refresh token and replace it with the new refresh token. + +4.3.3. Refresh Token Recommendations +The authorization server MAY revoke the old refresh token after issuing a new refresh token to the client. If a new refresh token is issued, the refresh token scope MUST be identical to that of the refresh token included by the client in the request. + +Authorization servers MAY revoke refresh tokens automatically in case of a security event, such as: + +password change + +logout at the authorization server + +Refresh tokens SHOULD expire if the client has been inactive for some time, i.e., the refresh token has not been used to obtain new access tokens for some time. The expiration time is at the discretion of the authorization server. It might be a global value or determined based on the client policy or the grant associated with the refresh token (and its sensitivity). + +4.4. Extension Grants +The client uses an extension grant type by specifying the grant type using an absolute URI (defined by the authorization server) as the value of the grant_type parameter of the token endpoint, and by adding any additional parameters necessary. + +For example, to request an access token using the Device Authorization Grant as defined by [RFC8628] after the user has authorized the client on a separate device, the client makes the following HTTPS request (with extra line breaks for display purposes only): + +POST /token HTTP/1.1 +Host: server.example.com +Content-Type: application/x-www-form-urlencoded + +grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code +&device_code=GmRhmhcxhwEzkoEqiMEg_DnyEysNkuNhszIySk9eS +&client_id=C409020731 +If the access token request is valid and authorized, the authorization server issues an access token and optional refresh token as described in Section 3.2.3. If the request failed client authentication or is invalid, the authorization server returns an error response as described in Section 3.2.4. + +5. Resource Requests + The client accesses protected resources by presenting an access token to the resource server. The resource server MUST validate the access token and ensure that it has not expired and that its scope covers the requested resource. The methods used by the resource server to validate the access token are beyond the scope of this specification, but generally involve an interaction or coordination between the resource server and the authorization server. For example, when the resource server and authorization server are colocated or are part of the same system, they may share a database or other storage; when the two components are operated independently, they may use Token Introspection [RFC7662] or a structured access token format such as a JWT [RFC9068]. + +5.1. Bearer Token Requests +This section defines two methods of sending Bearer tokens in resource requests to resource servers. Clients MUST use one of the two methods defined below, and MUST NOT use more than one method to transmit the token in each request. + +In particular, clients MUST NOT send the access token in a URI query parameter, and resource servers MUST ignore access tokens in a URI query parameter. + +5.1.1. Authorization Request Header Field +When sending the access token in the Authorization request header field defined by HTTP/1.1 [RFC7235], the client uses the Bearer scheme to transmit the access token. + +For example: + +GET /resource HTTP/1.1 +Host: server.example.com +Authorization: Bearer mF_9.B5f-4.1JqM +The syntax of the Authorization header field for this scheme follows the usage of the Basic scheme defined in Section 2 of [RFC2617]. Note that, as with Basic, it does not conform to the generic syntax defined in Section 1.2 of [RFC2617] but is compatible with the general authentication framework in HTTP 1.1 Authentication [RFC7235], although it does not follow the preferred practice outlined therein in order to reflect existing deployments. The syntax for Bearer credentials is as follows: + +token68 = 1*( ALPHA / DIGIT / +"-" / "." / "\_" / "~" / "+" / "/" ) *"=" +credentials = "bearer" 1\*SP token68 +Clients SHOULD make authenticated requests with a bearer token using the Authorization request header field with the Bearer HTTP authorization scheme. Resource servers MUST support this method. + +As described in Section 11.1 of [RFC9110], the string bearer is case-insensitive. This means all of the following are valid uses of the Authorization header: + +Authorization: Bearer mF_9.B5f-4.1JqM + +Authorization: bearer mF_9.B5f-4.1JqM + +Authorization: BEARER mF_9.B5f-4.1JqM + +Authorization: bEaReR mF_9.B5f-4.1JqM + +5.1.2. Form-Encoded Content Parameter +When sending the access token in the HTTP request content, the client adds the access token to the request content using the access_token parameter. The client MUST NOT use this method unless all of the following conditions are met: + +The HTTP request includes the Content-Type header field set to application/x-www-form-urlencoded. + +The content follows the encoding requirements of the application/x-www-form-urlencoded content-type as defined by the URL Living Standard [WHATWG.URL]. + +The HTTP request content is single-part. + +The content to be encoded in the request MUST consist entirely of ASCII [USASCII] characters. + +The HTTP request method is one for which the content has defined semantics. In particular, this means that the GET method MUST NOT be used. + +The content MAY include other request-specific parameters, in which case the access_token parameter MUST be properly separated from the request-specific parameters using & character(s) (ASCII code 38). + +For example, the client makes the following HTTP request using transport-layer security: + +POST /resource HTTP/1.1 +Host: server.example.com +Content-Type: application/x-www-form-urlencoded + +access_token=mF_9.B5f-4.1JqM +The application/x-www-form-urlencoded method SHOULD NOT be used except in application contexts where participating clients do not have access to the Authorization request header field. Resource servers MAY support this method. + +5.2. Access Token Validation +After receiving the access token, the resource server MUST check that the access token is not yet expired, is authorized to access the requested resource, was issued with the appropriate scope, and meets other policy requirements of the resource server to access the protected resource. + +Access tokens generally fall into two categories: reference tokens or self-encoded tokens. Reference tokens can be validated by querying the authorization server or looking up the token in a token database, whereas self-encoded tokens contain the authorization information in an encrypted and/or signed string which can be extracted by the resource server. + +A standardized method to query the authorization server to check the validity of an access token is defined in Token Introspection [RFC7662]. + +A standardized method of encoding information in a token string is defined in JWT Profile for Access Tokens [RFC9068]. + +See Section 7.1 for additional considerations around creating and validating access tokens. + +5.3. Error Response +If a resource access request fails, the resource server SHOULD inform the client of the error. The details of the error response is determined by the particular token type, such as the description of Bearer tokens in Section 5.3.2. + +5.3.1. The WWW-Authenticate Response Header Field +If the protected resource request does not include authentication credentials or does not contain an access token that enables access to the protected resource, the resource server MUST include the HTTP WWW-Authenticate response header field; it MAY include it in response to other conditions as well. The WWW-Authenticate header field uses the framework defined by HTTP/1.1 [RFC7235]. + +All challenges for this token type MUST use the auth-scheme value Bearer. This scheme MUST be followed by one or more auth-param values. The auth-param attributes used or defined by this specification for this token type are as follows. Other auth-param attributes MAY be used as well. + +"realm": +A realm attribute MAY be included to indicate the scope of protection in the manner described in HTTP/1.1 [RFC7235]. The realm attribute MUST NOT appear more than once. + +"scope": +The scope attribute is defined in Section 1.4.1. The scope attribute is a space-delimited list of case-sensitive scope values indicating the required scope of the access token for accessing the requested resource. scope values are implementation defined; there is no centralized registry for them; allowed values are defined by the authorization server. The order of scope values is not significant. In some cases, the scope value will be used when requesting a new access token with sufficient scope of access to utilize the protected resource. Use of the scope attribute is OPTIONAL. The scope attribute MUST NOT appear more than once. The scope value is intended for programmatic use and is not meant to be displayed to end users. + +Two example scope values follow; these are taken from the OpenID Connect [OpenID.Messages] and the Open Authentication Technology Committee (OATC) Online Multimedia Authorization Protocol [OMAP] OAuth 2.0 use cases, respectively: + +scope="openid profile email" +scope="urn:example:channel=HBO&urn:example:rating=G,PG-13" +"error": +If the protected resource request included an access token and failed authentication, the resource server SHOULD include the error attribute to provide the client with the reason why the access request was declined. The parameter value is described in Section 5.3.2. + +"error_description": +The resource server MAY include the error_description attribute to provide developers a human-readable explanation that is not meant to be displayed to end users. + +"error_uri": +The resource server MAY include the error_uri attribute with an absolute URI identifying a human-readable web page explaining the error. + +The error, error_description, and error_uri attributes MUST NOT appear more than once. + +Values for the scope attribute (specified in Appendix A.4) MUST NOT include characters outside the set %x21 / %x23-5B / %x5D-7E for representing scope values and %x20 for delimiters between scope values. Values for the error and error_description attributes (specified in Appendix A.7 and Appendix A.8) MUST NOT include characters outside the set %x20-21 / %x23-5B / %x5D-7E. Values for the error_uri attribute (specified in Appendix A.9 of) MUST conform to the URI-reference syntax and thus MUST NOT include characters outside the set %x21 / %x23-5B / %x5D-7E. + +5.3.2. Error Codes +When a request fails, the resource server responds using the appropriate HTTP status code (typically, 400, 401, 403, or 405) and includes one of the following error codes in the response: + +"invalid_request": +The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats the same parameter, uses more than one method for including an access token, or is otherwise malformed. The resource server SHOULD respond with the HTTP 400 (Bad Request) status code. + +"invalid_token": +The access token provided is expired, revoked, malformed, or invalid for other reasons. The resource server SHOULD respond with the HTTP 401 (Unauthorized) status code. The client MAY request a new access token and retry the protected resource request. + +"insufficient_scope": +The request requires higher privileges (scopes) than provided by the scopes granted to the client and represented by the access token. The resource server SHOULD respond with the HTTP 403 (Forbidden) status code and MAY include the scope attribute with the scope necessary to access the protected resource. + +Extensions may define additional error codes or specify additional circumstances in which the above error codes are returned. + +If the request lacks any authentication information (e.g., the client was unaware that authentication is necessary or attempted using an unsupported authentication method), the resource server SHOULD NOT include an error code or other error information. + +For example: + +HTTP/1.1 401 Unauthorized +WWW-Authenticate: Bearer realm="example" +And in response to a protected resource request with an authentication attempt using an expired access token: + +HTTP/1.1 401 Unauthorized +WWW-Authenticate: Bearer realm="example", +error="invalid_token", +error_description="The access token expired" 6. Extensibility +6.1. Defining Access Token Types +Access token types can be defined in one of two ways: registered in the Access Token Types registry (following the procedures in Section 11.1 of [RFC6749]), or by using a unique absolute URI as its name. + +6.1.1. Registered Access Token Types +[RFC6750] establishes a common registry in Section 11.4 of [RFC6749] for error values to be shared among OAuth token authentication schemes. + +New authentication schemes designed primarily for OAuth token authentication SHOULD define a mechanism for providing an error status code to the client, in which the error values allowed are registered in the error registry established by this specification. + +Such schemes MAY limit the set of valid error codes to a subset of the registered values. If the error code is returned using a named parameter, the parameter name SHOULD be error. + +Other schemes capable of being used for OAuth token authentication, but not primarily designed for that purpose, MAY bind their error values to the registry in the same manner. + +New authentication schemes MAY choose to also specify the use of the error_description and error_uri parameters to return error information in a manner parallel to their usage in this specification. + +Type names MUST conform to the type-name ABNF. If the type definition includes a new HTTP authentication scheme, the type name SHOULD be identical to the HTTP authentication scheme name (as defined by [RFC2617]). The token type example is reserved for use in examples. + +type-name = 1\*name-char +name-char = "-" / "." / "\_" / DIGIT / ALPHA +6.1.2. Vendor-Specific Access Token Types +Types utilizing a URI name SHOULD be limited to vendor-specific implementations that are not commonly applicable, and are specific to the implementation details of the resource server where they are used. + +All other types MUST be registered. + +6.2. Defining New Endpoint Parameters +New request or response parameters for use with the authorization endpoint or the token endpoint are defined and registered in the OAuth Parameters registry following the procedure in Section 11.2 of [RFC6749]. + +Parameter names MUST conform to the param-name ABNF, and parameter values syntax MUST be well-defined (e.g., using ABNF, or a reference to the syntax of an existing parameter). + +param-name = 1\*name-char +name-char = "-" / "." / "_" / DIGIT / ALPHA +Unregistered vendor-specific parameter extensions that are not commonly applicable and that are specific to the implementation details of the authorization server where they are used SHOULD utilize a vendor-specific prefix that is not likely to conflict with other registered values (e.g., begin with 'companyname_'). + +6.3. Defining New Authorization Grant Types +New authorization grant types can be defined by assigning them a unique absolute URI for use with the grant_type parameter. If the extension grant type requires additional token endpoint parameters, they MUST be registered in the OAuth Parameters registry as described by Section 11.2 of [RFC6749]. + +6.4. Defining New Authorization Endpoint Response Types +New response types for use with the authorization endpoint are defined and registered in the Authorization Endpoint Response Types registry following the procedure in Section 11.3 of [RFC6749]. Response type names MUST conform to the response-type ABNF. + +response-type = response-name *( SP response-name ) +response-name = 1*response-char +response-char = "\_" / DIGIT / ALPHA +If a response type contains one or more space characters (%x20), it is compared as a space-delimited list of values in which the order of values does not matter. Only one order of values can be registered, which covers all other arrangements of the same set of values. + +For example, an extension can define and register the code other_token response type. Once registered, the same combination cannot be registered as other_token code, but both values can be used to denote the same response type. + +6.5. Defining Additional Error Codes +In cases where protocol extensions (i.e., access token types, extension parameters, or extension grant types) require additional error codes to be used with the authorization code grant error response (Section 4.1.2.1), the token error response (Section 3.2.4), or the resource access error response (Section 5.3), such error codes MAY be defined. + +Extension error codes MUST be registered (following the procedures in Section 11.4 of [RFC6749]) if the extension they are used in conjunction with is a registered access token type, a registered endpoint parameter, or an extension grant type. Error codes used with unregistered extensions MAY be registered. + +Error codes MUST conform to the error ABNF and SHOULD be prefixed by an identifying name when possible. For example, an error identifying an invalid value set to the extension parameter example SHOULD be named example_invalid. + +error = 1\*error-char +error-char = %x20-21 / %x23-5B / %x5D-7E 7. Security Considerations +As a flexible and extensible framework, OAuth's security considerations depend on many factors. The following sections provide implementers with security guidelines focused on the three client profiles described in Section 2.1: web application, browser-based application, and native application. + +A comprehensive OAuth security model and analysis, as well as background for the protocol design, is provided by [RFC6819] and [RFC9700]. + +7.1. Access Token Security Considerations +7.1.1. Security Threats +The following list presents several common threats against protocols utilizing some form of tokens. This list of threats is based on NIST Special Publication 800-63 [NIST800-63]. + +7.1.1.1. Access token manufacture/modification +An attacker may generate a bogus access token or modify the token contents (such as the authentication or attribute statements) of an existing token, causing the resource server to grant inappropriate access to the client. For example, an attacker may modify the token to extend the validity period; a malicious client may modify the assertion to gain access to information that they should not be able to view. + +7.1.1.2. Access token information disclosure +Access tokens may contain authentication and attribute statements that include sensitive information. + +If the client should be prevented from observing the contents of the access token, content encryption MUST be applied. + +Since cookies are by default transmitted in cleartext, any information contained in them is at risk of disclosure: Bearer tokens MUST NOT be stored in cookies that can be sent in the clear. See Section 7 and 8 of [RFC6265] for security considerations about cookies. + +7.1.1.3. Access token redirect +An attacker uses an access token generated for consumption by one resource server to gain access to a different resource server that mistakenly believes the token to be for it. + +7.1.1.4. Access token replay +An attacker attempts to use an access token that has already been used with that resource server in the past. + +7.1.2. Threat Mitigation +A large range of threats can be mitigated by protecting the contents of the access token by using a digital signature, and by following best practices for signing key management such as periodic key rotation. + +Alternatively, a bearer token can contain a reference to authorization information, rather than encoding the information directly. Using a reference may require an extra interaction between a resource server and authorization server to resolve the reference to the authorization information. The mechanics of such an interaction are not defined by this specification, but one such mechanism is defined in Token Introspection [RFC7662]. + +This document does not specify the encoding or the contents of the access token; hence, detailed recommendations about the means of guaranteeing access token integrity protection are outside the scope of this specification. One example of an encoding and signing mechanism for access tokens is described in JSON Web Token Profile for Access Tokens [RFC9068]. + +To deal with access token redirects, it is important for the authorization server to include the identity of the intended recipients (the audience), typically a single resource server (or a list of resource servers), in the token. Restricting the use of the token to a specific scope is also RECOMMENDED. + +Section 1.5 provides information to protect against access token disclosure and providing confidentiality and integrity for the communications between client, resource server and authorization server. + +7.1.3. Summary of Recommendations +7.1.3.1. Safeguard bearer tokens +Client implementations MUST ensure that bearer tokens are not leaked to unintended parties, as they will be able to use them to gain access to protected resources. This is the primary security consideration when using bearer tokens and underlies all the more specific recommendations that follow. + +7.1.3.2. Validate TLS certificate chains +The client MUST validate the TLS certificate chain when making requests to protected resources. Failing to do so may enable DNS hijacking attacks to steal the token and gain unintended access. + +7.1.3.3. Always use TLS (https) +Clients MUST always use TLS (https) or equivalent transport security when making requests with bearer tokens. Failing to do so exposes the token to numerous attacks that could give attackers unintended access. + +7.1.3.4. Don't store bearer tokens in HTTP cookies +Implementations MUST NOT store bearer tokens within cookies that can be sent in the clear (which is the default transmission mode for cookies). Implementations that do store bearer tokens in cookies MUST take precautions against cross-site request forgery. + +7.1.3.5. Issue short-lived bearer tokens +Authorization servers SHOULD issue short-lived bearer tokens, particularly when issuing tokens to clients that run within a web browser or other environments where information leakage may occur. Using short-lived bearer tokens can reduce the impact of them being leaked. + +7.1.3.6. Issue scoped bearer tokens +Authorization servers SHOULD issue bearer tokens that contain an audience restriction, scoping their use to the intended resource server or set of resource servers. + +7.1.3.7. Don't pass bearer tokens in page URLs +Bearer tokens MUST NOT be passed in page URLs (for example, as query string parameters). Instead, bearer tokens SHOULD be passed in HTTP message headers or message bodies for which confidentiality measures are taken. Browsers, web servers, and other software may not adequately secure URLs in the browser history, web server logs, and other data structures. If bearer tokens are passed in page URLs, attackers might be able to steal them from the history data, logs, or other unsecured locations. + +7.1.4. Access Token Privilege Restriction +The privileges associated with an access token SHOULD be restricted to the minimum required for the particular application or use case. This prevents clients from exceeding the privileges authorized by the resource owner. It also prevents users from exceeding their privileges authorized by the respective security policy. Privilege restrictions also help to reduce the impact of access token leakage. + +In particular, access tokens SHOULD be restricted to certain resource servers (audience restriction), preferably to a single resource server. To put this into effect, the authorization server associates the access token with certain resource servers and every resource server is obliged to verify, for every request, whether the access token sent with that request was meant to be used for that particular resource server. If not, the resource server MUST refuse to serve the respective request. Clients and authorization servers MAY utilize the parameters scope or resource as specified in this document and [RFC8707], respectively, to determine the resource server they want to access. + +Additionally, access tokens SHOULD be restricted to certain resources and actions on resource servers or resources. To put this into effect, the authorization server associates the access token with the respective resource and actions and every resource server is obliged to verify, for every request, whether the access token sent with that request was meant to be used for that particular action on the particular resource. If not, the resource server must refuse to serve the respective request. Clients and authorization servers MAY utilize the parameter scope and authorization_details as specified in [RFC9396] to determine those resources and/or actions. + +7.2. Client Authentication +Depending on the overall process of client registration and credential lifecycle management, this may affect the confidence an authorization server has in a particular client. + +For example, authentication of a dynamically registered client does not prove the identity of the client, it only ensures that repeated requests to the authorization server were made from the same client instance. Such clients may be limited in terms of which scopes they are allowed to request, or may have other limitations such as shorter token lifetimes. + +In contrast, if there is a registered application whose developer's identity was verified, who signed a contract and is issued a client secret that is only used in a secure backend service, the authorization server might allow this client to request more sensitive scopes or to be issued longer-lasting tokens. + +7.3. Client Impersonation +If a confidential client has its credentials stolen, a malicious client can impersonate the client and obtain access to protected resources. + +The authorization server SHOULD enforce explicit resource owner authentication and provide the resource owner with information about the client and the requested authorization scope and lifetime. It is up to the resource owner to review the information in the context of the current client and to authorize or deny the request. + +The authorization server SHOULD NOT process repeated authorization requests automatically (without active resource owner interaction) without authenticating the client or relying on other measures to ensure that the repeated request comes from the original client and not an impersonator. + +7.3.1. Impersonation of Native Apps +As stated above, the authorization server SHOULD NOT process authorization requests automatically without user consent or interaction, except when the identity of the client can be assured. This includes the case where the user has previously approved an authorization request for a given client ID -- unless the identity of the client can be proven, the request SHOULD be processed as if no previous request had been approved. + +Measures such as claimed https scheme redirects MAY be accepted by authorization servers as identity proof. Some operating systems may offer alternative platform-specific identity features that MAY be accepted, as appropriate. + +7.3.2. Access Token Privilege Restriction +The client SHOULD request access tokens with the minimal scope necessary. The authorization server SHOULD take the client identity into account when choosing how to honor the requested scope and MAY issue an access token with fewer scopes than requested. + +The privileges associated with an access token SHOULD be restricted to the minimum required for the particular application or use case. This prevents clients from exceeding the privileges authorized by the resource owner. It also prevents users from exceeding their privileges authorized by the respective security policy. Privilege restrictions also help to reduce the impact of access token leakage. + +In particular, access tokens SHOULD be restricted to certain resource servers (audience restriction), preferably to a single resource server. To put this into effect, the authorization server associates the access token with certain resource servers and every resource server is obliged to verify, for every request, whether the access token sent with that request was meant to be used for that particular resource server. If not, the resource server MUST refuse to serve the respective request. Clients and authorization servers MAY utilize the parameters scope or resource as specified in [RFC8707], respectively, to determine the resource server they want to access. + +7.4. Client Impersonating Resource Owner +Resource servers may make access control decisions based on the identity of a resource owner for which an access token was issued, or based on the identity of a client in the client credentials grant. If both options are possible, depending on the details of the implementation, a client's identity may be mistaken for the identity of a resource owner. For example, if a client is able to choose its own client_id during registration with the authorization server, a malicious client may set it to a value identifying an end user (e.g., a sub value if OpenID Connect is used). If the resource server cannot properly distinguish between access tokens issued to clients and access tokens issued to end users, the client may then be able to access resource of the end user. + +If the authorization server has a common namespace for client IDs and user identifiers, causing the resource server to be unable to distinguish an access token authorized by a resource owner from an access token authorized by a client itself, authorization servers SHOULD NOT allow clients to influence their client_id or any other Claim if that can cause confusion with a genuine resource owner. Where this cannot be avoided, authorization servers MUST provide other means for the resource server to distinguish between the two types of access tokens. + +7.5. Authorization Code Security Considerations +7.5.1. Authorization Code Injection +Authorization code injection is an attack where the client receives an authorization code from the attacker in its redirect URI instead of the authorization code from the legitimate authorization server. Without protections in place, there is no mechanism by which the client can know that the attack has taken place. Authorization code injection can lead to both the attacker obtaining access to a victim's account, as well as a victim accidentally gaining access to the attacker's account. + +7.5.1.1. Countermeasures +To prevent injection of authorization codes into the client, using code_challenge and code_verifier is REQUIRED for clients, and authorization servers MUST enforce their use, unless both of the following criteria are met: + +The client is a confidential client. + +In the specific deployment and the specific request, there is reasonable assurance by the authorization server that the client implements the OpenID Connect nonce mechanism properly. + +In this case, using and enforcing code_challenge and code_verifier is still RECOMMENDED. + +The code_challenge or OpenID Connect nonce value MUST be transaction-specific and securely bound to the client and the user agent in which the transaction was started. If a transaction leads to an error, fresh values for code_challenge or nonce MUST be chosen. + +Relying on the client to validate the OpenID Connect nonce parameter means the authorization server has no way to confirm that the client has actually protected itself against authorization code injection attacks. If an attacker is able to inject an authorization code into a client, the client would still exchange the injected authorization code and obtain tokens, and would only later reject the ID token after validating the nonce and seeing that it doesn't match. In contrast, the authorization server enforcing the code_challenge and code_verifier parameters provides a higher security outcome, since the authorization server is able to recognize the authorization code injection attack pre-emptively and avoid issuing any tokens in the first place. + +Historic note: Although PKCE [RFC7636] (where the code_challenge and code_verifier parameters were created) was originally designed as a mechanism to protect native apps from authorization code exfiltration attacks, all kinds of OAuth clients, including web applications and other confidential clients, are susceptible to authorization code injection attacks, which are solved by the code_challenge and code_verifier mechanism. + +7.5.2. Reuse of Authorization Codes +Several types of attacks are possible if authorization codes are able to be used more than once. + +As described in Section 4.1.3, the authorization server must reject a token request and revoke any issued tokens when receiving a second valid request with an authorization code that has already been used to issue an access token. If an attacker is able to exfiltrate an authorization code and use it before the legitimate client, the attacker will obtain the access token and the legitimate client will not. Revoking any issued tokens means the attacker's tokens will then be revoked, stopping the attack from proceeding any further. + +However, the authorization server should only revoke issued tokens if the request containing the authorization code is also valid, including any other parameters such as the code_verifier and client authentication. The authorization server SHOULD NOT revoke any issued tokens when receiving a replayed authorization code that contains invalid parameters. If it were to do so, this would create a denial of service opportunity for an attacker who is able to obtain an authorization code but unable to obtain the client authentication or code_verifier by sending an invalid authorization code request before the legitimate client and thereby revoking the legitimate client's tokens once it makes the valid request. + +7.5.3. HTTP 307 Redirect +An authorization server which redirects a request that potentially contains user credentials MUST NOT use the 307 status code (Section 15.4.8 of [RFC9110]) for redirection. If an HTTP redirection (and not, for example, JavaScript) is used for such a request, AS SHOULD use the status code 303 ("See Other"). + +At the authorization endpoint, a typical protocol flow is that the AS prompts the user to enter their credentials in a form that is then submitted (using the POST method) back to the authorization server. The AS checks the credentials and, if successful, redirects the user agent to the client's redirect URI. + +If the status code 307 were used for redirection, the user agent would send the user credentials via a POST request to the client. + +This discloses the sensitive credentials to the client. If the client is malicious, it can use the credentials to impersonate the user at the AS. + +The behavior might be unexpected for developers, but is defined in Section 15.4.8 of [RFC9110]. This status code does not require the user agent to rewrite the POST request to a GET request and thereby drop the form data in the POST request content. + +In HTTP [RFC9110], only the status code 303 unambiguously enforces rewriting the HTTP POST request to an HTTP GET request. For all other status codes, including the popular 302, user agents can opt not to rewrite POST to GET requests and therefore reveal the user credentials to the client. (In practice, however, most user agents will only show this behaviour for 307 redirects.) + +7.6. Ensuring Endpoint Authenticity +The risk related to man-in-the-middle attacks is mitigated by the mandatory use of channel security mechanisms such as [RFC8446] for communicating with the Authorization and Token Endpoints. See Section 1.5 for further details. + +7.7. Credentials-Guessing Attacks +The authorization server MUST prevent attackers from guessing access tokens, authorization codes, refresh tokens, resource owner passwords, and client credentials. + +The probability of an attacker guessing generated tokens (and other credentials not intended for handling by end users) MUST be less than or equal to 2^(-128) and SHOULD be less than or equal to 2^(-160). + +The authorization server MUST utilize other means to protect credentials intended for end-user usage. + +7.8. Phishing Attacks +Wide deployment of this and similar protocols may cause end users to become inured to the practice of being redirected to websites where they are asked to enter their passwords. If end users are not careful to verify the authenticity of these websites before entering their credentials, it will be possible for attackers to exploit this practice to steal resource owners' passwords, and other phishable credentials such as OTPs. + +Service providers should attempt to educate end users about the risks phishing attacks pose and should provide mechanisms that make it easy for end users to confirm the authenticity of their sites, such as using phishing-resistant authenticators, as phishing resistant authenticators will offer a credential to log in to a certain site to the user only if the platform has successfully verified the site's origin. Client developers should consider the security implications of how they interact with the user agent (e.g., external, embedded), and the ability of the end user to verify the authenticity of the authorization server. + +See Section 1.5 for further details on mitigating the risk of phishing attacks. + +7.9. Cross-Site Request Forgery +An attacker might attempt to inject a request to the redirect URI of the legitimate client on the victim's device, e.g., to cause the client to access resources under the attacker's control. This is a variant of an attack known as Cross-Site Request Forgery (CSRF). + +The traditional countermeasure is that clients pass a random value, also known as a CSRF Token, in the state parameter that links the request to the redirect URI to the user agent session as described. This countermeasure is described in detail in Section 5.3.5 of [RFC6819]. The same protection is provided by the code_verifier parameter or the OpenID Connect nonce value. + +When using code_verifier instead of state or nonce for CSRF protection, it is important to note that: + +Clients MUST ensure that the AS supports the code_challenge_method intended to be used by the client. If an authorization server does not support the requested method, state or nonce MUST be used for CSRF protection instead. + +If state is used for carrying application state, and integrity of its contents is a concern, clients MUST protect state against tampering and swapping. This can be achieved by binding the contents of state to the browser session and/or signed/encrypted state values [I-D.bradley-oauth-jwt-encoded-state]. + +AS therefore MUST provide a way to detect their supported code challenge methods either via AS metadata according to [RFC8414] or provide a deployment-specific way to ensure or determine support. + +7.10. Clickjacking +As described in Section 4.4.1.9 of [RFC6819], the authorization request is susceptible to clickjacking attacks, also called user interface redressing. In such an attack, an attacker embeds the authorization endpoint user interface in an innocuous context. A user believing to interact with that context, for example, clicking on buttons, inadvertently interacts with the authorization endpoint user interface instead. The opposite can be achieved as well: A user believing to interact with the authorization endpoint might inadvertently type a password into an attacker-provided input field overlaid over the original user interface. Clickjacking attacks can be designed such that users can hardly notice the attack, for example using almost invisible iframes overlaid on top of other elements. + +An attacker can use this vector to obtain the user's authentication credentials, change the scope of access granted to the client, and potentially access the user's resources. + +Authorization servers MUST prevent clickjacking attacks. Multiple countermeasures are described in [RFC6819], including the use of the X-Frame-Options HTTP response header field and frame-busting JavaScript. In addition to those, authorization servers SHOULD also use Content Security Policy (CSP) level 2 [CSP-2] or greater. + +To be effective, CSP must be used on the authorization endpoint and, if applicable, other endpoints used to authenticate the user and authorize the client (e.g., the device authorization endpoint, login pages, error pages, etc.). This prevents framing by unauthorized origins in user agents that support CSP. The client MAY permit being framed by some other origin than the one used in its redirection endpoint. For this reason, authorization servers SHOULD allow administrators to configure allowed origins for particular clients and/or for clients to register these dynamically. + +Using CSP allows authorization servers to specify multiple origins in a single response header field and to constrain these using flexible patterns (see [CSP-2] for details). Level 2 of this standard provides a robust mechanism for protecting against clickjacking by using policies that restrict the origin of frames (using frame-ancestors) together with those that restrict the sources of scripts allowed to execute on an HTML page (by using script-src). A non-normative example of such a policy is shown in the following listing: + +HTTP/1.1 200 OK +Content-Security-Policy: frame-ancestors https://ext.example.org:8000 +Content-Security-Policy: script-src 'self' +X-Frame-Options: ALLOW-FROM https://ext.example.org:8000 +... +Because some user agents do not support [CSP-2], this technique SHOULD be combined with others, including those described in [RFC6819], unless such legacy user agents are explicitly unsupported by the authorization server. Even in such cases, additional countermeasures SHOULD still be employed. + +7.11. Injection and Input Validation +An injection attack occurs when an input or otherwise external variable is used by an application unsanitized and causes modification to the application logic. This may allow an attacker to gain access to the application device or its data, cause denial of service, or introduce a wide range of malicious side-effects. + +The authorization server and client MUST treat parameters received as potentially malicious external input and apply appropriate protections, in particular, the values of the state and redirect_uri parameters. + +7.12. Open Redirection +An open redirector is an endpoint that forwards a user's browser to an arbitrary URI obtained from a query parameter. Such endpoints are sometimes implemented, for example, to show a message before a user is then redirected to an external website, or to redirect users back to a URL they were intending to visit before being interrupted, e.g., by a login prompt. + +The following attacks can occur when an AS or client has an open redirector. + +7.12.1. Client as Open Redirector +Clients MUST NOT expose open redirectors. Attackers may use open redirectors to produce URLs pointing to the client and utilize them to exfiltrate authorization codes, as described in Section 4.1.1 of [RFC9700]. Another abuse case is to produce URLs that appear to point to the client. This might trick users into trusting the URL and follow it in their browser. This can be abused for phishing. + +In order to prevent open redirection, clients should only redirect if the target URLs are whitelisted or if the origin and integrity of a request can be authenticated. Countermeasures against open redirection are described by OWASP [owasp_redir]. + +7.12.2. Authorization Server as Open Redirector +Just as with clients, attackers could try to utilize a user's trust in the authorization server (and its URL in particular) for performing phishing attacks. OAuth authorization servers regularly redirect users to other web sites (the clients), but must do so safely. + +Section 4.1.2.1 already prevents open redirects by stating that the authorization server MUST NOT automatically redirect the user agent in case of an invalid combination of client_id and redirect_uri. + +However, an attacker could also utilize a correctly registered redirect URI to perform phishing attacks. The attacker could, for example, register a client via dynamic client registration [RFC7591] and execute one of the following attacks: + +Intentionally send an erroneous authorization request, e.g., by using an invalid scope value, thus instructing the authorization server to redirect the user agent to its phishing site. + +Intentionally send a valid authorization request with client_id and redirect_uri controlled by the attacker. After the user authenticates, the authorization server prompts the user to provide consent to the request. If the user notices an issue with the request and declines the request, the authorization server still redirects the user agent to the phishing site. In this case, the user agent will be redirected to the phishing site regardless of the action taken by the user. + +Intentionally send a valid silent authentication request (prompt=none) with client_id and redirect_uri controlled by the attacker. In this case, the authorization server will automatically redirect the user agent to the phishing site. + +The authorization server MUST take precautions to prevent these threats. The authorization server MUST always authenticate the user first and, with the exception of the silent authentication use case, prompt the user for credentials when needed, before redirecting the user. Based on its risk assessment, the authorization server needs to decide whether it can trust the redirect URI or not. It could take into account URI analytics done internally or through some external service to evaluate the credibility and trustworthiness content behind the URI, and the source of the redirect URI and other client data. + +The authorization server SHOULD only automatically redirect the user agent if it trusts the redirect URI. If the URI is not trusted, the authorization server MAY inform the user and rely on the user to make the correct decision. + +7.13. Transport Security +In some deployments, including those utilizing load balancers, the TLS connection to the resource server terminates prior to the actual server that provides the resource. This could leave the token unprotected between the front-end server where the TLS connection terminates and the back-end server that provides the resource. In such deployments, sufficient measures MUST be employed to ensure confidentiality of the access token between the front-end and back- end servers; encryption of the token is one such possible measure. + +See Section 17.2 of [RFC9110] for further information. + +7.14. Authorization Server Mix-Up Mitigation +Mix-up is an attack on scenarios where an OAuth client interacts with two or more authorization servers and at least one authorization server is under the control of the attacker. This can be the case, for example, if the attacker uses dynamic registration to register the client at his own authorization server or if an authorization server becomes compromised. + +When an OAuth client can only interact with one authorization server, a mix-up defense is not required. In scenarios where an OAuth client interacts with two or more authorization servers, however, clients MUST prevent mix-up attacks. Two different methods are discussed in the following. + +For both defenses, clients MUST store, for each authorization request, the issuer they sent the authorization request to, bind this information to the user agent, and check that the authorization response was received from the correct issuer. Clients MUST ensure that the subsequent access token request, if applicable, is sent to the same issuer. The issuer serves, via the associated metadata, as an abstract identifier for the combination of the authorization endpoint and token endpoint that are to be used in the flow. If an issuer identifier is not available, for example, if neither OAuth 2.0 Authorization Server Metadata [RFC8414] nor OpenID Connect Discovery [OpenID.Discovery] are used, a different unique identifier for this tuple or the tuple itself can be used instead. For brevity of presentation, such a deployment-specific identifier will be subsumed under the issuer (or issuer identifier) in the following. + +Note: Just storing the authorization server URL is not sufficient to identify mix-up attacks. An attacker might declare an uncompromised AS's authorization endpoint URL as "their" AS URL, but declare a token endpoint under their own control. + +See Section 4.4 of [RFC9700] for a detailed description of several types of mix-up attacks. + +7.14.1. Mix-Up Defense via Issuer Identification +This defense requires that the authorization server sends his issuer identifier in the authorization response to the client. When receiving the authorization response, the client MUST compare the received issuer identifier to the stored issuer identifier. If there is a mismatch, the client MUST abort the interaction. + +There are different ways this issuer identifier can be transported to the client: + +The issuer information can be transported, for example, via an optional response parameter iss (see Section 4.1.2). + +When OpenID Connect is used and an ID Token is returned in the authorization response, the client can evaluate the iss claim in the ID Token. + +In both cases, the iss value MUST be evaluated according to [RFC9207]. + +While this defense may require using an additional parameter to transport the issuer information, it is a robust and relatively simple defense against mix-up. + +7.14.2. Mix-Up Defense via Distinct Redirect URIs +For this defense, clients MUST use a distinct redirect URI for each issuer they interact with. + +Clients MUST check that the authorization response was received from the correct issuer by comparing the distinct redirect URI for the issuer to the URI where the authorization response was received on. If there is a mismatch, the client MUST abort the flow. + +While this defense builds upon existing OAuth functionality, it cannot be used in scenarios where clients only register once for the use of many different issuers (as in some open banking schemes) and due to the tight integration with the client registration, it is harder to deploy automatically. + +Furthermore, an attacker might be able to circumvent the protection offered by this defense by registering a new client with the "honest" AS using the redirect URI that the client assigned to the attacker's AS. The attacker could then run the attack as described above, replacing the client ID with the client ID of his newly created client. + +This defense SHOULD therefore only be used if other options are not available. + +8. Native Applications + Native applications are clients installed and executed on the device used by the resource owner (i.e., desktop applications or native mobile applications). Native applications require special consideration related to security, platform capabilities, and overall end-user experience. + +The guidance in this section is primarily in the context of native mobile apps as opposed to desktop apps. The native mobile platforms have matured more than the desktop platforms in terms of the capabilities provided to app developers relevant to the OAuth flows described here. While the guidance is primarily focused on mobile apps, much of it generally can apply to desktop apps as well. + +The authorization endpoint requires interaction between the client and the resource owner's user agent. The best current practice is to perform the OAuth authorization request in an external user agent (typically the browser) rather than an embedded user agent (such as one implemented with web-views). + +The native application can capture the response from the authorization server in several different ways with differing security properties of each. For example, using a redirect URI with an "app-claimed URL" or custom URL scheme registered with the operating system to invoke the client as the handler, manual copy-and-paste of the credentials, running a local web server, installing a user agent extension, or by providing a redirect URI identifying a server-hosted resource under the client's control, which in turn makes the response available to the native application. + +Previously, it was common for native apps to use embedded user agents (commonly implemented with web-views) for OAuth authorization requests. That approach has many drawbacks, including the host app being able to copy user credentials and cookies as well as the user needing to authenticate from scratch in each app. See Section 8.5.1 for a deeper analysis of the drawbacks of using embedded user agents for OAuth. + +Native app authorization requests that use the system browser are more secure and can take advantage of the user's authentication state on the device. Being able to use the existing authentication session in the browser enables single sign-on, as users don't need to authenticate to the authorization server each time they use a new app (unless required by the authorization server policy). + +Supporting authorization flows between a native app and the browser is possible without changing the OAuth protocol itself, as the OAuth authorization request and response are already defined in terms of URIs. This encompasses URIs that can be used for inter-app communication. Some OAuth server implementations that assume all clients are confidential web clients will need to add an understanding of public native app clients and the types of redirect URIs they use to support this best practice. + +8.1. Client Authentication of Native Apps +Secrets that are statically included as part of an app distributed to multiple users are not confidential secrets, as one user may inspect their copy and learn the shared secret. For this reason, authorization servers MUST NOT require client authentication of native app clients using a shared secret, as this serves no value beyond client identification which is already provided by the client_id request parameter. + +Authorization servers that still require a statically included shared secret for native app clients MUST treat the client as a public client (as defined in Section 2.1), and not accept the secret as proof of the client's identity. Without additional measures, such clients are subject to client impersonation (see Section 7.3.1). + +8.1.1. Registration of Native App Clients +Except when using a mechanism like Dynamic Client Registration [RFC7591] to provision per-instance credentials, native apps are classified as public clients, as defined in Section 2.1, and MUST be registered with the authorization server as such. Authorization servers MUST record the client type in the client registration details in order to identify and process requests accordingly. + +8.1.2. Native App Attestation +The draft specification [I-D.ietf-oauth-attestation-based-client-auth] defines a mechanism that can be used by a native app to obtain a key-bound attestation to authenticate to an authorization server or resource server. This can provide a higher level of assurance of a mobile app's identity. + +8.2. Using Inter-App URI Communication for OAuth in Native Apps +Just as URIs are used for OAuth on the web to initiate the authorization request and return the authorization response to the requesting website, URIs can be used by native apps to initiate the authorization request in the device's browser and return the response to the requesting native app. + +By adopting the same methods used on the web for OAuth, benefits seen in the web context like the usability of a single sign-on session and the security of a separate authentication context are likewise gained in the native app context. Reusing the same approach also reduces the implementation complexity and increases interoperability by relying on standards-based web flows that are not specific to a particular platform. + +Native apps MUST use an external user agent to perform OAuth authorization requests. This is achieved by opening the authorization request in the browser (detailed in Section 8.3) and using a redirect URI that will return the authorization response back to the native app (defined in Section 8.4). + +8.3. Initiating the Authorization Request from a Native App +Native apps needing user authorization create an authorization request URI with the authorization code grant type per Section 4.1 using a redirect URI capable of being received by the native app. + +The function of the redirect URI for a native app authorization request is similar to that of a web-based authorization request. Rather than returning the authorization response to the OAuth client's server, the redirect URI used by a native app returns the response to the app. Several options for a redirect URI that will return the authorization response to the native app in different platforms are documented in Section 8.4. Any redirect URI that allows the app to receive the URI and inspect its parameters is viable. + +After constructing the authorization request URI, the app uses platform-specific APIs to open the URI in an external user agent. Typically, the external user agent used is the default browser, that is, the application configured for handling http and https scheme URIs on the system; however, different browser selection criteria and other categories of external user agents MAY be used. + +This best practice focuses on the browser as the RECOMMENDED external user agent for native apps. An external user agent designed specifically for user authorization and capable of processing authorization requests and responses like a browser MAY also be used. Other external user agents, such as a native app provided by the authorization server may meet the criteria set out in this best practice, including using the same redirect URI properties, but their use is out of scope for this specification. + +Some platforms support a browser feature known as "in-app browser tabs", where an app can present a tab of the browser within the app context without switching apps, but still retain key benefits of the browser such as a shared authentication state and security context. On platforms where they are supported, it is RECOMMENDED, for usability reasons, that apps use in-app browser tabs for the authorization request. + +8.4. Receiving the Authorization Response in a Native App +There are several redirect URI options available to native apps for receiving the authorization response from the browser, the availability and user experience of which varies by platform. + +8.4.1. Claimed "https" Scheme URI Redirection +Some operating systems, in particular mobile operating systems, allow apps to claim https URIs (see Section 4.2.2 of [RFC9110]) in the domains they control. When the browser encounters a claimed URI, instead of the page being loaded in the browser, the native app is launched with the URI supplied as a launch parameter. + +Such URIs can be used as redirect URIs by native apps. They are indistinguishable to the authorization server from a regular web- based client redirect URI. An example is: + +https://app.example.com/oauth2redirect/example-provider +As the redirect URI alone is not enough to distinguish public native app clients from confidential web clients, it is REQUIRED in Section 8.1.1 that the client type be recorded during client registration to enable the server to determine the client type and act accordingly. + +App-claimed https scheme redirect URIs have some advantages compared to other native app redirect options in that the identity of the destination app is guaranteed to the authorization server by the operating system. For this reason, native apps SHOULD use them over the other options where possible. + +8.4.2. Loopback Interface Redirection +Native apps that are able to open a port on the loopback network interface without needing special permissions (typically, those on desktop operating systems) can use the loopback interface to receive the OAuth redirect. + +Loopback redirect URIs use the http scheme and are constructed with the loopback IP literal and whatever port the client is listening on. + +That is, http://127.0.0.1:{port}/{path} for IPv4, and http://[::1]:{port}/{path} for IPv6. An example redirect using the IPv4 loopback interface with a randomly assigned port: + +http://127.0.0.1:51004/oauth2redirect/example-provider +An example redirect using the IPv6 loopback interface with a randomly assigned port: + +http://[::1]:61023/oauth2redirect/example-provider +While redirect URIs using the name localhost (i.e., http://localhost:{port}/{path}) function similarly to loopback IP redirects, the use of localhost is NOT RECOMMENDED. Specifying a redirect URI with the loopback IP literal rather than localhost avoids inadvertently listening on network interfaces other than the loopback interface. It is also less susceptible to client-side firewalls and misconfigured host name resolution on the user's device. + +The authorization server MUST allow any port to be specified at the time of the request for loopback IP redirect URIs, to accommodate clients that obtain an available ephemeral port from the operating system at the time of the request. + +Clients SHOULD NOT assume that the device supports a particular version of the Internet Protocol. It is RECOMMENDED that clients attempt to bind to the loopback interface using both IPv4 and IPv6 and use whichever is available. + +8.4.3. Private-Use URI Scheme Redirection +Many mobile and desktop computing platforms support inter-app communication via URIs by allowing apps to register private-use URI schemes (sometimes colloquially referred to as "custom URL schemes") like com.example.app. When the browser or another app attempts to load a URI with a private-use URI scheme, the app that registered it is launched to handle the request. + +Many environments that support private-use URI schemes do not provide a mechanism to claim a scheme and prevent other parties from using another application's scheme. As such, clients using private-use URI schemes are vulnerable to potential attacks on their redirect URIs, so this option should only be used if the previously mentioned more secure options are not available. + +To perform an authorization request with a private-use URI scheme redirect, the native app launches the browser with a standard authorization request, but one where the redirect URI utilizes a private-use URI scheme it registered with the operating system. + +When choosing a URI scheme to associate with the app, apps MUST use a URI scheme based on a domain name under their control, expressed in reverse order, as recommended by Section 3.8 of [RFC7595] for private-use URI schemes. + +For example, an app that controls the domain name app.example.com can use com.example.app as their scheme. Some authorization servers assign client identifiers based on domain names, for example, client1234.usercontent.example.net, which can also be used as the domain name for the scheme when reversed in the same manner. A scheme such as myapp, however, would not meet this requirement, as it is not based on a domain name. + +When there are multiple apps by the same publisher, care must be taken so that each scheme is unique within that group. On platforms that use app identifiers based on reverse-order domain names, those identifiers can be reused as the private-use URI scheme for the OAuth redirect to help avoid this problem. + +Following the requirements of Section 3.2 of [RFC3986], as there is no naming authority for private-use URI scheme redirects, only a single slash (/) appears after the scheme component. A complete example of a redirect URI utilizing a private-use URI scheme is: + +com.example.app:/oauth2redirect/example-provider +When the authorization server completes the request, it redirects to the client's redirect URI as it would normally. As the redirect URI uses a private-use URI scheme, it results in the operating system launching the native app, passing in the URI as a launch parameter. Then, the native app uses normal processing for the authorization response. + +8.5. Security Considerations in Native Apps +8.5.1. Embedded User Agents in Native Apps +Embedded user agents are a technically possible method for authorizing native apps. These embedded user agents are unsafe for use by third parties to the authorization server by definition, as the app that hosts the embedded user agent can access the user's full authentication credentials, not just the OAuth authorization grant that was intended for the app. They are also typically sandboxed by the operating system and mechanisms such as WebAuthn that rely on the web origin are disabled. + +In typical web-view-based implementations of embedded user agents, the host application can record every keystroke entered in the login form to capture usernames and passwords, automatically submit forms to bypass user consent, and copy session cookies and use them to perform authenticated actions as the user. + +Even when used by trusted apps belonging to the same party as the authorization server, embedded user agents violate the principle of least privilege by having access to more powerful credentials than they need, potentially increasing the attack surface. + +Encouraging users to enter credentials in an embedded user agent without the usual address bar and visible certificate validation features that browsers have makes it impossible for the user to know if they are signing in to the legitimate site; even when they are, it trains them that it's OK to enter credentials without validating the site first. + +Aside from the security concerns, embedded user agents do not share the authentication state with other apps or the browser, requiring the user to log in for every authorization request, which is often considered an inferior user experience. + +8.5.2. Fake External User-Agents in Native Apps +The native app that is initiating the authorization request has a large degree of control over the user interface and can potentially present a fake external user agent, that is, an embedded user agent made to appear as an external user agent. + +When all good actors are using external user agents, the advantage is that it is possible for security experts to detect bad actors, as anyone faking an external user agent is provably bad. On the other hand, if good and bad actors alike are using embedded user agents, bad actors don't need to fake anything, making them harder to detect. Once a malicious app is detected, it may be possible to use this knowledge to blacklist the app's signature in malware scanning software, take removal action (in the case of apps distributed by app stores) and other steps to reduce the impact and spread of the malicious app. + +Authorization servers can also directly protect against fake external user agents by requiring an authentication factor only available to true external user agents. + +Users who are particularly concerned about their security when using in-app browser tabs may also take the additional step of opening the request in the full browser from the in-app browser tab and complete the authorization there, as most implementations of the in-app browser tab pattern offer such functionality. + +8.5.3. Malicious External User-Agents in Native Apps +If a malicious app is able to configure itself as the default handler for https scheme URIs in the operating system, it will be able to intercept authorization requests that use the default browser and abuse this position of trust for malicious ends such as phishing the user. + +This attack is not confined to OAuth; a malicious app configured in this way would present a general and ongoing risk to the user beyond OAuth usage by native apps. Many operating systems mitigate this issue by requiring an explicit user action to change the default handler for http and https scheme URIs. + +8.5.4. Loopback Redirect Considerations in Native Apps +Loopback interface redirect URIs MAY use the http scheme (i.e., without TLS). This is acceptable for loopback interface redirect URIs as the HTTP request never leaves the device. + +Clients SHOULD open the network port only when starting the authorization request and close it once the response is returned. + +Clients SHOULD listen on the loopback network interface only, in order to avoid interference by other network actors. + +Clients SHOULD use loopback IP literals rather than the string localhost as described in Section 8.4.2. + +9. Browser-Based Apps + Browser-based apps are clients that run in a web browser, typically written in JavaScript, also known as "single-page apps". These types of apps have particular security considerations similar to native apps. + +TODO: Bring in the normative text of the browser-based apps BCP when it is finalized. + +10. Differences from OAuth 2.0 + This draft consolidates the functionality in OAuth 2.0 [RFC6749], OAuth 2.0 for Native Apps [RFC8252], Proof Key for Code Exchange [RFC7636], OAuth 2.0 for Browser-Based Apps [I-D.ietf-oauth-browser-based-apps], OAuth Security Best Current Practice [RFC9700], and Bearer Token Usage [RFC6750]. + +Where a later draft updates or obsoletes functionality found in the original [RFC6749], that functionality in this draft is updated with the normative changes described in a later draft, or removed entirely. + +A non-normative list of changes from OAuth 2.0 is listed below: + +The authorization code grant is extended with the functionality from PKCE [RFC7636] such that the default method of using the authorization code grant according to this specification requires the addition of the PKCE parameters + +Redirect URIs must be compared using exact string matching as per Section 4.1.3 of [RFC9700] + +The Implicit grant (response_type=token) is omitted from this specification as per Section 2.1.2 of [RFC9700] + +The Resource Owner Password Credentials grant is omitted from this specification as per Section 2.4 of [RFC9700] + +Bearer token usage omits the use of bearer tokens in the query string of URIs as per Section 4.3.2 of [RFC9700] + +Refresh tokens for public clients must either be sender-constrained or one-time use as per Section 4.14.2 of [RFC9700] + +The token endpoint request containing an authorization code no longer contains the redirect_uri parameter + +Authorization servers must support client credentials in the request body + +10.1. Removal of the OAuth 2.0 Implicit grant +The OAuth 2.0 Implicit grant is omitted from OAuth 2.1 as it was deprecated in [RFC9700]. + +The intent of removing the Implicit grant is to no longer issue access tokens in the authorization response, as such tokens are vulnerable to leakage and injection, and are unable to be sender-constrained to a client. This behavior was indicated by clients using the response_type=token parameter. This value for the response_type parameter is no longer defined in OAuth 2.1. + +Removal of response_type=token does not have an effect on other extension response types returning other artifacts from the authorization endpoint, for example, response_type=id_token defined by [OpenID.Connect]. + +10.2. Redirect URI Parameter in Token Request +In OAuth 2.0, the request to the token endpoint in the authorization code flow (Section 4.1.3 of [RFC6749]) contains an optional redirect_uri parameter. The parameter was intended to prevent an authorization code injection attack, and was required if the redirect_uri parameter was sent in the original authorization request. The authorization request only required the redirect_uri parameter if multiple redirect URIs were registered to the specific client. However, in practice, many authorization server implementations required the redirect_uri parameter in the authorization request even if only one was registered, leading the redirect_uri parameter to be required at the token endpoint as well. + +In OAuth 2.1, authorization code injection is prevented by the code_challenge and code_verifier parameters, making the inclusion of the redirect_uri parameter serve no purpose in the token request. As such, it has been removed. + +For backwards compatibility of an authorization server wishing to support both OAuth 2.0 and OAuth 2.1 clients, the authorization server MUST allow clients to send the redirect_uri parameter in the token request (Section 4.1.3), and MUST enforce the parameter as described in [RFC6749]. The authorization server can use the client_id in the request to determine whether to enforce this behavior for the specific client that it knows will be using the older OAuth 2.0 behavior. + +A client following only the OAuth 2.1 recommendations will not send the redirect_uri in the token request, and therefore will not be compatible with an authorization server that expects the parameter in the token request. + +11. IANA Considerations + This document does not require any IANA actions. + +All referenced registries are defined by [RFC6749] and related documents that this work is based upon. No changes to those registries are required by this specification. + +12. References + 12.1. Normative References + [BCP195] + Saint-Andre, P., "Recommendations for Secure Use of Transport Layer Security (TLS)", 2015. + [RFC2119] + Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, . + [RFC2617] + Franks, J., Hallam-Baker, P., Hostetler, J., Lawrence, S., Leach, P., Luotonen, A., and L. Stewart, "HTTP Authentication: Basic and Digest Access Authentication", RFC 2617, DOI 10.17487/RFC2617, June 1999, . + [RFC3629] + Yergeau, F., "UTF-8, a transformation format of ISO 10646", STD 63, RFC 3629, DOI 10.17487/RFC3629, November 2003, . + [RFC3986] + Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform Resource Identifier (URI): Generic Syntax", STD 66, RFC 3986, DOI 10.17487/RFC3986, January 2005, . + [RFC4949] + Shirey, R., "Internet Security Glossary, Version 2", FYI 36, RFC 4949, DOI 10.17487/RFC4949, August 2007, . + [RFC5234] + Crocker, D., Ed. and P. Overell, "Augmented BNF for Syntax Specifications: ABNF", STD 68, RFC 5234, DOI 10.17487/RFC5234, January 2008, . + [RFC6749] + Hardt, D., Ed., "The OAuth 2.0 Authorization Framework", RFC 6749, DOI 10.17487/RFC6749, October 2012, . + [RFC6750] + Jones, M. and D. Hardt, "The OAuth 2.0 Authorization Framework: Bearer Token Usage", RFC 6750, DOI 10.17487/RFC6750, October 2012, . + [RFC7235] + Fielding, R., Ed. and J. Reschke, Ed., "Hypertext Transfer Protocol (HTTP/1.1): Authentication", RFC 7235, DOI 10.17487/RFC7235, June 2014, . + [RFC7521] + Campbell, B., Mortimore, C., Jones, M., and Y. Goland, "Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants", RFC 7521, DOI 10.17487/RFC7521, May 2015, . + [RFC7523] + Jones, M., Campbell, B., and C. Mortimore, "JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants", RFC 7523, DOI 10.17487/RFC7523, May 2015, . + [RFC7595] + Thaler, D., Ed., Hansen, T., and T. Hardie, "Guidelines and Registration Procedures for URI Schemes", BCP 35, RFC 7595, DOI 10.17487/RFC7595, June 2015, . + [RFC8174] + Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, . + [RFC8252] + Denniss, W. and J. Bradley, "OAuth 2.0 for Native Apps", BCP 212, RFC 8252, DOI 10.17487/RFC8252, October 2017, . + [RFC8259] + Bray, T., Ed., "The JavaScript Object Notation (JSON) Data Interchange Format", STD 90, RFC 8259, DOI 10.17487/RFC8259, December 2017, . + [RFC8446] + Rescorla, E., "The Transport Layer Security (TLS) Protocol Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018, . + [RFC9110] + Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP Semantics", STD 97, RFC 9110, DOI 10.17487/RFC9110, June 2022, . + [RFC9111] + Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP Caching", STD 98, RFC 9111, DOI 10.17487/RFC9111, June 2022, . + [RFC9207] + Meyer zu Selhausen, K. and D. Fett, "OAuth 2.0 Authorization Server Issuer Identification", RFC 9207, DOI 10.17487/RFC9207, March 2022, . + [RFC9700] + Lodderstedt, T., Bradley, J., Labunets, A., and D. Fett, "Best Current Practice for OAuth 2.0 Security", BCP 240, RFC 9700, DOI 10.17487/RFC9700, January 2025, . + [USASCII] + Institute, A. N. S., "Coded Character Set -- 7-bit American Standard Code for Information Interchange, ANSI X3.4", 1986. + [W3C.REC-xml-20081126] + Bray, T., Paoli, J., Sperberg-McQueen, C. M., Maler, E., and F. Yergeau, "Extensible Markup Language", November 2008, . + [WHATWG.CORS] + WHATWG, "Fetch Standard: CORS protocol", June 2023, . + [WHATWG.URL] + WHATWG, "URL", May 2022, . + 12.2. Informative References + [CSP-2] + "Content Security Policy Level 2", December 2016, . + [I-D.bradley-oauth-jwt-encoded-state] + Bradley, J., Lodderstedt, T., and H. Zandbelt, "Encoding claims in the OAuth 2 state parameter using a JWT", Work in Progress, Internet-Draft, draft-bradley-oauth-jwt-encoded-state-09, 4 November 2018, . + [I-D.ietf-oauth-attestation-based-client-auth] + Looker, T., Bastian, P., and C. Bormann, "OAuth 2.0 Attestation-Based Client Authentication", Work in Progress, Internet-Draft, draft-ietf-oauth-attestation-based-client-auth-07, 15 September 2025, . + [I-D.ietf-oauth-browser-based-apps] + Parecki, A., De Ryck, P., and D. Waite, "OAuth 2.0 for Browser-Based Applications", Work in Progress, Internet-Draft, draft-ietf-oauth-browser-based-apps-26, 3 December 2025, . + [I-D.ietf-oauth-rfc7523bis] + Jones, M. B., Campbell, B., Mortimore, C., and F. Skokan, "Updates to OAuth 2.0 JSON Web Token (JWT) Client Authentication and Assertion-Based Authorization Grants", Work in Progress, Internet-Draft, draft-ietf-oauth-rfc7523bis-05, 12 January 2026, . + [NIST800-63] + Burr, W., Dodson, D., Newton, E., Perlner, R., Polk, T., Gupta, S., and E. Nabbus, "NIST Special Publication 800-63-1, INFORMATION SECURITY", December 2011, . + [OMAP] + Huff, J., Schlacht, D., Nadalin, A., Simmons, J., Rosenberg, P., Madsen, P., Ace, T., Rickelton-Abdi, C., and B. Boyer, "Online Multimedia Authorization Protocol: An Industry Standard for Authorized Access to Internet Multimedia Resources", August 2012, . + [OpenID.Connect] + Sakimura, N., Bradley, J., Jones, M., de Medeiros, B., and C. Mortimore, "OpenID Connect Core 1.0 incorporating errata set 2", December 2023, . + [OpenID.Discovery] + Sakimura, N., Bradley, J., Jones, M., and E. Jay, "OpenID Connect Discovery 1.0 incorporating errata set 2", December 2023, . + [OpenID.Messages] + Sakimura, N., Bradley, J., Jones, M., de Medeiros, B., Mortimore, C., and E. Jay, "OpenID Connect Messages 1.0", June 2012, . + [owasp_redir] + "OWASP Cheat Sheet Series - Unvalidated Redirects and Forwards", 2020, . + [RFC6265] + Barth, A., "HTTP State Management Mechanism", RFC 6265, DOI 10.17487/RFC6265, April 2011, . + [RFC6819] + Lodderstedt, T., Ed., McGloin, M., and P. Hunt, "OAuth 2.0 Threat Model and Security Considerations", RFC 6819, DOI 10.17487/RFC6819, January 2013, . + [RFC7009] + Lodderstedt, T., Ed., Dronia, S., and M. Scurtescu, "OAuth 2.0 Token Revocation", RFC 7009, DOI 10.17487/RFC7009, August 2013, . + [RFC7519] + Jones, M., Bradley, J., and N. Sakimura, "JSON Web Token (JWT)", RFC 7519, DOI 10.17487/RFC7519, May 2015, . + [RFC7591] + Richer, J., Ed., Jones, M., Bradley, J., Machulak, M., and P. Hunt, "OAuth 2.0 Dynamic Client Registration Protocol", RFC 7591, DOI 10.17487/RFC7591, July 2015, . + [RFC7592] + Richer, J., Ed., Jones, M., Bradley, J., and M. Machulak, "OAuth 2.0 Dynamic Client Registration Management Protocol", RFC 7592, DOI 10.17487/RFC7592, July 2015, . + [RFC7636] + Sakimura, N., Ed., Bradley, J., and N. Agarwal, "Proof Key for Code Exchange by OAuth Public Clients", RFC 7636, DOI 10.17487/RFC7636, September 2015, . + [RFC7662] + Richer, J., Ed., "OAuth 2.0 Token Introspection", RFC 7662, DOI 10.17487/RFC7662, October 2015, . + [RFC8414] + Jones, M., Sakimura, N., and J. Bradley, "OAuth 2.0 Authorization Server Metadata", RFC 8414, DOI 10.17487/RFC8414, June 2018, . + [RFC8628] + Denniss, W., Bradley, J., Jones, M., and H. Tschofenig, "OAuth 2.0 Device Authorization Grant", RFC 8628, DOI 10.17487/RFC8628, August 2019, . + [RFC8705] + Campbell, B., Bradley, J., Sakimura, N., and T. Lodderstedt, "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens", RFC 8705, DOI 10.17487/RFC8705, February 2020, . + [RFC8707] + Campbell, B., Bradley, J., and H. Tschofenig, "Resource Indicators for OAuth 2.0", RFC 8707, DOI 10.17487/RFC8707, February 2020, . + [RFC9068] + Bertocci, V., "JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens", RFC 9068, DOI 10.17487/RFC9068, October 2021, . + [RFC9126] + Lodderstedt, T., Campbell, B., Sakimura, N., Tonge, D., and F. Skokan, "OAuth 2.0 Pushed Authorization Requests", RFC 9126, DOI 10.17487/RFC9126, September 2021, . + [RFC9396] + Lodderstedt, T., Richer, J., and B. Campbell, "OAuth 2.0 Rich Authorization Requests", RFC 9396, DOI 10.17487/RFC9396, May 2023, . + [RFC9449] + Fett, D., Campbell, B., Bradley, J., Lodderstedt, T., Jones, M., and D. Waite, "OAuth 2.0 Demonstrating Proof of Possession (DPoP)", RFC 9449, DOI 10.17487/RFC9449, September 2023, . + [RFC9470] + Bertocci, V. and B. Campbell, "OAuth 2.0 Step Up Authentication Challenge Protocol", RFC 9470, DOI 10.17487/RFC9470, September 2023, . + [W3C.REC-html401-19991224] + Hors, A. L., Ed., Raggett, D., Ed., and I. Jacobs, Ed., "HTML 4.01 Specification", W3C REC REC-html401-19991224, W3C REC-html401-19991224, 24 December 1999, . + Appendix A. Augmented Backus-Naur Form (ABNF) Syntax + This section provides Augmented Backus-Naur Form (ABNF) syntax descriptions for the elements defined in this specification using the notation of [RFC5234]. The ABNF below is defined in terms of Unicode code points [W3C.REC-xml-20081126]; these characters are typically encoded in UTF-8. Elements are presented in the order first defined. + +Some of the definitions that follow use the "URI-reference" definition from [RFC3986]. + +Some of the definitions that follow use these common definitions: + +VSCHAR = %x20-7E +NQCHAR = %x21 / %x23-5B / %x5D-7E +NQSCHAR = %x20-21 / %x23-5B / %x5D-7E +A.1. "client_id" Syntax +The client_id element is defined in Section 2.4.1: + +client-id = \*VSCHAR +A.2. "client_secret" Syntax +The client_secret element is defined in Section 2.4.1: + +client-secret = \*VSCHAR +A.3. "response_type" Syntax +The response_type element is defined in Section 4.1.1 and Section 6.4: + +response-type = response-name *( SP response-name ) +response-name = 1*response-char +response-char = "\_" / DIGIT / ALPHA +A.4. "scope" Syntax +The scope element is defined in Section 1.4.1: + +scope = scope-token *( SP scope-token ) +scope-token = 1*NQCHAR +A.5. "state" Syntax +The state element is defined in Section 4.1.1, Section 4.1.2, and Section 4.1.2.1: + +state = 1\*VSCHAR +A.6. "redirect_uri" Syntax +The redirect_uri element is defined in Section 4.1.1, and Section 4.1.3: + +redirect-uri = URI-reference +A.7. "error" Syntax +The error element is defined in Sections Section 4.1.2.1, Section 3.2.4, and Section 5.3: + +error = 1\*NQSCHAR +A.8. "error_description" Syntax +The error_description element is defined in Sections Section 4.1.2.1, Section 3.2.4, and Section 5.3: + +error-description = 1\*NQSCHAR +A.9. "error_uri" Syntax +The error_uri element is defined in Sections Section 4.1.2.1, Section 3.2.4, and Section 5.3: + +error-uri = URI-reference +A.10. "grant_type" Syntax +The grant_type element is defined in Section Section 3.2.2: + +grant-type = grant-name / URI-reference +grant-name = 1\*name-char +name-char = "-" / "." / "\_" / DIGIT / ALPHA +A.11. "code" Syntax +The code element is defined in Section 4.1.3: + +code = 1\*VSCHAR +A.12. "access_token" Syntax +The access_token element is defined in Section 3.2.3: + +access-token = 1\*VSCHAR +A.13. "token_type" Syntax +The token_type element is defined in Section 3.2.3, and Section 6.1: + +token-type = type-name / URI-reference +type-name = 1\*name-char +name-char = "-" / "." / "\_" / DIGIT / ALPHA +A.14. "expires_in" Syntax +The expires_in element is defined in Section 3.2.3: + +expires-in = 1\*DIGIT +A.15. "refresh_token" Syntax +The refresh_token element is defined in Section 3.2.3 and Section 4.3: + +refresh-token = 1\*VSCHAR +A.16. Endpoint Parameter Syntax +The syntax for new endpoint parameters is defined in Section 6.2: + +param-name = 1\*name-char +name-char = "-" / "." / "\_" / DIGIT / ALPHA +A.17. "code_verifier" Syntax +ABNF for code_verifier is as follows. + +code-verifier = 43\*128unreserved +unreserved = ALPHA / DIGIT / "-" / "." / "\_" / "~" +ALPHA = %x41-5A / %x61-7A +DIGIT = %x30-39 +A.18. "code_challenge" Syntax +ABNF for code_challenge is as follows. + +code-challenge = 43\*128unreserved +unreserved = ALPHA / DIGIT / "-" / "." / "\_" / "~" +ALPHA = %x41-5A / %x61-7A +DIGIT = %x30-39 +Appendix B. Use of application/x-www-form-urlencoded Media Type +At the time of publication of [RFC6749], the application/x-www-form-urlencoded media type was defined in Section 17.13.4 of [W3C.REC-html401-19991224] but not registered in the IANA MIME Media Types registry (http://www.iana.org/assignments/media-types). Furthermore, that definition is incomplete, as it does not consider non-US-ASCII characters. + +To address this shortcoming when generating contents using this media type, names and values MUST be encoded using the UTF-8 character encoding scheme [RFC3629] first; the resulting octet sequence then needs to be further encoded using the escaping rules defined in [W3C.REC-html401-19991224]. + +When parsing data from a content using this media type, the names and values resulting from reversing the name/value encoding consequently need to be treated as octet sequences, to be decoded using the UTF-8 character encoding scheme. + +For example, the value consisting of the six Unicode code points (1) U+0020 (SPACE), (2) U+0025 (PERCENT SIGN), (3) U+0026 (AMPERSAND), (4) U+002B (PLUS SIGN), (5) U+00A3 (POUND SIGN), and (6) U+20AC (EURO SIGN) would be encoded into the octet sequence below (using hexadecimal notation): + +20 25 26 2B C2 A3 E2 82 AC +and then represented in the content as: + ++%25%26%2B%C2%A3%E2%82%AC +Appendix C. Serializations +Various messages in this specification are serialized using one of the methods described below. This section describes the syntax of these serialization methods; other sections describe when they can and must be used. Note that not all methods can be used for all messages. + +C.1. Query String Serialization +In order to serialize the parameters using the Query String Serialization, the Client constructs the string by adding the parameters and values to the query component of a URL using the application/x-www-form-urlencoded format as defined by [WHATWG.URL]. Query String Serialization is typically used in HTTP GET requests. + +C.2. Form-Encoded Serialization +Parameters and their values are Form Serialized by adding the parameter names and values to the entity body of the HTTP request using the application/x-www-form-urlencoded format as defined by Appendix B. Form Serialization is typically used in HTTP POST requests. + +C.3. JSON Serialization +The parameters are serialized into a JSON [RFC8259] object structure by adding each parameter at the highest structure level. Parameter names and string values are represented as JSON strings. Numerical values are represented as JSON numbers. Boolean values are represented as JSON booleans. Omitted parameters and parameters with no value SHOULD be omitted from the object and not represented by a JSON null value, unless otherwise specified. A parameter MAY have a JSON object or a JSON array as its value. The order of parameters does not matter and can vary. + +Appendix D. Extensions +Below is a list of well-established extensions at the time of publication: + +[RFC7009]: Token Revocation + +The Token Revocation extension defines a mechanism for clients to indicate to the authorization server that an access token is no longer needed. + +[RFC7591]: Dynamic Client Registration + +Dynamic Client Registration provides a mechanism for programmatically registering clients with an authorization server. + +[RFC7662]: Token Introspection + +The Token Introspection extension defines a mechanism for resource servers to obtain information about access tokens. + +[RFC8414]: Authorization Server Metadata + +Authorization Server Metadata (also known as OAuth Discovery) defines an endpoint clients can use to look up the information needed to interact with a particular OAuth server, such as the location of the authorization and token endpoints and the supported grant types. + +[RFC8628]: OAuth 2.0 Device Authorization Grant + +The Device Authorization Grant (formerly known as the Device Flow) is an extension that enables devices with no browser or limited input capability to obtain an access token. This is commonly used by smart TV apps, or devices like hardware video encoders that can stream video to a streaming video service. + +[RFC8705]: Mutual TLS + +Mutual TLS describes a mechanism of binding tokens to the clients they were issued to, as well as a client authentication mechanism, via TLS certificate authentication. + +[RFC8707]: Resource Indicators + +Provides a way for the client to explicitly signal to the authorization server where it intends to use the access token it is requesting. + +[RFC9068]: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens + +This specification defines a profile for issuing OAuth access tokens in JSON Web Token (JWT) format. + +[RFC9126]: Pushed Authorization Requests + +The Pushed Authorization Requests extension describes a technique of initiating an OAuth flow from the back channel, providing better security and more flexibility for building complex authorization requests. + +[RFC9207]: Authorization Server Issuer Identification + +The iss parameter in the authorization response indicates the identity of the authorization server to prevent mix-up attacks in the client. + +[RFC9396]: Rich Authorization Requests + +Rich Authorization Requests specifies a new parameter authorization_details that is used to carry fine-grained authorization data in the OAuth authorization request. + +[RFC9449]: Demonstrating Proof of Possession (DPoP) + +DPoP describes a mechanism for sender-constraining OAuth 2.0 tokens via a proof-of-possession mechanism on the application level. + +[RFC9470]: Step-Up Authentication Challenge Protocol + +Step-Up Auth describes a mechanism that resource servers can use to signal to a client that the authentication event associated with the access token of the current request does not meet its authentication requirements. + +Appendix E. Acknowledgements +This specification is the work of the OAuth Working Group, and its starting point was based on the contents of the following specifications: OAuth 2.0 Authorization Framework (RFC 6749), OAuth 2.0 for Native Apps (RFC 8252), OAuth Security Best Current Practice, and OAuth 2.0 for Browser-Based Apps. The editors would like to thank everyone involved in the creation of those specifications upon which this is built. + +The editors would also like to thank the following individuals for their ideas, feedback, corrections, and wording that helped shape this version of the specification: Andrii Deinega, Bob Hamburg, Brian Campbell, Daniel Fett, Deng Chao, Emelia Smith, Falko, Filip Skokan, Joseph Heenan, Justin Richer, Karsten Meyer zu Selhausen, Michael Jones, Michael Peck, Roberto Polli, Tim Würtele and Vittorio Bertocci. + +Discussions around this specification have also occurred at the OAuth Security Workshop in 2021 and 2022. The authors thank the organizers of the workshop (Guido Schmitz, Steinar Noem, and Daniel Fett) for hosting an event that's conducive to collaboration and community input. + +Appendix F. Document History +[[To be removed from the final specification]] + +-15 + +add additional context for JWT client authentication and specifically recommend RFC7523bis + +editorial clarifications and updates + +clarify error responses in authorization endpoint and token endpoint + +synced language from RFC9700 for AS open redirect considerations + +applied RFC6750 erratas 3500 and 6613 + +resolved ambiguity around repeated parameters + +-14 + +Editorial clarifications + +Corrected an instance of "relying party" vs "resource server" + +Add references to client_secret_post and client_secret_basic terms from RFC7591 + +Replaced "sanitize" language with treating as untrusted input + +Clarified that native apps guidance applies primarily to mobile app platforms + +Clarify that there is no requirement that an AS supports public or confidential clients in particular + +-13 + +Updated references to RFC 9700 + +Updated and sorted list of OAuth extensions + +Updated references to link to section numbers + +-12 + +Updated language around client registration to better reflect alternative registration methods such as those in use by OpenID Federation and open ecosystems + +Added DPoP and Step-Up Auth to appendix of extensions + +Updated reference for case insensitivity of auth scheme to HTTP instead of ABNF + +Corrected an instance of "relying party" vs "client" + +Moved client_id requirement to the individual grant types + +Consolidated the descriptions of serialization methods to the appendix + +-11 + +Explicitly mention that Bearer is case insensitive + +Recommend against defining custom scopes that conflict with known scopes + +Change client credentials to be required to be supported in the request body to avoid HTTP Basic authentication encoding interop issues + +-10 + +Clarify that the client id is an opaque string + +Extensions may define additional error codes on a resource request + +Improved formatting for error field definitions + +Moved and expanded "scope" definition to introduction section + +Split access token section into structure and request + +Renamed b64token to token68 for consistency with RFC7235 + +Restored content from old appendix B about application/x-www-form-urlencoded + +Clarified that clients must not parse access tokens + +Expanded text around when redirect_uri parameter is required in the authorization request + +Changed "permissions" to "privileges" in refresh token section for consistency + +Consolidated authorization code flow security considerations + +Clarified authorization code reuse - an authorization code can only obtain an access token once + +-09 + +AS MUST NOT support CORS requests at authorization endpoint + +more detail on asymmetric client authentication + +sync CSRF description from security BCP + +update and move sender-constrained access tokens section + +sync client impersonating resource owner with security BCP + +add reference to authorization request from redirect URI registration section + +sync refresh rotation section from security BCP + +sync redirect URI matching text from security BCP + +updated references to RAR (RFC9396) + +clarifications on URIs + +removed redirect_uri from the token request + +expanded security considerations around code_verifier + +revised introduction section + +-08 + +Updated acknowledgments + +Swap "by a trusted party" with "by an outside party" in client ID definition + +Replaced "verify the identity of the resource owner" with "authenticate" + +Clarified refresh token rotation to match RFC6819 + +Added appendix to hold application/x-www-form-urlencoded examples + +Fixed references to entries in appendix + +Incorporated new "Phishing via AS" section from Security BCP + +Rephrase description of the motivation for client authentication + +Moved "scope" parameter in token request into specific grant types to match OAuth 2.0 + +Updated Clickjacking and Open Redirection description from the latest version of the Security BCP + +Moved normative requirements out of authorization code security considerations section + +Security considerations clarifications, and removed a duplicate section + +-07 + +Removed "third party" from abstract + +Added MFA and passwordless as additional motiviations in introduction + +Mention PAR as one way redirect URI registration can happen + +Added a reference to requiring CORS headers on the token endpoint + +Updated reference to OMAP extension + +Fixed numbering in sequence diagram + +-06 + +Removed "credentialed client" term + +Simplified definition of "confidential" and "public" clients + +Incorporated the iss response parameter referencing RFC9207 + +Added section on access token validation by the RS + +Removed requirement for authorization servers to support all 3 redirect methods for native apps + +Fixes for some references + +Updates HTTP references to RFC 9110 + +Clarifies "authorization grant" term + +Clarifies client credential grant usage + +Clean up authorization code diagram + +Updated reference for application/x-www-form-urlencoded and removed outdated note about it not being in the IANA registry + +-05 + +Added a section about the removal of the implicit flow + +Moved many normative requirements from security considerations into the appropriate inline sections + +Reorganized and consolidated TLS language + +Require TLS on redirect URIs except for localhost/custom URL scheme + +Updated refresh token guidance to match security BCP + +-04 + +Added explicit mention of not sending access tokens in URI query strings + +Clarifications on definition of client types + +Consolidated text around loopback vs localhost + +Editorial clarifications throughout the document + +-03 + +refactoring to collect all the grant types under the same top-level header in section 4 + +Better split normative and security consideration text into the appropriate places, both moving text that was really security considerations out of the main part of the document, as well as pulling normative requirements from the security considerations sections into the appropriate part of the main document + +Incorporated many of the published errata on RFC6749 + +Updated references to various RFCs + +Editorial clarifications throughout the document + +-02 + +-01 + +-00 + +initial revision + +Authors' Addresses +Dick Hardt +Hellō +Email: dick.hardt@gmail.com +Aaron Parecki +Okta +Email: aaron@parecki.com +URI: https://aaronparecki.com +Torsten Lodderstedt +SPRIND +Email: torsten@lodderstedt.net diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..22b7f19 --- /dev/null +++ b/go.mod @@ -0,0 +1,69 @@ +module giter.top/smart + +go 1.25.5 + +require ( + github.com/gin-gonic/gin v1.12.0 + github.com/google/uuid v1.6.0 + github.com/google/wire v0.7.0 + github.com/redis/go-redis/v9 v9.18.0 + github.com/spf13/viper v1.21.0 + golang.org/x/crypto v0.48.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/google/subcommands v1.2.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.41.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a6cfc41 --- /dev/null +++ b/go.sum @@ -0,0 +1,164 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= +github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/internal/auth/handler/login.go b/internal/auth/handler/login.go new file mode 100644 index 0000000..4d62ac9 --- /dev/null +++ b/internal/auth/handler/login.go @@ -0,0 +1,189 @@ +package handler + +import ( + "errors" + "log/slog" + "net/http" + "time" + + "giter.top/smart/internal/auth/oauth2" + "giter.top/smart/internal/auth/session" + "giter.top/smart/internal/iam/entity" + iamrepo "giter.top/smart/internal/iam/repository" + "giter.top/smart/pkg/config" + "giter.top/smart/pkg/utils/codec" + "github.com/gin-gonic/gin" +) + +// LoginHandler JSON 登录:校验密码后签发 OAuth2 授权码(PKCE),并可选下发会话 Cookie(与 /oauth/authorize 兼容)。 +type LoginHandler struct { + cfg *config.Config + users iamrepo.UserRepository + sess *session.Store + oauth *oauth2.Service +} + +// NewLoginHandler 构造。 +func NewLoginHandler(cfg *config.Config, users iamrepo.UserRepository, sess *session.Store, oauth *oauth2.Service) *LoginHandler { + return &LoginHandler{cfg: cfg, users: users, sess: sess, oauth: oauth} +} + +type loginBody struct { + TenantID string `json:"tenant_id"` + UserName string `json:"user_name"` + Password string `json:"password"` + ClientID string `json:"client_id"` + RedirectURI string `json:"redirect_uri"` + CodeChallenge string `json:"code_challenge"` + CodeChallengeMethod string `json:"code_challenge_method"` + State string `json:"state"` + Scope string `json:"scope"` +} + +type apiEnvelope struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data,omitempty"` +} + +// Login POST /api/v1/auth/login +func (h *LoginHandler) Login(c *gin.Context) { + var req loginBody + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusOK, apiEnvelope{Code: 400, Msg: "请求参数无效: " + err.Error(), Data: nil}) + return + } + if req.UserName == "" || req.Password == "" { + c.JSON(http.StatusOK, apiEnvelope{Code: 400, Msg: "缺少 user_name 或 password", Data: nil}) + return + } + if req.ClientID == "" || req.RedirectURI == "" || req.CodeChallenge == "" { + c.JSON(http.StatusOK, apiEnvelope{Code: 400, Msg: "缺少 client_id、redirect_uri 或 code_challenge", Data: nil}) + return + } + if req.CodeChallengeMethod == "" { + c.JSON(http.StatusOK, apiEnvelope{Code: 400, Msg: "缺少 code_challenge_method", Data: nil}) + return + } + + tid := req.TenantID + if tid == "" { + tid = entity.PlatformTenantID + } + + u, err := h.users.GetByUserName(c.Request.Context(), tid, req.UserName) + if err != nil { + slog.Warn("auth_login_failed", "reason", "user_not_found", "tenant_id", tid, "user_name", req.UserName, "client_ip", c.ClientIP()) + c.JSON(http.StatusUnauthorized, apiEnvelope{Code: 401, Msg: "用户名或密码错误", Data: nil}) + return + } + if err := codec.VerifyPassword(req.Password, u.PasswordHash); err != nil { + slog.Warn("auth_login_failed", "reason", "bad_password", "tenant_id", tid, "user_name", req.UserName, "client_ip", c.ClientIP()) + c.JSON(http.StatusUnauthorized, apiEnvelope{Code: 401, Msg: "用户名或密码错误", Data: nil}) + return + } + if u.Status != 1 { + slog.Warn("auth_login_failed", "reason", "user_disabled", "tenant_id", tid, "user_id", u.ID, "client_ip", c.ClientIP()) + c.JSON(http.StatusForbidden, apiEnvelope{Code: 403, Msg: "用户已禁用", Data: nil}) + return + } + + codePlain, err := h.oauth.IssueAuthorizationCodeAfterPasswordAuth( + c.Request.Context(), + req.ClientID, + req.RedirectURI, + u.ID, + u.TenantID, + req.Scope, + req.CodeChallenge, + req.CodeChallengeMethod, + ) + if err != nil { + switch { + case errors.Is(err, oauth2.ErrInvalidClient): + c.JSON(http.StatusOK, apiEnvelope{Code: 400, Msg: "无效的 client_id", Data: nil}) + return + case errors.Is(err, oauth2.ErrInvalidRedirectURI): + c.JSON(http.StatusOK, apiEnvelope{Code: 400, Msg: "redirect_uri 与客户端登记不一致", Data: nil}) + return + case errors.Is(err, oauth2.ErrPKCERequired): + c.JSON(http.StatusOK, apiEnvelope{Code: 400, Msg: "code_challenge 或 code_challenge_method 无效(需 S256)", Data: nil}) + return + default: + slog.Error("auth_login_issue_code", "err", err) + c.JSON(http.StatusInternalServerError, apiEnvelope{Code: 500, Msg: "服务器错误", Data: nil}) + return + } + } + + sid, err := h.sess.Create(c.Request.Context(), u.ID, u.TenantID) + if err != nil { + slog.Error("auth_login_session", "err", err) + c.JSON(http.StatusInternalServerError, apiEnvelope{Code: 500, Msg: "会话创建失败", Data: nil}) + return + } + h.setSessionCookie(c, sid) + + data := gin.H{ + "authorization_code": codePlain, + } + if req.State != "" { + data["state"] = req.State + } + + slog.Info("auth_login_ok", "tenant_id", u.TenantID, "user_id", u.ID, "user_name", req.UserName, "client_ip", c.ClientIP()) + c.JSON(http.StatusOK, apiEnvelope{Code: 200, Msg: "操作成功", Data: data}) +} + +// Logout POST /api/v1/auth/logout +func (h *LoginHandler) Logout(c *gin.Context) { + sid, err := c.Cookie(h.cfg.Auth.Session.CookieName) + if err == nil && sid != "" { + _ = h.sess.Delete(c.Request.Context(), sid) + } + h.clearSessionCookie(c) + c.JSON(http.StatusOK, apiEnvelope{Code: 200, Msg: "操作成功", Data: nil}) +} + +func (h *LoginHandler) setSessionCookie(c *gin.Context, sid string) { + same := sameSite(h.cfg.Auth.Session.SameSite) + ttl := h.cfg.Auth.Session.TTL + if ttl == 0 { + ttl = 168 * time.Hour + } + http.SetCookie(c.Writer, &http.Cookie{ + Name: h.cfg.Auth.Session.CookieName, + Value: sid, + Path: "/", + Domain: h.cfg.Auth.Session.CookieDomain, + MaxAge: int(ttl.Seconds()), + Secure: h.cfg.Auth.Session.CookieSecure, + HttpOnly: true, + SameSite: same, + }) +} + +func (h *LoginHandler) clearSessionCookie(c *gin.Context) { + same := sameSite(h.cfg.Auth.Session.SameSite) + http.SetCookie(c.Writer, &http.Cookie{ + Name: h.cfg.Auth.Session.CookieName, + Value: "", + Path: "/", + Domain: h.cfg.Auth.Session.CookieDomain, + MaxAge: -1, + Secure: h.cfg.Auth.Session.CookieSecure, + HttpOnly: true, + SameSite: same, + }) +} + +func sameSite(s string) http.SameSite { + switch s { + case "strict": + return http.SameSiteStrictMode + case "none": + return http.SameSiteNoneMode + default: + return http.SameSiteLaxMode + } +} diff --git a/internal/auth/http_register.go b/internal/auth/http_register.go new file mode 100644 index 0000000..ef5db75 --- /dev/null +++ b/internal/auth/http_register.go @@ -0,0 +1,38 @@ +package auth + +import ( + "giter.top/smart/internal/auth/handler" + "giter.top/smart/internal/auth/oauth2" + "github.com/gin-gonic/gin" +) + +// AuthRoutes 认证相关 HTTP(OAuth2、登录)。 +type AuthRoutes struct { + bearer gin.HandlerFunc + loginRL gin.HandlerFunc + tokenRL gin.HandlerFunc + oauthH *oauth2.Handler + loginH *handler.LoginHandler +} + +// NewAuthRoutes 构造(loginRL/tokenRL 使用 Wire 专用类型,见 wire_provider.go)。 +func NewAuthRoutes(bearer gin.HandlerFunc, loginRL LoginRateLimitWire, tokenRL TokenRateLimitWire, oauthH *oauth2.Handler, loginH *handler.LoginHandler) *AuthRoutes { + return &AuthRoutes{ + bearer: bearer, + loginRL: gin.HandlerFunc(loginRL), + tokenRL: gin.HandlerFunc(tokenRL), + oauthH: oauthH, + loginH: loginH, + } +} + +// Register 实现 server.HttpRoutes:OAuth 在根路径,/api/v1 挂 Bearer 与登录。 +func (r *AuthRoutes) Register(engine *gin.Engine, apiGroup *gin.RouterGroup) { + apiGroup.Use(r.bearer) + apiGroup.POST("/auth/login", r.loginRL, r.loginH.Login) + apiGroup.POST("/auth/logout", r.loginH.Logout) + + engine.GET("/oauth/authorize", r.oauthH.Authorize) + engine.POST("/oauth/token", r.tokenRL, r.oauthH.Token) + engine.POST("/oauth/introspect", r.tokenRL, r.oauthH.Introspect) +} diff --git a/internal/auth/middleware/bearer.go b/internal/auth/middleware/bearer.go new file mode 100644 index 0000000..1df4c5b --- /dev/null +++ b/internal/auth/middleware/bearer.go @@ -0,0 +1,41 @@ +package middleware + +import ( + "strings" + + "giter.top/smart/internal/auth/oauth2" + "github.com/gin-gonic/gin" +) + +// Context keys for auth principal +const ( + CtxUserID = "auth_user_id" + CtxTenantID = "auth_tenant_id" + CtxScope = "auth_scope" +) + +// NewBearer 解析 opaque Bearer access_token,写入上下文;无 Bearer 或无效时继续放行(兼容未迁移接口)。 +func NewBearer(store *oauth2.Store) gin.HandlerFunc { + return func(c *gin.Context) { + h := c.GetHeader("Authorization") + const prefix = "Bearer " + if !strings.HasPrefix(h, prefix) { + c.Next() + return + } + raw := strings.TrimSpace(strings.TrimPrefix(h, prefix)) + if raw == "" { + c.Next() + return + } + p, err := store.LookupAccessToken(c.Request.Context(), raw) + if err != nil { + c.Next() + return + } + c.Set(CtxUserID, p.UserID) + c.Set(CtxTenantID, p.TenantID) + c.Set(CtxScope, p.Scope) + c.Next() + } +} diff --git a/internal/auth/middleware/ratelimit.go b/internal/auth/middleware/ratelimit.go new file mode 100644 index 0000000..9edb378 --- /dev/null +++ b/internal/auth/middleware/ratelimit.go @@ -0,0 +1,57 @@ +package middleware + +import ( + "net" + "strings" + "sync" + + "github.com/gin-gonic/gin" + "golang.org/x/time/rate" +) + +// PerIPMinute 按客户端 IP 的固定窗口速率(每分钟 perMinute 次,burst 取 perMinute 与 64 的较小值)。 +// 进程内 map 可能随 IP 数增长,多实例部署请在网关侧限流。 +func PerIPMinute(enabled bool, perMinute int) gin.HandlerFunc { + if !enabled || perMinute <= 0 { + return func(c *gin.Context) { c.Next() } + } + burst := perMinute + if burst > 64 { + burst = 64 + } + if burst < 5 { + burst = 5 + } + lim := rate.Limit(float64(perMinute) / 60.0) + var mu sync.Mutex + limiters := make(map[string]*rate.Limiter) + return func(c *gin.Context) { + ip := clientIP(c) + mu.Lock() + limiter, ok := limiters[ip] + if !ok { + limiter = rate.NewLimiter(lim, burst) + limiters[ip] = limiter + } + mu.Unlock() + if !limiter.Allow() { + c.AbortWithStatusJSON(429, gin.H{"error": "rate_limit_exceeded"}) + return + } + c.Next() + } +} + +func clientIP(c *gin.Context) string { + if xff := c.GetHeader("X-Forwarded-For"); xff != "" { + parts := strings.Split(xff, ",") + if len(parts) > 0 { + return strings.TrimSpace(parts[0]) + } + } + host, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)) + if err != nil { + return c.Request.RemoteAddr + } + return host +} diff --git a/internal/auth/oauth2/errors.go b/internal/auth/oauth2/errors.go new file mode 100644 index 0000000..6c0f125 --- /dev/null +++ b/internal/auth/oauth2/errors.go @@ -0,0 +1,10 @@ +package oauth2 + +import "errors" + +// JSON 登录签发授权码时与 Authorize 对齐校验。 +var ( + ErrInvalidClient = errors.New("oauth2: invalid client_id") + ErrInvalidRedirectURI = errors.New("oauth2: invalid redirect_uri") + ErrPKCERequired = errors.New("oauth2: invalid code_challenge or code_challenge_method") +) diff --git a/internal/auth/oauth2/handler.go b/internal/auth/oauth2/handler.go new file mode 100644 index 0000000..ae29bd8 --- /dev/null +++ b/internal/auth/oauth2/handler.go @@ -0,0 +1,28 @@ +package oauth2 + +import "github.com/gin-gonic/gin" + +// Handler 绑定 Gin 与 Service。 +type Handler struct { + svc *Service +} + +// NewHandler 构造。 +func NewHandler(svc *Service) *Handler { + return &Handler{svc: svc} +} + +// Authorize GET /oauth/authorize +func (h *Handler) Authorize(c *gin.Context) { + h.svc.Authorize(c) +} + +// Token POST /oauth/token +func (h *Handler) Token(c *gin.Context) { + h.svc.Token(c) +} + +// Introspect POST /oauth/introspect +func (h *Handler) Introspect(c *gin.Context) { + h.svc.Introspect(c) +} diff --git a/internal/auth/oauth2/model.go b/internal/auth/oauth2/model.go new file mode 100644 index 0000000..af99168 --- /dev/null +++ b/internal/auth/oauth2/model.go @@ -0,0 +1,64 @@ +package oauth2 + +import "time" + +// OAuthClient oauth_client +type OAuthClient struct { + ID string `gorm:"primaryKey;type:varchar(36)"` + ClientID string `gorm:"size:64;not null;uniqueIndex"` + ClientSecretHash *string `gorm:"size:255"` + RedirectURIsJSON string `gorm:"column:redirect_uris;type:text;not null"` + IsPublic bool `gorm:"not null;default:true"` + CreatedAt time.Time `gorm:"not null"` +} + +func (OAuthClient) TableName() string { return "oauth_client" } + +// OAuthAuthorizationCode oauth_authorization_code +type OAuthAuthorizationCode struct { + ID string `gorm:"primaryKey;type:varchar(36)"` + CodeHash string `gorm:"size:64;not null;uniqueIndex"` + ClientID string `gorm:"size:64;not null"` + RedirectURI string `gorm:"type:text;not null"` + UserID string `gorm:"size:36;not null"` + TenantID string `gorm:"size:36;not null"` + Scope string `gorm:"type:text;not null"` + CodeChallenge string `gorm:"size:128;not null"` + CodeChallengeMethod string `gorm:"size:16;not null"` + ExpiresAt time.Time `gorm:"not null"` + Used bool `gorm:"not null;default:false"` + CreatedAt time.Time `gorm:"not null"` +} + +func (OAuthAuthorizationCode) TableName() string { return "oauth_authorization_code" } + +// OAuthAccessToken oauth_access_token +type OAuthAccessToken struct { + ID string `gorm:"primaryKey;type:varchar(36)"` + TokenHash string `gorm:"size:64;not null;uniqueIndex"` + ClientID string `gorm:"size:64;not null"` + UserID string `gorm:"size:36;not null"` + TenantID string `gorm:"size:36;not null"` + Scope string `gorm:"type:text;not null"` + ExpiresAt time.Time `gorm:"not null"` + RevokedAt *time.Time `gorm:""` + CreatedAt time.Time `gorm:"not null"` +} + +func (OAuthAccessToken) TableName() string { return "oauth_access_token" } + +// OAuthRefreshToken oauth_refresh_token +type OAuthRefreshToken struct { + ID string `gorm:"primaryKey;type:varchar(36)"` + TokenHash string `gorm:"size:64;not null;uniqueIndex"` + AccessTokenID string `gorm:"size:36;not null;index"` + ClientID string `gorm:"size:64;not null"` + UserID string `gorm:"size:36;not null"` + TenantID string `gorm:"size:36;not null"` + Scope string `gorm:"type:text;not null"` + ExpiresAt time.Time `gorm:"not null"` + RevokedAt *time.Time `gorm:""` + CreatedAt time.Time `gorm:"not null"` +} + +func (OAuthRefreshToken) TableName() string { return "oauth_refresh_token" } diff --git a/internal/auth/oauth2/pkce.go b/internal/auth/oauth2/pkce.go new file mode 100644 index 0000000..bec13d1 --- /dev/null +++ b/internal/auth/oauth2/pkce.go @@ -0,0 +1,23 @@ +package oauth2 + +import ( + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "strings" +) + +// VerifyPKCES256 校验 code_verifier 是否与 code_challenge(S256)一致。 +func VerifyPKCES256(codeVerifier, codeChallenge string) bool { + if codeVerifier == "" || codeChallenge == "" { + return false + } + sum := sha256.Sum256([]byte(codeVerifier)) + expected := base64.RawURLEncoding.EncodeToString(sum[:]) + return subtle.ConstantTimeCompare([]byte(expected), []byte(codeChallenge)) == 1 +} + +// NormalizeCodeChallengeMethod 返回小写方法名;仅支持 S256(OAuth 2.1 推荐)。 +func NormalizeCodeChallengeMethod(m string) string { + return strings.TrimSpace(strings.ToLower(m)) +} diff --git a/internal/auth/oauth2/service.go b/internal/auth/oauth2/service.go new file mode 100644 index 0000000..af16dd9 --- /dev/null +++ b/internal/auth/oauth2/service.go @@ -0,0 +1,341 @@ +package oauth2 + +import ( + "context" + "errors" + "log/slog" + "net/http" + "net/url" + "strings" + "time" + + "giter.top/smart/internal/auth/session" + "giter.top/smart/pkg/config" + "giter.top/smart/pkg/security" + "github.com/gin-gonic/gin" +) + +// Service OAuth2 授权码 + PKCE + opaque token。 +type Service struct { + cfg *config.Config + store *Store + sess *session.Store +} + +// NewService 构造。 +func NewService(cfg *config.Config, store *Store, sess *session.Store) *Service { + return &Service{cfg: cfg, store: store, sess: sess} +} + +func (s *Service) durations() (authCode, access, refresh time.Duration) { + authCode = s.cfg.Auth.OAuth2.AuthCodeTTL + if authCode == 0 { + authCode = 120 * time.Second + } + access = s.cfg.Auth.OAuth2.AccessTokenTTL + if access == 0 { + access = 15 * time.Minute + } + refresh = s.cfg.Auth.OAuth2.RefreshTokenTTL + if refresh == 0 { + refresh = 720 * time.Hour + } + return authCode, access, refresh +} + +// IssueAuthorizationCodeAfterPasswordAuth 在已通过用户名密码校验的上下文中签发 PKCE 绑定授权码(与 Authorize 中 CreateAuthorizationCode 一致)。 +func (s *Service) IssueAuthorizationCodeAfterPasswordAuth(ctx context.Context, clientID, redirectURI, userID, tenantID, scope, codeChallenge, challengeMethod string) (codePlain string, err error) { + if scope == "" { + scope = "openid" + } + method := NormalizeCodeChallengeMethod(challengeMethod) + if codeChallenge == "" || method != "s256" { + return "", ErrPKCERequired + } + cli, err := s.store.GetClientByClientID(ctx, clientID) + if err != nil { + if errors.Is(err, ErrNotFound) { + return "", ErrInvalidClient + } + return "", err + } + uris, err := ParseRedirectURIs(cli.RedirectURIsJSON) + if err != nil || !RedirectURIMatch(uris, redirectURI) { + return "", ErrInvalidRedirectURI + } + codePlain, err = security.RandomURLSafe(32) + if err != nil { + return "", err + } + codeTTL, _, _ := s.durations() + exp := time.Now().Add(codeTTL) + if err := s.store.CreateAuthorizationCode(ctx, codePlain, clientID, redirectURI, userID, tenantID, scope, codeChallenge, "S256", exp); err != nil { + return "", err + } + return codePlain, nil +} + +func (s *Service) publicAuthorizeURL(c *gin.Context) string { + base := strings.TrimRight(s.cfg.Auth.PublicBaseURL, "/") + if base == "" { + scheme := "http" + if c.Request.TLS != nil { + scheme = "https" + } + if xf := c.GetHeader("X-Forwarded-Proto"); xf == "https" { + scheme = "https" + } + base = scheme + "://" + c.Request.Host + } + return base + "/oauth/authorize?" + c.Request.URL.RawQuery +} + +// Authorize GET /oauth/authorize +func (s *Service) Authorize(c *gin.Context) { + q := c.Request.URL.Query() + if q.Get("response_type") != "code" { + c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported_response_type"}) + return + } + clientID := q.Get("client_id") + redirectURI := q.Get("redirect_uri") + state := q.Get("state") + scope := q.Get("scope") + if scope == "" { + scope = "openid" + } + challenge := q.Get("code_challenge") + method := NormalizeCodeChallengeMethod(q.Get("code_challenge_method")) + if challenge == "" || method != "s256" { + s.redirectOAuthError(c, redirectURI, state, "invalid_request", "code_challenge and code_challenge_method=S256 required") + return + } + + cli, err := s.store.GetClientByClientID(c.Request.Context(), clientID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_client"}) + return + } + uris, err := ParseRedirectURIs(cli.RedirectURIsJSON) + if err != nil || !RedirectURIMatch(uris, redirectURI) { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_redirect_uri"}) + return + } + + sid, err := c.Cookie(s.cfg.Auth.Session.CookieName) + if err != nil || sid == "" { + login := strings.TrimRight(s.cfg.Auth.OAuth2.FrontendLoginURL, "?") + ret := s.publicAuthorizeURL(c) + u, _ := url.Parse(login) + q2 := u.Query() + q2.Set("return_to", ret) + u.RawQuery = q2.Encode() + c.Redirect(http.StatusFound, u.String()) + return + } + userID, tenantID, err := s.sess.Get(c.Request.Context(), sid) + if err != nil { + login := strings.TrimRight(s.cfg.Auth.OAuth2.FrontendLoginURL, "?") + ret := s.publicAuthorizeURL(c) + u, _ := url.Parse(login) + q2 := u.Query() + q2.Set("return_to", ret) + u.RawQuery = q2.Encode() + c.Redirect(http.StatusFound, u.String()) + return + } + + codePlain, err := security.RandomURLSafe(32) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "server_error"}) + return + } + codeTTL, _, _ := s.durations() + exp := time.Now().Add(codeTTL) + if err := s.store.CreateAuthorizationCode(c.Request.Context(), codePlain, clientID, redirectURI, userID, tenantID, scope, challenge, "S256", exp); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "server_error"}) + return + } + redir, err := url.Parse(redirectURI) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_redirect_uri"}) + return + } + rq := redir.Query() + rq.Set("code", codePlain) + if state != "" { + rq.Set("state", state) + } + redir.RawQuery = rq.Encode() + c.Redirect(http.StatusFound, redir.String()) +} + +func (s *Service) redirectOAuthError(c *gin.Context, redirectURI, state, errCode, desc string) { + if redirectURI == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": errCode, "error_description": desc}) + return + } + u, e := url.Parse(redirectURI) + if e != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": errCode}) + return + } + q := u.Query() + q.Set("error", errCode) + q.Set("error_description", desc) + if state != "" { + q.Set("state", state) + } + u.RawQuery = q.Encode() + c.Redirect(http.StatusFound, u.String()) +} + +// Token POST /oauth/token +func (s *Service) Token(c *gin.Context) { + if err := c.Request.ParseForm(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"}) + return + } + gt := c.PostForm("grant_type") + switch gt { + case "authorization_code": + s.tokenAuthorizationCode(c) + case "refresh_token": + s.tokenRefresh(c) + default: + c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported_grant_type"}) + } +} + +func (s *Service) tokenAuthorizationCode(c *gin.Context) { + code := c.PostForm("code") + redirectURI := c.PostForm("redirect_uri") + clientID := c.PostForm("client_id") + verifier := c.PostForm("code_verifier") + if code == "" || redirectURI == "" || clientID == "" || verifier == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"}) + return + } + row, err := s.store.ConsumeAuthorizationCode(c.Request.Context(), code, clientID, redirectURI) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant"}) + return + } + if !VerifyPKCES256(verifier, row.CodeChallenge) { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": "pkce verification failed"}) + return + } + _, accessTTL, refreshTTL := s.durations() + accessPlain, err := security.RandomURLSafe(32) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "server_error"}) + return + } + refreshPlain, err := security.RandomURLSafe(48) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "server_error"}) + return + } + if err := s.store.IssueAccessAndRefresh(c.Request.Context(), accessPlain, refreshPlain, clientID, row.UserID, row.TenantID, row.Scope, accessTTL, refreshTTL); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "server_error"}) + return + } + slog.Info("oauth2_token_issued", "grant_type", "authorization_code", "client_id", clientID, "user_id", row.UserID, "tenant_id", row.TenantID, "client_ip", c.ClientIP()) + s.jsonAccessToken(c, accessPlain, refreshPlain, accessTTL) +} + +func (s *Service) tokenRefresh(c *gin.Context) { + refresh := c.PostForm("refresh_token") + clientID := c.PostForm("client_id") + if refresh == "" || clientID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"}) + return + } + _, accessTTL, refreshTTL := s.durations() + newAccess, err := security.RandomURLSafe(32) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "server_error"}) + return + } + newRefresh, err := security.RandomURLSafe(48) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "server_error"}) + return + } + if err := s.store.RotateByRefreshToken(c.Request.Context(), clientID, refresh, newAccess, newRefresh, accessTTL, refreshTTL); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant"}) + return + } + slog.Info("oauth2_token_issued", "grant_type", "refresh_token", "client_id", clientID, "client_ip", c.ClientIP()) + s.jsonAccessToken(c, newAccess, newRefresh, accessTTL) +} + +func (s *Service) jsonAccessToken(c *gin.Context, access, refresh string, accessTTL time.Duration) { + c.JSON(http.StatusOK, gin.H{ + "access_token": access, + "token_type": "Bearer", + "expires_in": int(accessTTL.Seconds()), + "refresh_token": refresh, + }) +} + +// Introspect POST /oauth/introspect(RFC 7662),与 opaque 查表语义一致。 +func (s *Service) Introspect(c *gin.Context) { + if err := c.Request.ParseForm(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"active": false}) + return + } + tok := c.PostForm("token") + hint := strings.TrimSpace(c.PostForm("token_type_hint")) + if tok == "" { + c.JSON(http.StatusOK, gin.H{"active": false}) + return + } + ctx := c.Request.Context() + + tryRefreshFirst := hint == "refresh_token" + if tryRefreshFirst { + if row, err := s.store.LookupRefreshTokenRow(ctx, tok); err == nil { + slog.Info("oauth2_introspect", "active", true, "token_type", "refresh_token", "client_id", row.ClientID, "sub", row.UserID) + c.JSON(http.StatusOK, gin.H{ + "active": true, + "scope": row.Scope, + "client_id": row.ClientID, + "token_type": "refresh_token", + "sub": row.UserID, + "exp": row.ExpiresAt.Unix(), + }) + return + } + c.JSON(http.StatusOK, gin.H{"active": false}) + return + } + + if row, err := s.store.LookupAccessTokenRow(ctx, tok); err == nil { + slog.Info("oauth2_introspect", "active", true, "token_type", "access_token", "client_id", row.ClientID, "sub", row.UserID) + c.JSON(http.StatusOK, gin.H{ + "active": true, + "scope": row.Scope, + "client_id": row.ClientID, + "token_type": "access_token", + "sub": row.UserID, + "exp": row.ExpiresAt.Unix(), + }) + return + } + + if row, err := s.store.LookupRefreshTokenRow(ctx, tok); err == nil { + slog.Info("oauth2_introspect", "active", true, "token_type", "refresh_token", "client_id", row.ClientID, "sub", row.UserID) + c.JSON(http.StatusOK, gin.H{ + "active": true, + "scope": row.Scope, + "client_id": row.ClientID, + "token_type": "refresh_token", + "sub": row.UserID, + "exp": row.ExpiresAt.Unix(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{"active": false}) +} diff --git a/internal/auth/oauth2/store.go b/internal/auth/oauth2/store.go new file mode 100644 index 0000000..6752e24 --- /dev/null +++ b/internal/auth/oauth2/store.go @@ -0,0 +1,265 @@ +package oauth2 + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "time" + + "giter.top/smart/pkg/utils/id" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +// ErrNotFound 未找到记录。 +var ErrNotFound = errors.New("oauth2: not found") + +func hashToken(raw string) string { + sum := sha256.Sum256([]byte(raw)) + return hex.EncodeToString(sum[:]) +} + +// Store OAuth 持久化。 +type Store struct { + db *gorm.DB +} + +// NewStore 创建 Store。 +func NewStore(db *gorm.DB) *Store { + return &Store{db: db} +} + +// GetClientByClientID 按 client_id 查客户端。 +func (st *Store) GetClientByClientID(ctx context.Context, clientID string) (*OAuthClient, error) { + var row OAuthClient + err := st.db.WithContext(ctx).Where("client_id = ?", clientID).First(&row).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } + return nil, err + } + return &row, nil +} + +// ParseRedirectURIs 解析 redirect_uris JSON 数组。 +func ParseRedirectURIs(raw string) ([]string, error) { + var uris []string + if raw == "" { + return nil, errors.New("empty redirect_uris") + } + if err := json.Unmarshal([]byte(raw), &uris); err != nil { + return nil, err + } + return uris, nil +} + +// RedirectURIMatch OAuth 2.1 精确匹配。 +func RedirectURIMatch(allowed []string, u string) bool { + for _, x := range allowed { + if x == u { + return true + } + } + return false +} + +// CreateAuthorizationCode 写入授权码(code 明文仅返回给调用方,库存哈希)。 +func (st *Store) CreateAuthorizationCode(ctx context.Context, codePlain string, clientID, redirectURI, userID, tenantID, scope, challenge, method string, expiresAt time.Time) error { + row := OAuthAuthorizationCode{ + ID: id.New(), + CodeHash: hashToken(codePlain), + ClientID: clientID, + RedirectURI: redirectURI, + UserID: userID, + TenantID: tenantID, + Scope: scope, + CodeChallenge: challenge, + CodeChallengeMethod: method, + ExpiresAt: expiresAt, + Used: false, + CreatedAt: time.Now(), + } + return st.db.WithContext(ctx).Create(&row).Error +} + +// ConsumeAuthorizationCode 校验并一次性消费授权码,返回行数据供发 token。 +func (st *Store) ConsumeAuthorizationCode(ctx context.Context, codePlain, clientID, redirectURI string) (*OAuthAuthorizationCode, error) { + h := hashToken(codePlain) + var out *OAuthAuthorizationCode + err := st.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var row OAuthAuthorizationCode + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("code_hash = ?", h).First(&row).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrNotFound + } + return err + } + if row.Used { + return ErrNotFound + } + if time.Now().After(row.ExpiresAt) { + return ErrNotFound + } + if row.ClientID != clientID || row.RedirectURI != redirectURI { + return ErrNotFound + } + if err := tx.Model(&OAuthAuthorizationCode{}).Where("id = ?", row.ID).Update("used", true).Error; err != nil { + return err + } + out = &row + return nil + }) + if err != nil { + return nil, err + } + return out, nil +} + +// TokenPrincipal opaque access token 解析结果。 +type TokenPrincipal struct { + UserID string + TenantID string + Scope string +} + +// LookupAccessToken 按明文 access token 查有效记录。 +func (st *Store) LookupAccessToken(ctx context.Context, raw string) (*TokenPrincipal, error) { + h := hashToken(raw) + var row OAuthAccessToken + err := st.db.WithContext(ctx).Where("token_hash = ? AND revoked_at IS NULL", h).First(&row).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } + return nil, err + } + if time.Now().After(row.ExpiresAt) { + return nil, ErrNotFound + } + return &TokenPrincipal{UserID: row.UserID, TenantID: row.TenantID, Scope: row.Scope}, nil +} + +// LookupAccessTokenRow 按明文查 access token 行(自省用)。 +func (st *Store) LookupAccessTokenRow(ctx context.Context, raw string) (*OAuthAccessToken, error) { + h := hashToken(raw) + var row OAuthAccessToken + err := st.db.WithContext(ctx).Where("token_hash = ? AND revoked_at IS NULL", h).First(&row).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } + return nil, err + } + if time.Now().After(row.ExpiresAt) { + return nil, ErrNotFound + } + return &row, nil +} + +// LookupRefreshTokenRow 按明文查 refresh token 行(自省用)。 +func (st *Store) LookupRefreshTokenRow(ctx context.Context, raw string) (*OAuthRefreshToken, error) { + h := hashToken(raw) + var row OAuthRefreshToken + err := st.db.WithContext(ctx).Where("token_hash = ? AND revoked_at IS NULL", h).First(&row).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } + return nil, err + } + if time.Now().After(row.ExpiresAt) { + return nil, ErrNotFound + } + return &row, nil +} + +// IssueAccessAndRefresh 写入 access + refresh(opaque 明文仅调用方返回给客户端)。 +func (st *Store) IssueAccessAndRefresh(ctx context.Context, accessPlain, refreshPlain, clientID, userID, tenantID, scope string, accessTTL, refreshTTL time.Duration) error { + now := time.Now() + accessID := id.New() + refreshID := id.New() + at := OAuthAccessToken{ + ID: accessID, + TokenHash: hashToken(accessPlain), + ClientID: clientID, + UserID: userID, + TenantID: tenantID, + Scope: scope, + ExpiresAt: now.Add(accessTTL), + CreatedAt: now, + } + rt := OAuthRefreshToken{ + ID: refreshID, + TokenHash: hashToken(refreshPlain), + AccessTokenID: accessID, + ClientID: clientID, + UserID: userID, + TenantID: tenantID, + Scope: scope, + ExpiresAt: now.Add(refreshTTL), + CreatedAt: now, + } + return st.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Create(&at).Error; err != nil { + return err + } + return tx.Create(&rt).Error + }) +} + +// RotateByRefreshToken 使用 refresh 换发新 access+refresh,旧令牌作废;client_id 须与注册一致。 +func (st *Store) RotateByRefreshToken(ctx context.Context, clientID, refreshPlain, newAccessPlain, newRefreshPlain string, accessTTL, refreshTTL time.Duration) error { + h := hashToken(refreshPlain) + return st.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var row OAuthRefreshToken + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("token_hash = ? AND revoked_at IS NULL", h).First(&row).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrNotFound + } + return err + } + if row.ClientID != clientID { + return ErrNotFound + } + if time.Now().After(row.ExpiresAt) { + return ErrNotFound + } + now := time.Now() + if err := tx.Model(&OAuthRefreshToken{}).Where("id = ?", row.ID).Update("revoked_at", now).Error; err != nil { + return err + } + if err := tx.Model(&OAuthAccessToken{}).Where("id = ?", row.AccessTokenID).Update("revoked_at", now).Error; err != nil { + return err + } + newAID := id.New() + newRID := id.New() + at := OAuthAccessToken{ + ID: newAID, + TokenHash: hashToken(newAccessPlain), + ClientID: row.ClientID, + UserID: row.UserID, + TenantID: row.TenantID, + Scope: row.Scope, + ExpiresAt: now.Add(accessTTL), + CreatedAt: now, + } + rt := OAuthRefreshToken{ + ID: newRID, + TokenHash: hashToken(newRefreshPlain), + AccessTokenID: newAID, + ClientID: row.ClientID, + UserID: row.UserID, + TenantID: row.TenantID, + Scope: row.Scope, + ExpiresAt: now.Add(refreshTTL), + CreatedAt: now, + } + if err := tx.Create(&at).Error; err != nil { + return err + } + return tx.Create(&rt).Error + }) +} diff --git a/internal/auth/scope/scope.go b/internal/auth/scope/scope.go new file mode 100644 index 0000000..ce0b5ce --- /dev/null +++ b/internal/auth/scope/scope.go @@ -0,0 +1,44 @@ +package scope + +import ( + "strings" +) + +// Split 将空格分隔的 scope 拆成列表。 +func Split(scope string) []string { + if strings.TrimSpace(scope) == "" { + return nil + } + parts := strings.Fields(scope) + out := make([]string, 0, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p != "" { + out = append(out, p) + } + } + return out +} + +// Contains 判断 scope 字符串是否包含指定权限标记。 +func Contains(scope, want string) bool { + if want == "" { + return true + } + for _, s := range Split(scope) { + if s == want { + return true + } + } + return false +} + +// HasAPIAccess 约定含 `api` 或 `api.*` 前缀即表示可访问业务 API(可与 IAM 菜单权限组合使用)。 +func HasAPIAccess(scope string) bool { + for _, s := range Split(scope) { + if s == "api" || strings.HasPrefix(s, "api.") { + return true + } + } + return false +} diff --git a/internal/auth/session/store.go b/internal/auth/session/store.go new file mode 100644 index 0000000..cbc8d15 --- /dev/null +++ b/internal/auth/session/store.go @@ -0,0 +1,76 @@ +package session + +import ( + "context" + "encoding/json" + "errors" + "time" + + "giter.top/smart/pkg/config" + "giter.top/smart/pkg/security" + + "github.com/redis/go-redis/v9" +) + +// ErrInvalidSession 会话不存在或已过期。 +var ErrInvalidSession = errors.New("session: invalid or expired") + +const redisKeyPrefix = "auth:sess:" + +type payload struct { + UserID string `json:"user_id"` + TenantID string `json:"tenant_id"` +} + +// Store Redis 会话(供 OAuth authorize 与登出)。 +type Store struct { + rdb redis.UniversalClient + cfg *config.Config +} + +// NewStore 创建会话存储。 +func NewStore(rdb redis.UniversalClient, cfg *config.Config) *Store { + return &Store{rdb: rdb, cfg: cfg} +} + +func (s *Store) ttl() time.Duration { + t := s.cfg.Auth.Session.TTL + if t == 0 { + return 168 * time.Hour + } + return t +} + +// Create 创建会话并返回 session id(写入 Cookie 用)。 +func (s *Store) Create(ctx context.Context, userID, tenantID string) (sid string, err error) { + sid, err = security.RandomURLSafe(32) + if err != nil { + return "", err + } + b, err := json.Marshal(payload{UserID: userID, TenantID: tenantID}) + if err != nil { + return "", err + } + return sid, s.rdb.Set(ctx, redisKeyPrefix+sid, b, s.ttl()).Err() +} + +// Get 解析会话。 +func (s *Store) Get(ctx context.Context, sid string) (userID, tenantID string, err error) { + b, err := s.rdb.Get(ctx, redisKeyPrefix+sid).Bytes() + if err == redis.Nil { + return "", "", ErrInvalidSession + } + if err != nil { + return "", "", err + } + var p payload + if err := json.Unmarshal(b, &p); err != nil { + return "", "", ErrInvalidSession + } + return p.UserID, p.TenantID, nil +} + +// Delete 登出时删除。 +func (s *Store) Delete(ctx context.Context, sid string) error { + return s.rdb.Del(ctx, redisKeyPrefix+sid).Err() +} diff --git a/internal/auth/wire_provider.go b/internal/auth/wire_provider.go new file mode 100644 index 0000000..31d61ae --- /dev/null +++ b/internal/auth/wire_provider.go @@ -0,0 +1,43 @@ +package auth + +import ( + "giter.top/smart/internal/auth/handler" + "giter.top/smart/internal/auth/middleware" + "giter.top/smart/internal/auth/oauth2" + "giter.top/smart/internal/auth/session" + "giter.top/smart/pkg/config" + "github.com/gin-gonic/gin" + "github.com/google/wire" +) + +// ProviderSet Wire 注入。 +var ProviderSet = wire.NewSet( + session.NewStore, + oauth2.NewStore, + oauth2.NewService, + oauth2.NewHandler, + handler.NewLoginHandler, + ProvideBearer, + ProvideLoginRLimitWire, + ProvideTokenRLimitWire, + NewAuthRoutes, +) + +// ProvideBearer 提供 Gin 中间件。 +func ProvideBearer(store *oauth2.Store) gin.HandlerFunc { + return middleware.NewBearer(store) +} + +// LoginRateLimitWire、TokenRateLimitWire 用于 Wire 区分多个 gin.HandlerFunc 形参。 +type LoginRateLimitWire gin.HandlerFunc +type TokenRateLimitWire gin.HandlerFunc + +// ProvideLoginRLimitWire 登录接口限流。 +func ProvideLoginRLimitWire(cfg *config.Config) LoginRateLimitWire { + return LoginRateLimitWire(middleware.PerIPMinute(cfg.Auth.RateLimit.Enabled, cfg.Auth.RateLimit.LoginPerMinute)) +} + +// ProvideTokenRLimitWire 令牌与自省端点限流。 +func ProvideTokenRLimitWire(cfg *config.Config) TokenRateLimitWire { + return TokenRateLimitWire(middleware.PerIPMinute(cfg.Auth.RateLimit.Enabled, cfg.Auth.RateLimit.TokenPerMinute)) +} diff --git a/internal/data/provider.go b/internal/data/provider.go new file mode 100644 index 0000000..6df5425 --- /dev/null +++ b/internal/data/provider.go @@ -0,0 +1,9 @@ +package data + +import ( + "giter.top/smart/pkg/cache" + "giter.top/smart/pkg/db" + "github.com/google/wire" +) + +var ProviderSet = wire.NewSet(db.NewDB, cache.NewRedis) \ No newline at end of file diff --git a/internal/iam/entity/dept.go b/internal/iam/entity/dept.go new file mode 100644 index 0000000..d4fca1d --- /dev/null +++ b/internal/iam/entity/dept.go @@ -0,0 +1,24 @@ +package entity + +import ( + "time" + + "gorm.io/gorm" +) + +// Dept 部门 iam_dept(根部门 parent_id 为空字符串) +type Dept struct { + ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"` + TenantID string `json:"tenant_id" gorm:"size:36;not null;index:idx_dept_tenant"` + ParentID string `json:"parent_id" gorm:"size:36;default:'';index:idx_dept_parent"` + DeptName string `json:"dept_name" gorm:"size:128;not null"` + DeptPath string `json:"dept_path" gorm:"type:text"` + LeaderID *string `json:"leader_id" gorm:"size:36"` + SortOrder int `json:"sort_order" gorm:"default:0"` + Status int16 `json:"status" gorm:"default:1"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func (Dept) TableName() string { return "iam_dept" } diff --git a/internal/iam/entity/menu.go b/internal/iam/entity/menu.go new file mode 100644 index 0000000..034a194 --- /dev/null +++ b/internal/iam/entity/menu.go @@ -0,0 +1,32 @@ +package entity + +import ( + "time" + + "gorm.io/gorm" +) + +// PublicOverviewPerms 动态导航中「概览页」类公开权限标识(PRD:所有用户默认可见,需在菜单中配置同名 perms) +const PublicOverviewPerms = "public:overview" + +// Menu 菜单 iam_menu(全局,不按租户分表;根节点 parent_id 为空字符串) +type Menu struct { + ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"` + ParentID string `json:"parent_id" gorm:"size:36;default:'';index:idx_menu_parent"` + MenuName string `json:"menu_name" gorm:"size:128;not null"` + MenuType int16 `json:"menu_type" gorm:"not null"` // 1目录 2菜单 3按钮 + Perms string `json:"perms" gorm:"size:128;uniqueIndex"` + Path string `json:"path" gorm:"size:255"` + Component string `json:"component" gorm:"size:255"` + Icon string `json:"icon" gorm:"size:64"` + SortOrder int `json:"sort_order" gorm:"default:0"` + IsVisible bool `json:"is_visible" gorm:"default:true"` + IsBuiltin bool `json:"is_builtin" gorm:"default:false"` + ExternalLink string `json:"external_link" gorm:"size:512"` + Status int16 `json:"status" gorm:"default:1"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func (Menu) TableName() string { return "iam_menu" } diff --git a/internal/iam/entity/role.go b/internal/iam/entity/role.go new file mode 100644 index 0000000..8690d38 --- /dev/null +++ b/internal/iam/entity/role.go @@ -0,0 +1,43 @@ +package entity + +import ( + "time" + + "gorm.io/gorm" +) + +// 数据范围并集优先级(数值越大权限越大) +const ( + DataScopeSelf int16 = 1 + DataScopeDept int16 = 2 + DataScopeDeptTree int16 = 3 + DataScopeAll int16 = 4 +) + +// Role 角色 iam_role +type Role struct { + ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"` + TenantID string `json:"tenant_id" gorm:"size:36;not null;index:idx_role_tenant"` + RoleCode string `json:"role_code" gorm:"size:64;not null"` + RoleName string `json:"role_name" gorm:"size:128;not null"` + DataScope int16 `json:"data_scope" gorm:"default:4"` // 1本人 2本部门 3本部门及子部门 4全部 + Description string `json:"description" gorm:"size:512"` + IsBuiltin bool `json:"is_builtin" gorm:"default:false"` + Status int16 `json:"status" gorm:"default:1"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func (Role) TableName() string { return "iam_role" } + + +// RoleMenu 角色菜单 iam_role_menu +type RoleMenu struct { + ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"` + RoleID string `json:"role_id" gorm:"size:36;not null;uniqueIndex:uk_role_menu"` + MenuID string `json:"menu_id" gorm:"size:36;not null;uniqueIndex:uk_role_menu"` + CreatedAt time.Time `json:"created_at"` +} + +func (RoleMenu) TableName() string { return "iam_role_menu" } diff --git a/internal/iam/entity/tenant.go b/internal/iam/entity/tenant.go new file mode 100644 index 0000000..1a8a052 --- /dev/null +++ b/internal/iam/entity/tenant.go @@ -0,0 +1,25 @@ +package entity + +import ( + "time" + + "gorm.io/gorm" +) + +// PlatformTenantID 平台租户主键(与初始化数据一致;菜单维护等仅平台租户可操作) +const PlatformTenantID = "00000000-0000-0000-0000-000000000001" + +// Tenant 租户 iam_tenant +type Tenant struct { + ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"` + TenantCode string `json:"tenant_code" gorm:"size:64;uniqueIndex;not null"` + TenantName string `json:"tenant_name" gorm:"size:128;not null"` + AdminUserID *string `json:"admin_user_id" gorm:"size:36"` + Status int16 `json:"status" gorm:"default:1"` // 1 正常 0 冻结 -1 删除(逻辑) + ExpireTime *time.Time `json:"expire_time"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func (Tenant) TableName() string { return "iam_tenant" } diff --git a/internal/iam/entity/user.go b/internal/iam/entity/user.go new file mode 100644 index 0000000..f51086b --- /dev/null +++ b/internal/iam/entity/user.go @@ -0,0 +1,53 @@ +package entity + +import ( + "time" + + "gorm.io/gorm" +) + +// User 用户 iam_user +type User struct { + ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"` + TenantID string `json:"tenant_id" gorm:"size:36;not null;index:idx_user_tenant"` + DeptID *string `json:"dept_id" gorm:"size:36;index:idx_user_dept"` + UserName string `json:"user_name" gorm:"size:64;not null"` + RealName string `json:"real_name" gorm:"size:64"` + PasswordHash string `json:"-" gorm:"size:255;not null"` + Phone string `json:"phone" gorm:"size:20"` + Email string `json:"email" gorm:"size:128"` + Avatar string `json:"avatar" gorm:"size:512"` + Gender int16 `json:"gender" gorm:"default:0"` + Status int16 `json:"status" gorm:"default:1"` + LoginAttempts int `json:"login_attempts" gorm:"default:0"` + LockedUntil *time.Time `json:"locked_until"` + LastLoginAt *time.Time `json:"last_login_at"` + LastLoginIP string `json:"last_login_ip" gorm:"size:45"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func (User) TableName() string { return "iam_user" } + + +// UserDept 用户部门关联 iam_user_dept +type UserDept struct { + ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"` + UserID string `json:"user_id" gorm:"size:36;not null;uniqueIndex:uk_user_dept"` + DeptID string `json:"dept_id" gorm:"size:36;not null;uniqueIndex:uk_user_dept"` + IsPrimary bool `json:"is_primary" gorm:"default:false"` + CreatedAt time.Time `json:"created_at"` +} + +func (UserDept) TableName() string { return "iam_user_dept" } + +// UserRole 用户角色 iam_user_role +type UserRole struct { + ID string `json:"id" gorm:"primaryKey;type:varchar(36);not null"` + UserID string `json:"user_id" gorm:"size:36;not null;uniqueIndex:uk_user_role"` + RoleID string `json:"role_id" gorm:"size:36;not null;uniqueIndex:uk_user_role"` + CreatedAt time.Time `json:"created_at"` +} + +func (UserRole) TableName() string { return "iam_user_role" } diff --git a/internal/iam/handler/dept_handler.go b/internal/iam/handler/dept_handler.go new file mode 100644 index 0000000..4339c12 --- /dev/null +++ b/internal/iam/handler/dept_handler.go @@ -0,0 +1,95 @@ +package handler + +import ( + "net/http" + + "giter.top/smart/internal/iam/service" + "github.com/gin-gonic/gin" +) + +type DeptHandler struct { + svc service.DeptService +} + +func NewDeptHandler(svc service.DeptService) *DeptHandler { + return &DeptHandler{svc: svc} +} + +func (h *DeptHandler) Tree(c *gin.Context) { + tid := headerTenantID(c) + keyword := c.Query("keyword") + var leaderID *string + if s := c.Query("leader_id"); s != "" { + leaderID = &s + } + tree, err := h.svc.Tree(c.Request.Context(), tid, keyword, leaderID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, tree) +} + +func (h *DeptHandler) Create(c *gin.Context) { + tid := headerTenantID(c) + var req service.CreateDeptRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + d, err := h.svc.Create(c.Request.Context(), tid, &req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, d) +} + +func (h *DeptHandler) Update(c *gin.Context) { + tid := headerTenantID(c) + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + var req service.UpdateDeptRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + d, err := h.svc.Update(c.Request.Context(), tid, id, &req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, d) +} + +func (h *DeptHandler) Delete(c *gin.Context) { + tid := headerTenantID(c) + var ids []string + if err := c.ShouldBindJSON(&ids); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := h.svc.Delete(c.Request.Context(), tid, ids); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.Status(http.StatusNoContent) +} + +func (h *DeptHandler) Get(c *gin.Context) { + tid := headerTenantID(c) + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + d, err := h.svc.Get(c.Request.Context(), tid, id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, d) +} diff --git a/internal/iam/handler/helpers.go b/internal/iam/handler/helpers.go new file mode 100644 index 0000000..1ad224c --- /dev/null +++ b/internal/iam/handler/helpers.go @@ -0,0 +1,53 @@ +package handler + +import ( + "strconv" + + authmw "giter.top/smart/internal/auth/middleware" + "giter.top/smart/internal/iam/entity" + "github.com/gin-gonic/gin" +) + +func atoiDef(s string, def int) int { + if s == "" { + return def + } + v, err := strconv.Atoi(s) + if err != nil { + return def + } + return v +} + +// headerTenantID 当前租户:优先 OAuth2 Bearer 解析结果,其次 X-Tenant-ID,缺省平台租户。 +func headerTenantID(c *gin.Context) string { + if v, ok := c.Get(authmw.CtxTenantID); ok { + if s, ok2 := v.(string); ok2 && s != "" { + return s + } + } + s := c.GetHeader("X-Tenant-ID") + if s == "" { + return entity.PlatformTenantID + } + return s +} + +// headerUserID 当前用户:优先 OAuth2 opaque access_token 对应用户,其次 X-User-ID。 +func headerUserID(c *gin.Context) string { + if v, ok := c.Get(authmw.CtxUserID); ok { + if s, ok2 := v.(string); ok2 && s != "" { + return s + } + } + return c.GetHeader("X-User-ID") +} + +// headerGrantorUserID 请求头 X-Grantor-User-ID(授权人,用于防越权校验) +func headerGrantorUserID(c *gin.Context) *string { + s := c.GetHeader("X-Grantor-User-ID") + if s == "" { + return nil + } + return &s +} diff --git a/internal/iam/handler/menu_handler.go b/internal/iam/handler/menu_handler.go new file mode 100644 index 0000000..7857d31 --- /dev/null +++ b/internal/iam/handler/menu_handler.go @@ -0,0 +1,141 @@ +package handler + +import ( + "errors" + "net/http" + "strconv" + + "giter.top/smart/internal/iam/entity" + "giter.top/smart/internal/iam/repository" + "giter.top/smart/internal/iam/service" + "github.com/gin-gonic/gin" +) + +type MenuHandler struct { + svc service.MenuService +} + +func NewMenuHandler(svc service.MenuService) *MenuHandler { + return &MenuHandler{svc: svc} +} + +func isPlatformAdmin(c *gin.Context) bool { + return headerTenantID(c) == entity.PlatformTenantID +} + +func (h *MenuHandler) Create(c *gin.Context) { + var req service.CreateMenuRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + m, err := h.svc.Create(c.Request.Context(), &req, isPlatformAdmin(c)) + if err != nil { + if errors.Is(err, repository.ErrForbidden) { + c.JSON(http.StatusForbidden, gin.H{"error": "仅平台管理员可维护菜单"}) + return + } + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, m) +} + +func (h *MenuHandler) Update(c *gin.Context) { + mid := c.Param("id") + if mid == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + var req service.UpdateMenuRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + m, err := h.svc.Update(c.Request.Context(), mid, &req, isPlatformAdmin(c)) + if err != nil { + if errors.Is(err, repository.ErrForbidden) { + c.JSON(http.StatusForbidden, gin.H{"error": "仅平台管理员可维护菜单"}) + return + } + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, m) +} + +func (h *MenuHandler) Delete(c *gin.Context) { + var ids []string + if err := c.ShouldBindJSON(&ids); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := h.svc.Delete(c.Request.Context(), ids, isPlatformAdmin(c)); err != nil { + if errors.Is(err, repository.ErrForbidden) { + c.JSON(http.StatusForbidden, gin.H{"error": "仅平台管理员可维护菜单"}) + return + } + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.Status(http.StatusNoContent) +} + +func (h *MenuHandler) Get(c *gin.Context) { + mid := c.Param("id") + if mid == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + m, err := h.svc.Get(c.Request.Context(), mid) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, m) +} + +func (h *MenuHandler) Tree(c *gin.Context) { + var mt *int16 + if s := c.Query("menu_type"); s != "" { + v64, err := strconv.ParseInt(s, 10, 16) + if err == nil { + v := int16(v64) + mt = &v + } + } + tree, err := h.svc.Tree(c.Request.Context(), mt) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, tree) +} + +func (h *MenuHandler) Nav(c *gin.Context) { + uid := headerUserID(c) + if uid == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "需要 X-User-ID"}) + return + } + tree, err := h.svc.NavForUser(c.Request.Context(), uid) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, tree) +} + +func (h *MenuHandler) Perms(c *gin.Context) { + uid := headerUserID(c) + if uid == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "需要 X-User-ID"}) + return + } + perms, err := h.svc.PermsForUser(c.Request.Context(), uid) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"perms": perms}) +} diff --git a/internal/iam/handler/role_handler.go b/internal/iam/handler/role_handler.go new file mode 100644 index 0000000..8be9008 --- /dev/null +++ b/internal/iam/handler/role_handler.go @@ -0,0 +1,117 @@ +package handler + +import ( + "net/http" + + "giter.top/smart/internal/iam/service" + "github.com/gin-gonic/gin" +) + +type RoleHandler struct { + svc service.RoleService +} + +func NewRoleHandler(svc service.RoleService) *RoleHandler { + return &RoleHandler{svc: svc} +} + +func (h *RoleHandler) Create(c *gin.Context) { + tid := headerTenantID(c) + var req service.CreateRoleRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + role, err := h.svc.Create(c.Request.Context(), tid, &req, headerGrantorUserID(c)) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, role) +} + +func (h *RoleHandler) Update(c *gin.Context) { + tid := headerTenantID(c) + rid := c.Param("id") + if rid == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + var req service.UpdateRoleRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + r, err := h.svc.Update(c.Request.Context(), tid, rid, &req, headerGrantorUserID(c)) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, r) +} + +func (h *RoleHandler) Delete(c *gin.Context) { + tid := headerTenantID(c) + var ids []string + if err := c.ShouldBindJSON(&ids); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := h.svc.Delete(c.Request.Context(), tid, ids); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.Status(http.StatusNoContent) +} + +func (h *RoleHandler) Get(c *gin.Context) { + tid := headerTenantID(c) + rid := c.Param("id") + if rid == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + r, err := h.svc.Get(c.Request.Context(), tid, rid) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, r) +} + +func (h *RoleHandler) List(c *gin.Context) { + tid := headerTenantID(c) + name := c.Query("name") + code := c.Query("code") + page := atoiDef(c.Query("page"), 1) + pageSize := atoiDef(c.Query("page_size"), 10) + resp, err := h.svc.List(c.Request.Context(), tid, name, code, page, pageSize) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, resp) +} + +type assignMenusBody struct { + MenuIDs []string `json:"menu_ids"` +} + +func (h *RoleHandler) AssignMenus(c *gin.Context) { + tid := headerTenantID(c) + rid := c.Param("id") + if rid == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + var body assignMenusBody + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := h.svc.AssignMenus(c.Request.Context(), tid, rid, body.MenuIDs, headerGrantorUserID(c)); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.Status(http.StatusNoContent) +} diff --git a/internal/iam/handler/tenant_handler.go b/internal/iam/handler/tenant_handler.go new file mode 100644 index 0000000..c8a3744 --- /dev/null +++ b/internal/iam/handler/tenant_handler.go @@ -0,0 +1,98 @@ +package handler + +import ( + "net/http" + "strconv" + + "giter.top/smart/internal/iam/service" + "github.com/gin-gonic/gin" +) + +type TenantHandler struct { + svc service.TenantService +} + +func NewTenantHandler(svc service.TenantService) *TenantHandler { + return &TenantHandler{svc: svc} +} + +func (h *TenantHandler) Create(c *gin.Context) { + var req service.CreateTenantRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + t, err := h.svc.Create(c.Request.Context(), &req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, t) +} + +func (h *TenantHandler) Update(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + var req service.UpdateTenantRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + t, err := h.svc.Update(c.Request.Context(), id, &req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, t) +} + +func (h *TenantHandler) Delete(c *gin.Context) { + var ids []string + if err := c.ShouldBindJSON(&ids); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := h.svc.Delete(c.Request.Context(), ids); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.Status(http.StatusNoContent) +} + +func (h *TenantHandler) Get(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + t, err := h.svc.Get(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, t) +} + +func (h *TenantHandler) List(c *gin.Context) { + name := c.Query("name") + code := c.Query("code") + var status *int16 + if s := c.Query("status"); s != "" { + v64, err := strconv.ParseInt(s, 10, 16) + if err == nil { + v := int16(v64) + status = &v + } + } + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) + resp, err := h.svc.List(c.Request.Context(), name, code, status, page, pageSize) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, resp) +} diff --git a/internal/iam/handler/user_handler.go b/internal/iam/handler/user_handler.go new file mode 100644 index 0000000..2164c2c --- /dev/null +++ b/internal/iam/handler/user_handler.go @@ -0,0 +1,123 @@ +package handler + +import ( + "net/http" + "strconv" + + "giter.top/smart/internal/iam/service" + "github.com/gin-gonic/gin" +) + +type UserHandler struct { + svc service.UserService +} + +func NewUserHandler(svc service.UserService) *UserHandler { + return &UserHandler{svc: svc} +} + +func (h *UserHandler) Create(c *gin.Context) { + tid := headerTenantID(c) + var req service.CreateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + u, err := h.svc.Create(c.Request.Context(), tid, &req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, u) +} + +func (h *UserHandler) Update(c *gin.Context) { + tid := headerTenantID(c) + uid := c.Param("id") + if uid == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + var req service.UpdateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + u, err := h.svc.Update(c.Request.Context(), tid, uid, &req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, u) +} + +func (h *UserHandler) Delete(c *gin.Context) { + tid := headerTenantID(c) + var ids []string + if err := c.ShouldBindJSON(&ids); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := h.svc.Delete(c.Request.Context(), tid, ids); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.Status(http.StatusNoContent) +} + +func (h *UserHandler) Get(c *gin.Context) { + tid := headerTenantID(c) + uid := c.Param("id") + if uid == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + u, err := h.svc.Get(c.Request.Context(), tid, uid) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, u) +} + +func (h *UserHandler) List(c *gin.Context) { + tid := headerTenantID(c) + q := &service.UserListQuery{ + Keyword: c.Query("keyword"), + Page: atoiDef(c.Query("page"), 1), + PageSize: atoiDef(c.Query("page_size"), 10), + } + if s := c.Query("dept_id"); s != "" { + q.DeptID = &s + } + if s := c.Query("role_id"); s != "" { + q.RoleID = &s + } + if s := c.Query("status"); s != "" { + v64, err := strconv.ParseInt(s, 10, 16) + if err == nil { + v := int16(v64) + q.Status = &v + } + } + resp, err := h.svc.List(c.Request.Context(), tid, q) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, resp) +} + +func (h *UserHandler) DataScope(c *gin.Context) { + uid := headerUserID(c) + if uid == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "需要 X-User-ID"}) + return + } + ds, err := h.svc.DataScopeForUser(c.Request.Context(), uid) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data_scope": ds}) +} diff --git a/internal/iam/http_register.go b/internal/iam/http_register.go new file mode 100644 index 0000000..f29876d --- /dev/null +++ b/internal/iam/http_register.go @@ -0,0 +1,87 @@ +package iam + +import ( + "giter.top/smart/internal/iam/handler" + "github.com/gin-gonic/gin" +) + +type IamRoutes struct { + tenantHandler *handler.TenantHandler + deptHandler *handler.DeptHandler + roleHandler *handler.RoleHandler + userHandler *handler.UserHandler + menuHandler *handler.MenuHandler +} + +func NewIamRoutes(tenantHandler *handler.TenantHandler, deptHandler *handler.DeptHandler, roleHandler *handler.RoleHandler, userHandler *handler.UserHandler, menuHandler *handler.MenuHandler) *IamRoutes { + return &IamRoutes{ + tenantHandler: tenantHandler, + deptHandler: deptHandler, + roleHandler: roleHandler, + userHandler: userHandler, + menuHandler: menuHandler, + } +} +// TODO 添加注册信息 +func (s *IamRoutes) Register(engine *gin.Engine, apiGroup *gin.RouterGroup) { + // group :=engine.Group("/iam") + group := apiGroup.Group("/iam") + s.registerTenantRoutes(group) + s.registerDeptRoutes(group) + s.registerRoleRoutes(group) + s.registerUserRoutes(group) + s.registerMenuRoutes(group) +} + +func (s *IamRoutes) registerTenantRoutes(group *gin.RouterGroup) { + tg := group.Group("/tenant") + { + tg.POST("/create", s.tenantHandler.Create) + tg.PUT("/update/:id", s.tenantHandler.Update) + tg.DELETE("/delete-batch", s.tenantHandler.Delete) + tg.GET("/get/:id", s.tenantHandler.Get) + tg.GET("/list", s.tenantHandler.List) + } +} +func (s *IamRoutes) registerDeptRoutes(group *gin.RouterGroup) { + dg := group.Group("/dept") + { + dg.POST("/create", s.deptHandler.Create) + dg.PUT("/update/:id", s.deptHandler.Update) + dg.DELETE("/delete-batch", s.deptHandler.Delete) + dg.GET("/get/:id", s.deptHandler.Get) + dg.GET("/tree", s.deptHandler.Tree) + } +} +func (s *IamRoutes) registerRoleRoutes(group *gin.RouterGroup) { + rg := group.Group("/role") + { + rg.POST("/create", s.roleHandler.Create) + rg.PUT("/update/:id", s.roleHandler.Update) + rg.DELETE("/delete-batch", s.roleHandler.Delete) + rg.GET("/get/:id", s.roleHandler.Get) + rg.GET("/list", s.roleHandler.List) + } +} +func (s *IamRoutes) registerUserRoutes(group *gin.RouterGroup) { + ug := group.Group("/user") + { + ug.POST("/create", s.userHandler.Create) + ug.PUT("/update/:id", s.userHandler.Update) + ug.DELETE("/delete-batch", s.userHandler.Delete) + ug.GET("/get/:id", s.userHandler.Get) + ug.GET("/list", s.userHandler.List) + } +} +func (s *IamRoutes) registerMenuRoutes(group *gin.RouterGroup) { + mg := group.Group("/menu") + { + mg.POST("/create", s.menuHandler.Create) + mg.PUT("/update/:id", s.menuHandler.Update) + mg.DELETE("/delete-batch", s.menuHandler.Delete) + mg.GET("/get/:id", s.menuHandler.Get) + mg.GET("/tree", s.menuHandler.Tree) + mg.GET("/nav", s.menuHandler.Nav) + mg.GET("/perms", s.menuHandler.Perms) + } +} \ No newline at end of file diff --git a/internal/iam/repository/dept_repository.go b/internal/iam/repository/dept_repository.go new file mode 100644 index 0000000..0acb32f --- /dev/null +++ b/internal/iam/repository/dept_repository.go @@ -0,0 +1,93 @@ +package repository + +import ( + "context" + + "giter.top/smart/internal/iam/entity" + "gorm.io/gorm" +) + +// DeptRepository 部门数据访问 +type DeptRepository interface { + Create(ctx context.Context, d *entity.Dept) error + Update(ctx context.Context, d *entity.Dept) error + Delete(ctx context.Context, id string) error + GetByID(ctx context.Context, id string) (*entity.Dept, error) + ListByTenant(ctx context.Context, tenantID string) ([]entity.Dept, error) + CountChildren(ctx context.Context, id string) (int64, error) + ExistsSiblingName(ctx context.Context, tenantID, parentID, name string, excludeID string) (bool, error) + FindRoot(ctx context.Context, tenantID string) (*entity.Dept, error) + UpdatePath(ctx context.Context, id string, path string) error +} + +type deptRepository struct { + db *gorm.DB +} + +func NewDeptRepository(db *gorm.DB) DeptRepository { + return &deptRepository{db: db} +} + +func (r *deptRepository) Create(ctx context.Context, d *entity.Dept) error { + return r.db.WithContext(ctx).Create(d).Error +} + +func (r *deptRepository) Update(ctx context.Context, d *entity.Dept) error { + return r.db.WithContext(ctx).Save(d).Error +} + +func (r *deptRepository) Delete(ctx context.Context, id string) error { + return r.db.WithContext(ctx).Delete(&entity.Dept{}, "id = ?", id).Error +} + +func (r *deptRepository) GetByID(ctx context.Context, id string) (*entity.Dept, error) { + var out entity.Dept + err := r.db.WithContext(ctx).Where("id = ?", id).First(&out).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrNotFound + } + return nil, err + } + return &out, nil +} + +func (r *deptRepository) ListByTenant(ctx context.Context, tenantID string) ([]entity.Dept, error) { + var rows []entity.Dept + err := r.db.WithContext(ctx).Where("tenant_id = ?", tenantID).Order("sort_order ASC, created_at ASC").Find(&rows).Error + return rows, err +} + +func (r *deptRepository) CountChildren(ctx context.Context, id string) (int64, error) { + var n int64 + err := r.db.WithContext(ctx).Model(&entity.Dept{}).Where("parent_id = ?", id).Count(&n).Error + return n, err +} + +func (r *deptRepository) ExistsSiblingName(ctx context.Context, tenantID, parentID, name string, excludeID string) (bool, error) { + q := r.db.WithContext(ctx).Model(&entity.Dept{}).Where("tenant_id = ? AND parent_id = ? AND dept_name = ?", tenantID, parentID, name) + if excludeID != "" { + q = q.Where("id <> ?", excludeID) + } + var n int64 + err := q.Count(&n).Error + return n > 0, err +} + +func (r *deptRepository) FindRoot(ctx context.Context, tenantID string) (*entity.Dept, error) { + var out entity.Dept + err := r.db.WithContext(ctx). + Where("tenant_id = ? AND (parent_id = '' OR parent_id = '0')", tenantID). + First(&out).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrNotFound + } + return nil, err + } + return &out, nil +} + +func (r *deptRepository) UpdatePath(ctx context.Context, id string, path string) error { + return r.db.WithContext(ctx).Model(&entity.Dept{}).Where("id = ?", id).Update("dept_path", path).Error +} diff --git a/internal/iam/repository/errors.go b/internal/iam/repository/errors.go new file mode 100644 index 0000000..1cc0cc1 --- /dev/null +++ b/internal/iam/repository/errors.go @@ -0,0 +1,10 @@ +package repository + +import "errors" + +var ( + ErrNotFound = errors.New("not found") + ErrConflict = errors.New("conflict") + ErrInvalidState = errors.New("invalid state") + ErrForbidden = errors.New("forbidden") +) diff --git a/internal/iam/repository/menu_repository.go b/internal/iam/repository/menu_repository.go new file mode 100644 index 0000000..ad59674 --- /dev/null +++ b/internal/iam/repository/menu_repository.go @@ -0,0 +1,111 @@ +package repository + +import ( + "context" + + "giter.top/smart/internal/iam/entity" + "gorm.io/gorm" +) + +// MenuRepository 菜单 +type MenuRepository interface { + Create(ctx context.Context, m *entity.Menu) error + Update(ctx context.Context, m *entity.Menu) error + Delete(ctx context.Context, id string) error + GetByID(ctx context.Context, id string) (*entity.Menu, error) + ListAll(ctx context.Context) ([]entity.Menu, error) + ListByType(ctx context.Context, menuType *int16) ([]entity.Menu, error) + ExistsPerms(ctx context.Context, perms string, excludeID string) (bool, error) + CountChildren(ctx context.Context, parentID string) (int64, error) + CountRoleRefs(ctx context.Context, menuID string) (int64, error) + ListByPerms(ctx context.Context, perms string) ([]entity.Menu, error) + ListIDsByPermsIn(ctx context.Context, perms []string) ([]string, error) +} + +type menuRepository struct { + db *gorm.DB +} + +func NewMenuRepository(db *gorm.DB) MenuRepository { + return &menuRepository{db: db} +} + +func (r *menuRepository) Create(ctx context.Context, m *entity.Menu) error { + return r.db.WithContext(ctx).Create(m).Error +} + +func (r *menuRepository) Update(ctx context.Context, m *entity.Menu) error { + return r.db.WithContext(ctx).Save(m).Error +} + +func (r *menuRepository) Delete(ctx context.Context, id string) error { + return r.db.WithContext(ctx).Delete(&entity.Menu{}, "id = ?", id).Error +} + +func (r *menuRepository) GetByID(ctx context.Context, id string) (*entity.Menu, error) { + var out entity.Menu + err := r.db.WithContext(ctx).Where("id = ?", id).First(&out).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrNotFound + } + return nil, err + } + return &out, nil +} + +func (r *menuRepository) ListAll(ctx context.Context) ([]entity.Menu, error) { + var rows []entity.Menu + err := r.db.WithContext(ctx).Order("sort_order ASC, created_at ASC").Find(&rows).Error + return rows, err +} + +func (r *menuRepository) ListByType(ctx context.Context, menuType *int16) ([]entity.Menu, error) { + q := r.db.WithContext(ctx).Model(&entity.Menu{}) + if menuType != nil { + q = q.Where("menu_type = ?", *menuType) + } + var rows []entity.Menu + err := q.Order("sort_order ASC, created_at ASC").Find(&rows).Error + return rows, err +} + +func (r *menuRepository) ExistsPerms(ctx context.Context, perms string, excludeID string) (bool, error) { + if perms == "" { + return false, nil + } + q := r.db.WithContext(ctx).Model(&entity.Menu{}).Where("perms = ?", perms) + if excludeID != "" { + q = q.Where("id <> ?", excludeID) + } + var n int64 + err := q.Count(&n).Error + return n > 0, err +} + +func (r *menuRepository) CountChildren(ctx context.Context, parentID string) (int64, error) { + var n int64 + err := r.db.WithContext(ctx).Model(&entity.Menu{}).Where("parent_id = ?", parentID).Count(&n).Error + return n, err +} + +func (r *menuRepository) CountRoleRefs(ctx context.Context, menuID string) (int64, error) { + var n int64 + err := r.db.WithContext(ctx).Model(&entity.RoleMenu{}).Where("menu_id = ?", menuID).Count(&n).Error + return n, err +} + +func (r *menuRepository) ListByPerms(ctx context.Context, perms string) ([]entity.Menu, error) { + var rows []entity.Menu + err := r.db.WithContext(ctx).Where("perms = ?", perms).Find(&rows).Error + return rows, err +} + +func (r *menuRepository) ListIDsByPermsIn(ctx context.Context, perms []string) ([]string, error) { + if len(perms) == 0 { + return nil, nil + } + var ids []string + err := r.db.WithContext(ctx).Model(&entity.Menu{}).Where("perms IN ?", perms).Pluck("id", &ids).Error + return ids, err +} diff --git a/internal/iam/repository/role_repository.go b/internal/iam/repository/role_repository.go new file mode 100644 index 0000000..31eb4f4 --- /dev/null +++ b/internal/iam/repository/role_repository.go @@ -0,0 +1,147 @@ +package repository + +import ( + "context" + + "giter.top/smart/internal/iam/entity" + "giter.top/smart/pkg/utils/id" + "gorm.io/gorm" +) + +// RoleRepository 角色与角色菜单 +type RoleRepository interface { + Create(ctx context.Context, r *entity.Role) error + Update(ctx context.Context, r *entity.Role) error + Delete(ctx context.Context, id string) error + GetByID(ctx context.Context, id string) (*entity.Role, error) + List(ctx context.Context, tenantID string, name, code string, page, pageSize int) ([]entity.Role, int64, error) + ExistsCode(ctx context.Context, tenantID string, code string, excludeID string) (bool, error) + CountUsers(ctx context.Context, roleID string) (int64, error) + ReplaceRoleMenus(ctx context.Context, roleID string, menuIDs []string) error + ListMenuIDsByRole(ctx context.Context, roleID string) ([]string, error) + ListMenuIDsByRoles(ctx context.Context, roleIDs []string) ([]string, error) + ListRolesByUser(ctx context.Context, userID string) ([]entity.Role, error) +} + +type roleRepository struct { + db *gorm.DB +} + +func NewRoleRepository(db *gorm.DB) RoleRepository { + return &roleRepository{db: db} +} + +func (r *roleRepository) Create(ctx context.Context, row *entity.Role) error { + return r.db.WithContext(ctx).Create(row).Error +} + +func (r *roleRepository) Update(ctx context.Context, row *entity.Role) error { + return r.db.WithContext(ctx).Save(row).Error +} + +func (r *roleRepository) Delete(ctx context.Context, id string) error { + return r.db.WithContext(ctx).Delete(&entity.Role{}, "id = ?", id).Error +} + +func (r *roleRepository) GetByID(ctx context.Context, id string) (*entity.Role, error) { + var out entity.Role + err := r.db.WithContext(ctx).Where("id = ?", id).First(&out).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrNotFound + } + return nil, err + } + return &out, nil +} + +func (r *roleRepository) List(ctx context.Context, tenantID string, name, code string, page, pageSize int) ([]entity.Role, int64, error) { + q := r.db.WithContext(ctx).Model(&entity.Role{}).Where("tenant_id = ?", tenantID) + if name != "" { + q = q.Where("role_name LIKE ?", "%"+name+"%") + } + if code != "" { + q = q.Where("role_code LIKE ?", "%"+code+"%") + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + offset := (page - 1) * pageSize + var rows []entity.Role + err := q.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&rows).Error + return rows, total, err +} + +func (r *roleRepository) ExistsCode(ctx context.Context, tenantID string, code string, excludeID string) (bool, error) { + q := r.db.WithContext(ctx).Model(&entity.Role{}).Where("tenant_id = ? AND role_code = ?", tenantID, code) + if excludeID != "" { + q = q.Where("id <> ?", excludeID) + } + var n int64 + err := q.Count(&n).Error + return n > 0, err +} + +func (r *roleRepository) CountUsers(ctx context.Context, roleID string) (int64, error) { + var n int64 + err := r.db.WithContext(ctx).Model(&entity.UserRole{}).Where("role_id = ?", roleID).Count(&n).Error + return n, err +} + +func (r *roleRepository) ReplaceRoleMenus(ctx context.Context, roleID string, menuIDs []string) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Where("role_id = ?", roleID).Delete(&entity.RoleMenu{}).Error; err != nil { + return err + } + for _, mid := range menuIDs { + rm := entity.RoleMenu{ID: id.New(), RoleID: roleID, MenuID: mid} + if err := tx.Create(&rm).Error; err != nil { + return err + } + } + return nil + }) +} + +func (r *roleRepository) ListMenuIDsByRole(ctx context.Context, roleID string) ([]string, error) { + var ids []string + err := r.db.WithContext(ctx).Model(&entity.RoleMenu{}).Where("role_id = ?", roleID).Pluck("menu_id", &ids).Error + return ids, err +} + +func (r *roleRepository) ListMenuIDsByRoles(ctx context.Context, roleIDs []string) ([]string, error) { + if len(roleIDs) == 0 { + return nil, nil + } + var raw []string + err := r.db.WithContext(ctx).Model(&entity.RoleMenu{}).Where("role_id IN ?", roleIDs).Pluck("menu_id", &raw).Error + if err != nil { + return nil, err + } + seen := make(map[string]struct{}, len(raw)) + var ids []string + for _, menuID := range raw { + if _, ok := seen[menuID]; ok { + continue + } + seen[menuID] = struct{}{} + ids = append(ids, menuID) + } + return ids, nil +} + +func (r *roleRepository) ListRolesByUser(ctx context.Context, userID string) ([]entity.Role, error) { + var roles []entity.Role + err := r.db.WithContext(ctx).Table("iam_role"). + Joins("JOIN iam_user_role ur ON ur.role_id = iam_role.id"). + Where("ur.user_id = ?", userID). + Find(&roles).Error + return roles, err +} diff --git a/internal/iam/repository/tenant_repository.go b/internal/iam/repository/tenant_repository.go new file mode 100644 index 0000000..551dcc6 --- /dev/null +++ b/internal/iam/repository/tenant_repository.go @@ -0,0 +1,109 @@ +package repository + +import ( + "context" + + "giter.top/smart/internal/iam/entity" + "gorm.io/gorm" +) + +// TenantRepository 租户数据访问 +type TenantRepository interface { + Create(ctx context.Context, t *entity.Tenant) error + Update(ctx context.Context, t *entity.Tenant) error + GetByID(ctx context.Context, id string) (*entity.Tenant, error) + GetByCode(ctx context.Context, code string) (*entity.Tenant, error) + List(ctx context.Context, name, code string, status *int16, page, pageSize int) ([]entity.Tenant, int64, error) + CountUsers(ctx context.Context, tenantID string) (int64, error) + CountDepts(ctx context.Context, tenantID string) (int64, error) + ExistsCode(ctx context.Context, code string, excludeID string) (bool, error) +} + +type tenantRepository struct { + db *gorm.DB +} + +func NewTenantRepository(db *gorm.DB) TenantRepository { + return &tenantRepository{db: db} +} + +func (r *tenantRepository) Create(ctx context.Context, t *entity.Tenant) error { + return r.db.WithContext(ctx).Create(t).Error +} + +func (r *tenantRepository) Update(ctx context.Context, t *entity.Tenant) error { + return r.db.WithContext(ctx).Save(t).Error +} + +func (r *tenantRepository) GetByID(ctx context.Context, id string) (*entity.Tenant, error) { + var out entity.Tenant + err := r.db.WithContext(ctx).Where("id = ?", id).First(&out).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrNotFound + } + return nil, err + } + return &out, nil +} + +func (r *tenantRepository) GetByCode(ctx context.Context, code string) (*entity.Tenant, error) { + var out entity.Tenant + err := r.db.WithContext(ctx).Where("tenant_code = ?", code).First(&out).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrNotFound + } + return nil, err + } + return &out, nil +} + +func (r *tenantRepository) List(ctx context.Context, name, code string, status *int16, page, pageSize int) ([]entity.Tenant, int64, error) { + q := r.db.WithContext(ctx).Model(&entity.Tenant{}) + if name != "" { + q = q.Where("tenant_name LIKE ?", "%"+name+"%") + } + if code != "" { + q = q.Where("tenant_code LIKE ?", "%"+code+"%") + } + if status != nil { + q = q.Where("status = ?", *status) + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + offset := (page - 1) * pageSize + var rows []entity.Tenant + err := q.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&rows).Error + return rows, total, err +} + +func (r *tenantRepository) CountUsers(ctx context.Context, tenantID string) (int64, error) { + var n int64 + err := r.db.WithContext(ctx).Model(&entity.User{}).Where("tenant_id = ?", tenantID).Count(&n).Error + return n, err +} + +func (r *tenantRepository) CountDepts(ctx context.Context, tenantID string) (int64, error) { + var n int64 + err := r.db.WithContext(ctx).Model(&entity.Dept{}).Where("tenant_id = ?", tenantID).Count(&n).Error + return n, err +} + +func (r *tenantRepository) ExistsCode(ctx context.Context, code string, excludeID string) (bool, error) { + q := r.db.WithContext(ctx).Model(&entity.Tenant{}).Where("tenant_code = ?", code) + if excludeID != "" { + q = q.Where("id <> ?", excludeID) + } + var n int64 + err := q.Count(&n).Error + return n > 0, err +} diff --git a/internal/iam/repository/user_repository.go b/internal/iam/repository/user_repository.go new file mode 100644 index 0000000..23d269b --- /dev/null +++ b/internal/iam/repository/user_repository.go @@ -0,0 +1,179 @@ +package repository + +import ( + "context" + + "giter.top/smart/internal/iam/entity" + "giter.top/smart/pkg/utils/id" + "gorm.io/gorm" +) + +// UserRepository 用户数据访问 +type UserRepository interface { + Create(ctx context.Context, u *entity.User) error + Update(ctx context.Context, u *entity.User) error + Delete(ctx context.Context, id string) error + GetByID(ctx context.Context, id string) (*entity.User, error) + GetByUserName(ctx context.Context, tenantID string, userName string) (*entity.User, error) + ExistsUserName(ctx context.Context, tenantID string, userName string, excludeID string) (bool, error) + CountByDept(ctx context.Context, deptID string) (int64, error) + List(ctx context.Context, tenantID string, deptID *string, roleID *string, keyword string, status *int16, page, pageSize int) ([]entity.User, int64, error) + ReplaceUserDepts(ctx context.Context, userID string, primaryDept string, deptIDs []string) error + ReplaceUserRoles(ctx context.Context, userID string, roleIDs []string) error + ListRoleIDs(ctx context.Context, userID string) ([]string, error) + ListDeptIDs(ctx context.Context, userID string) ([]string, error) +} + +type userRepository struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) UserRepository { + return &userRepository{db: db} +} + +func (r *userRepository) Create(ctx context.Context, u *entity.User) error { + return r.db.WithContext(ctx).Create(u).Error +} + +func (r *userRepository) Update(ctx context.Context, u *entity.User) error { + return r.db.WithContext(ctx).Save(u).Error +} + +func (r *userRepository) Delete(ctx context.Context, id string) error { + return r.db.WithContext(ctx).Delete(&entity.User{}, "id = ?", id).Error +} + +func (r *userRepository) GetByID(ctx context.Context, id string) (*entity.User, error) { + var out entity.User + err := r.db.WithContext(ctx).Where("id = ?", id).First(&out).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrNotFound + } + return nil, err + } + return &out, nil +} + +func (r *userRepository) GetByUserName(ctx context.Context, tenantID string, userName string) (*entity.User, error) { + var out entity.User + err := r.db.WithContext(ctx).Where("tenant_id = ? AND user_name = ?", tenantID, userName).First(&out).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, ErrNotFound + } + return nil, err + } + return &out, nil +} + +func (r *userRepository) ExistsUserName(ctx context.Context, tenantID string, userName string, excludeID string) (bool, error) { + q := r.db.WithContext(ctx).Model(&entity.User{}).Where("tenant_id = ? AND user_name = ?", tenantID, userName) + if excludeID != "" { + q = q.Where("id <> ?", excludeID) + } + var n int64 + err := q.Count(&n).Error + return n > 0, err +} + +func (r *userRepository) CountByDept(ctx context.Context, deptID string) (int64, error) { + var n int64 + err := r.db.WithContext(ctx).Raw(` + SELECT COUNT(*) FROM ( + SELECT id FROM iam_user WHERE dept_id = ? AND deleted_at IS NULL + UNION + SELECT user_id FROM iam_user_dept WHERE dept_id = ? + ) t`, deptID, deptID).Scan(&n).Error + return n, err +} + +func (r *userRepository) List(ctx context.Context, tenantID string, deptID *string, roleID *string, keyword string, status *int16, page, pageSize int) ([]entity.User, int64, error) { + q := r.db.WithContext(ctx).Model(&entity.User{}).Where("tenant_id = ?", tenantID) + if deptID != nil { + d := *deptID + q = q.Where("dept_id = ? OR id IN (SELECT user_id FROM iam_user_dept WHERE dept_id = ?)", d, d) + } + if roleID != nil { + sub := r.db.WithContext(ctx).Model(&entity.UserRole{}).Select("user_id").Where("role_id = ?", *roleID) + q = q.Where("id IN (?)", sub) + } + if keyword != "" { + kw := "%" + keyword + "%" + q = q.Where("user_name LIKE ? OR real_name LIKE ? OR phone LIKE ? OR email LIKE ?", kw, kw, kw, kw) + } + if status != nil { + q = q.Where("status = ?", *status) + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + offset := (page - 1) * pageSize + var rows []entity.User + err := q.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&rows).Error + return rows, total, err +} + +func (r *userRepository) ReplaceUserDepts(ctx context.Context, userID string, primaryDept string, deptIDs []string) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Where("user_id = ?", userID).Delete(&entity.UserDept{}).Error; err != nil { + return err + } + seen := map[string]struct{}{} + for _, did := range deptIDs { + if _, ok := seen[did]; ok { + continue + } + seen[did] = struct{}{} + ud := entity.UserDept{ + ID: id.New(), + UserID: userID, + DeptID: did, + IsPrimary: did == primaryDept, + } + if err := tx.Create(&ud).Error; err != nil { + return err + } + } + if len(deptIDs) == 0 && primaryDept != "" { + ud := entity.UserDept{ID: id.New(), UserID: userID, DeptID: primaryDept, IsPrimary: true} + return tx.Create(&ud).Error + } + return nil + }) +} + +func (r *userRepository) ReplaceUserRoles(ctx context.Context, userID string, roleIDs []string) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Where("user_id = ?", userID).Delete(&entity.UserRole{}).Error; err != nil { + return err + } + for _, rid := range roleIDs { + ur := entity.UserRole{ID: id.New(), UserID: userID, RoleID: rid} + if err := tx.Create(&ur).Error; err != nil { + return err + } + } + return nil + }) +} + +func (r *userRepository) ListRoleIDs(ctx context.Context, userID string) ([]string, error) { + var ids []string + err := r.db.WithContext(ctx).Model(&entity.UserRole{}).Where("user_id = ?", userID).Pluck("role_id", &ids).Error + return ids, err +} + +func (r *userRepository) ListDeptIDs(ctx context.Context, userID string) ([]string, error) { + var ids []string + err := r.db.WithContext(ctx).Model(&entity.UserDept{}).Where("user_id = ?", userID).Pluck("dept_id", &ids).Error + return ids, err +} diff --git a/internal/iam/service/constants.go b/internal/iam/service/constants.go new file mode 100644 index 0000000..371b4b4 --- /dev/null +++ b/internal/iam/service/constants.go @@ -0,0 +1,4 @@ +package service + +// DefaultTenantAdminRoleCode 新租户初始化时的单位管理员角色编码 +const DefaultTenantAdminRoleCode = "tenant_admin" diff --git a/internal/iam/service/dept_service.go b/internal/iam/service/dept_service.go new file mode 100644 index 0000000..f3f1355 --- /dev/null +++ b/internal/iam/service/dept_service.go @@ -0,0 +1,326 @@ +package service + +import ( + "context" + "errors" + "fmt" + "strings" + + "giter.top/smart/internal/iam/entity" + "giter.top/smart/internal/iam/repository" + "giter.top/smart/pkg/utils/id" +) + +// DeptService 部门 +type DeptService interface { + Tree(ctx context.Context, tenantID string, keyword string, leaderID *string) ([]DeptNode, error) + Create(ctx context.Context, tenantID string, req *CreateDeptRequest) (*entity.Dept, error) + Update(ctx context.Context, tenantID string, id string, req *UpdateDeptRequest) (*entity.Dept, error) + Delete(ctx context.Context, tenantID string, ids []string) error + Get(ctx context.Context, tenantID string, id string) (*entity.Dept, error) +} + +type CreateDeptRequest struct { + ParentID string `json:"parent_id"` + DeptName string `json:"dept_name" binding:"required,max=128"` + LeaderID *string `json:"leader_id"` + SortOrder int `json:"sort_order"` +} + +type UpdateDeptRequest struct { + ParentID *string `json:"parent_id"` + DeptName *string `json:"dept_name" binding:"omitempty,max=128"` + LeaderID *string `json:"leader_id"` + SortOrder *int `json:"sort_order"` +} + +// DeptNode 树节点 +type DeptNode struct { + entity.Dept + Children []DeptNode `json:"children,omitempty"` +} + +type deptService struct { + depts repository.DeptRepository + users repository.UserRepository +} + +func NewDeptService(depts repository.DeptRepository, users repository.UserRepository) DeptService { + return &deptService{depts: depts, users: users} +} + +func isDeptRoot(d *entity.Dept) bool { + return d.ParentID == "" || d.ParentID == "0" +} + +func (s *deptService) Tree(ctx context.Context, tenantID string, keyword string, leaderID *string) ([]DeptNode, error) { + rows, err := s.depts.ListByTenant(ctx, tenantID) + if err != nil { + return nil, err + } + filtered := rows + if keyword != "" || leaderID != nil { + filtered = make([]entity.Dept, 0) + for _, d := range rows { + if keyword != "" && !strings.Contains(d.DeptName, keyword) { + continue + } + if leaderID != nil && (d.LeaderID == nil || *d.LeaderID != *leaderID) { + continue + } + filtered = append(filtered, d) + } + if keyword != "" || leaderID != nil { + filtered = s.includeAncestors(rows, filtered) + } + } + return buildDeptTree(filtered, ""), nil +} + +func (s *deptService) includeAncestors(all []entity.Dept, matched []entity.Dept) []entity.Dept { + idSet := map[string]struct{}{} + byID := map[string]entity.Dept{} + for _, d := range all { + byID[d.ID] = d + } + for _, d := range matched { + cur := d + for { + idSet[cur.ID] = struct{}{} + if isDeptRoot(&cur) { + break + } + p, ok := byID[cur.ParentID] + if !ok { + break + } + cur = p + } + } + out := make([]entity.Dept, 0, len(idSet)) + for _, d := range all { + if _, ok := idSet[d.ID]; ok { + out = append(out, d) + } + } + return out +} + +func buildDeptTree(rows []entity.Dept, parentID string) []DeptNode { + children := map[string][]entity.Dept{} + for _, d := range rows { + pid := d.ParentID + if d.ParentID == "0" { + pid = "" + } + children[pid] = append(children[pid], d) + } + var walk func(pid string) []DeptNode + walk = func(pid string) []DeptNode { + list := children[pid] + out := make([]DeptNode, 0, len(list)) + for _, d := range list { + out = append(out, DeptNode{Dept: d, Children: walk(d.ID)}) + } + return out + } + return walk(parentID) +} + +func (s *deptService) Create(ctx context.Context, tenantID string, req *CreateDeptRequest) (*entity.Dept, error) { + parentKey := req.ParentID + if parentKey == "0" { + parentKey = "" + } + ok, err := s.depts.ExistsSiblingName(ctx, tenantID, parentKey, req.DeptName, "") + if err != nil { + return nil, err + } + if ok { + return nil, fmt.Errorf("同级部门名称已存在") + } + d := &entity.Dept{ + ID: id.New(), + TenantID: tenantID, + ParentID: parentKey, + DeptName: req.DeptName, + LeaderID: req.LeaderID, + SortOrder: req.SortOrder, + Status: 1, + } + if err := s.depts.Create(ctx, d); err != nil { + return nil, err + } + path := fmt.Sprintf("/%s/", d.ID) + if parentKey != "" { + p, err := s.depts.GetByID(ctx, parentKey) + if err != nil { + return nil, err + } + if p.TenantID != tenantID { + return nil, fmt.Errorf("父部门不属于当前租户") + } + base := p.DeptPath + if base == "" { + base = fmt.Sprintf("/%s/", p.ID) + } + path = base + fmt.Sprintf("%s/", d.ID) + } + if err := s.depts.UpdatePath(ctx, d.ID, path); err != nil { + return nil, err + } + d.DeptPath = path + return d, nil +} + +func (s *deptService) Update(ctx context.Context, tenantID string, id string, req *UpdateDeptRequest) (*entity.Dept, error) { + d, err := s.depts.GetByID(ctx, id) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + return nil, fmt.Errorf("部门不存在") + } + return nil, err + } + if d.TenantID != tenantID { + return nil, fmt.Errorf("部门不属于当前租户") + } + if isDeptRoot(d) { + if req.ParentID != nil && *req.ParentID != "" && *req.ParentID != "0" { + return nil, fmt.Errorf("根部门禁止移动") + } + if req.DeptName != nil && *req.DeptName != "" && *req.DeptName != d.DeptName { + return nil, fmt.Errorf("根部门禁止重命名") + } + } + curParent := d.ParentID + if curParent == "0" { + curParent = "" + } + var newParentForName string + if req.ParentID != nil { + np := *req.ParentID + if np == "0" { + np = "" + } + newParentForName = np + } else { + newParentForName = curParent + } + if req.DeptName != nil && *req.DeptName != "" { + ok, err := s.depts.ExistsSiblingName(ctx, tenantID, newParentForName, *req.DeptName, id) + if err != nil { + return nil, err + } + if ok { + return nil, fmt.Errorf("同级部门名称已存在") + } + d.DeptName = *req.DeptName + } + if req.ParentID != nil { + npID := *req.ParentID + if npID == "0" { + npID = "" + } + if npID != curParent { + if npID == id { + return nil, fmt.Errorf("不能将部门移动到自身之下") + } + if npID != "" { + if s.isDescendant(ctx, id, npID) { + return nil, fmt.Errorf("禁止移动至子部门(防环)") + } + np, err := s.depts.GetByID(ctx, npID) + if err != nil { + return nil, fmt.Errorf("父部门无效") + } + if np.TenantID != tenantID { + return nil, fmt.Errorf("父部门不属于当前租户") + } + d.ParentID = npID + base := np.DeptPath + if base == "" { + base = fmt.Sprintf("/%s/", np.ID) + } + d.DeptPath = base + fmt.Sprintf("%s/", d.ID) + } else { + d.ParentID = "" + d.DeptPath = fmt.Sprintf("/%s/", d.ID) + } + _ = s.depts.UpdatePath(ctx, d.ID, d.DeptPath) + } + } + if req.LeaderID != nil { + d.LeaderID = req.LeaderID + } + if req.SortOrder != nil { + d.SortOrder = *req.SortOrder + } + if err := s.depts.Update(ctx, d); err != nil { + return nil, err + } + return d, nil +} + +func (s *deptService) isDescendant(ctx context.Context, rootID, nodeID string) bool { + if nodeID == rootID { + return true + } + cur, err := s.depts.GetByID(ctx, nodeID) + if err != nil { + return false + } + for i := 0; i < 64 && cur.ParentID != "" && cur.ParentID != "0"; i++ { + if cur.ParentID == rootID { + return true + } + cur, err = s.depts.GetByID(ctx, cur.ParentID) + if err != nil { + return false + } + } + return false +} + +func (s *deptService) Delete(ctx context.Context, tenantID string, ids []string) error { + for _, did := range ids { + d, err := s.depts.GetByID(ctx, did) + if err != nil { + return err + } + if d.TenantID != tenantID { + return fmt.Errorf("部门 %s 不属于当前租户", did) + } + if isDeptRoot(d) { + return fmt.Errorf("根部门禁止删除") + } + n, err := s.depts.CountChildren(ctx, did) + if err != nil { + return err + } + if n > 0 { + return fmt.Errorf("部门 %s 存在子部门", did) + } + uc, err := s.users.CountByDept(ctx, did) + if err != nil { + return err + } + if uc > 0 { + return fmt.Errorf("部门 %s 仍存在用户", did) + } + if err := s.depts.Delete(ctx, did); err != nil { + return err + } + } + return nil +} + +func (s *deptService) Get(ctx context.Context, tenantID string, id string) (*entity.Dept, error) { + d, err := s.depts.GetByID(ctx, id) + if err != nil { + return nil, err + } + if d.TenantID != tenantID { + return nil, fmt.Errorf("部门不属于当前租户") + } + return d, nil +} diff --git a/internal/iam/service/menu_service.go b/internal/iam/service/menu_service.go new file mode 100644 index 0000000..f0909a5 --- /dev/null +++ b/internal/iam/service/menu_service.go @@ -0,0 +1,319 @@ +package service + +import ( + "context" + "errors" + "fmt" + "sort" + + "giter.top/smart/internal/iam/entity" + "giter.top/smart/internal/iam/repository" + "giter.top/smart/pkg/utils/id" +) + +// MenuService 菜单(全局资源) +type MenuService interface { + Create(ctx context.Context, req *CreateMenuRequest, isPlatform bool) (*entity.Menu, error) + Update(ctx context.Context, mid string, req *UpdateMenuRequest, isPlatform bool) (*entity.Menu, error) + Delete(ctx context.Context, ids []string, isPlatform bool) error + Get(ctx context.Context, mid string) (*entity.Menu, error) + Tree(ctx context.Context, menuType *int16) ([]MenuNode, error) + NavForUser(ctx context.Context, userID string) ([]MenuNode, error) + PermsForUser(ctx context.Context, userID string) ([]string, error) +} + +type CreateMenuRequest struct { + ParentID string `json:"parent_id"` + MenuName string `json:"menu_name" binding:"required,max=128"` + MenuType int16 `json:"menu_type" binding:"required"` + Perms string `json:"perms"` + Path string `json:"path"` + Component string `json:"component"` + Icon string `json:"icon"` + SortOrder int `json:"sort_order"` + IsVisible bool `json:"is_visible"` + IsBuiltin bool `json:"is_builtin"` + ExternalLink string `json:"external_link"` +} + +type UpdateMenuRequest struct { + ParentID *string `json:"parent_id"` + MenuName *string `json:"menu_name"` + SortOrder *int `json:"sort_order"` + IsVisible *bool `json:"is_visible"` + Path *string `json:"path"` + Component *string `json:"component"` + Icon *string `json:"icon"` + ExternalLink *string `json:"external_link"` + Status *int16 `json:"status"` +} + +// MenuNode 菜单树节点 +type MenuNode struct { + entity.Menu + Children []MenuNode `json:"children,omitempty"` +} + +type menuService struct { + menus repository.MenuRepository + roles repository.RoleRepository + users repository.UserRepository +} + +func NewMenuService(menus repository.MenuRepository, roles repository.RoleRepository, users repository.UserRepository) MenuService { + return &menuService{menus: menus, roles: roles, users: users} +} + +func normalizeMenuParent(pid string) string { + if pid == "0" { + return "" + } + return pid +} + +func (s *menuService) Create(ctx context.Context, req *CreateMenuRequest, isPlatform bool) (*entity.Menu, error) { + if !isPlatform { + return nil, repository.ErrForbidden + } + if req.Perms != "" { + ok, err := s.menus.ExistsPerms(ctx, req.Perms, "") + if err != nil { + return nil, err + } + if ok { + return nil, fmt.Errorf("权限标识已存在") + } + } + m := &entity.Menu{ + ID: id.New(), + ParentID: normalizeMenuParent(req.ParentID), + MenuName: req.MenuName, + MenuType: req.MenuType, + Perms: req.Perms, + Path: req.Path, + Component: req.Component, + Icon: req.Icon, + SortOrder: req.SortOrder, + IsVisible: req.IsVisible, + IsBuiltin: req.IsBuiltin, + ExternalLink: req.ExternalLink, + Status: 1, + } + if err := s.menus.Create(ctx, m); err != nil { + return nil, err + } + return m, nil +} + +func (s *menuService) Update(ctx context.Context, mid string, req *UpdateMenuRequest, isPlatform bool) (*entity.Menu, error) { + if !isPlatform { + return nil, repository.ErrForbidden + } + m, err := s.menus.GetByID(ctx, mid) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + return nil, fmt.Errorf("菜单不存在") + } + return nil, err + } + if m.IsBuiltin { + return nil, fmt.Errorf("系统内置菜单禁止修改") + } + if req.MenuName != nil { + m.MenuName = *req.MenuName + } + if req.SortOrder != nil { + m.SortOrder = *req.SortOrder + } + if req.IsVisible != nil { + m.IsVisible = *req.IsVisible + } + if req.Path != nil { + m.Path = *req.Path + } + if req.Component != nil { + m.Component = *req.Component + } + if req.Icon != nil { + m.Icon = *req.Icon + } + if req.ExternalLink != nil { + m.ExternalLink = *req.ExternalLink + } + if req.Status != nil { + m.Status = *req.Status + } + if err := s.menus.Update(ctx, m); err != nil { + return nil, err + } + return m, nil +} + +func (s *menuService) Delete(ctx context.Context, ids []string, isPlatform bool) error { + if !isPlatform { + return repository.ErrForbidden + } + for _, mid := range ids { + m, err := s.menus.GetByID(ctx, mid) + if err != nil { + return err + } + if m.IsBuiltin { + return fmt.Errorf("系统内置菜单禁止删除") + } + n, err := s.menus.CountChildren(ctx, mid) + if err != nil { + return err + } + if n > 0 { + return fmt.Errorf("存在子菜单,无法删除") + } + rn, err := s.menus.CountRoleRefs(ctx, mid) + if err != nil { + return err + } + if rn > 0 { + return fmt.Errorf("菜单仍被角色引用") + } + if err := s.menus.Delete(ctx, mid); err != nil { + return err + } + } + return nil +} + +func (s *menuService) Get(ctx context.Context, mid string) (*entity.Menu, error) { + return s.menus.GetByID(ctx, mid) +} + +func (s *menuService) Tree(ctx context.Context, menuType *int16) ([]MenuNode, error) { + rows, err := s.menus.ListByType(ctx, menuType) + if err != nil { + return nil, err + } + return buildMenuTreeRows(rows), nil +} + +func buildMenuTreeRows(rows []entity.Menu) []MenuNode { + byParent := map[string][]entity.Menu{} + for _, m := range rows { + pid := normalizeMenuParent(m.ParentID) + byParent[pid] = append(byParent[pid], m) + } + for k := range byParent { + sort.Slice(byParent[k], func(i, j int) bool { + if byParent[k][i].SortOrder != byParent[k][j].SortOrder { + return byParent[k][i].SortOrder < byParent[k][j].SortOrder + } + return byParent[k][i].ID < byParent[k][j].ID + }) + } + var walk func(pid string) []MenuNode + walk = func(pid string) []MenuNode { + list := byParent[pid] + out := make([]MenuNode, 0, len(list)) + for _, m := range list { + out = append(out, MenuNode{Menu: m, Children: walk(m.ID)}) + } + return out + } + return walk("") +} + +func (s *menuService) NavForUser(ctx context.Context, userID string) ([]MenuNode, error) { + rids, err := s.users.ListRoleIDs(ctx, userID) + if err != nil { + return nil, err + } + menuIDs, err := s.roles.ListMenuIDsByRoles(ctx, rids) + if err != nil { + return nil, err + } + allowed := map[string]struct{}{} + for _, mid := range menuIDs { + allowed[mid] = struct{}{} + } + pub, err := s.menus.ListByPerms(ctx, entity.PublicOverviewPerms) + if err != nil { + return nil, err + } + for _, m := range pub { + allowed[m.ID] = struct{}{} + } + all, err := s.menus.ListAll(ctx) + if err != nil { + return nil, err + } + byID := map[string]entity.Menu{} + for _, m := range all { + byID[m.ID] = m + } + for _, m := range all { + if _, ok := allowed[m.ID]; !ok { + continue + } + cur := m + for { + pid := normalizeMenuParent(cur.ParentID) + if pid == "" { + break + } + p, ok := byID[pid] + if !ok { + break + } + allowed[p.ID] = struct{}{} + cur = p + } + } + filtered := make([]entity.Menu, 0) + for _, m := range all { + if _, ok := allowed[m.ID]; ok && m.Status == 1 && m.IsVisible { + filtered = append(filtered, m) + } + } + tree := buildMenuTreeRows(filtered) + return pruneEmptyDirs(tree), nil +} + +func pruneEmptyDirs(nodes []MenuNode) []MenuNode { + out := make([]MenuNode, 0, len(nodes)) + for _, n := range nodes { + ch := pruneEmptyDirs(n.Children) + if n.MenuType == 1 && len(ch) == 0 { + continue + } + n.Children = ch + out = append(out, n) + } + return out +} + +func (s *menuService) PermsForUser(ctx context.Context, userID string) ([]string, error) { + rids, err := s.users.ListRoleIDs(ctx, userID) + if err != nil { + return nil, err + } + mids, err := s.roles.ListMenuIDsByRoles(ctx, rids) + if err != nil { + return nil, err + } + all, err := s.menus.ListAll(ctx) + if err != nil { + return nil, err + } + idset := map[string]struct{}{} + for _, mid := range mids { + idset[mid] = struct{}{} + } + var perms []string + for _, m := range all { + if _, ok := idset[m.ID]; !ok { + continue + } + if m.Perms != "" { + perms = append(perms, m.Perms) + } + } + return perms, nil +} diff --git a/internal/iam/service/role_service.go b/internal/iam/service/role_service.go new file mode 100644 index 0000000..dca8961 --- /dev/null +++ b/internal/iam/service/role_service.go @@ -0,0 +1,223 @@ +package service + +import ( + "context" + "errors" + "fmt" + + "giter.top/smart/internal/iam/entity" + "giter.top/smart/internal/iam/repository" + "giter.top/smart/pkg/utils/id" +) + +// RoleService 角色 +type RoleService interface { + Create(ctx context.Context, tenantID string, req *CreateRoleRequest, grantorUserID *string) (*entity.Role, error) + Update(ctx context.Context, tenantID string, rid string, req *UpdateRoleRequest, grantorUserID *string) (*entity.Role, error) + Delete(ctx context.Context, tenantID string, ids []string) error + Get(ctx context.Context, tenantID string, rid string) (*entity.Role, error) + List(ctx context.Context, tenantID string, name, code string, page, pageSize int) (*RoleListResponse, error) + AssignMenus(ctx context.Context, tenantID string, roleID string, menuIDs []string, grantorUserID *string) error +} + +type CreateRoleRequest struct { + RoleCode string `json:"role_code" binding:"required,max=64"` + RoleName string `json:"role_name" binding:"required,max=128"` + DataScope int16 `json:"data_scope" binding:"required"` + Description string `json:"description"` + MenuIDs []string `json:"menu_ids"` +} + +type UpdateRoleRequest struct { + RoleName *string `json:"role_name"` + DataScope *int16 `json:"data_scope"` + Description *string `json:"description"` + MenuIDs []string `json:"menu_ids"` +} + +type RoleListResponse struct { + Items []entity.Role `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` +} + +type roleService struct { + roles repository.RoleRepository + users repository.UserRepository + menus repository.MenuRepository +} + +func NewRoleService(roles repository.RoleRepository, users repository.UserRepository, menus repository.MenuRepository) RoleService { + return &roleService{roles: roles, users: users, menus: menus} +} + +func (s *roleService) grantorMenuSet(ctx context.Context, grantorUserID string) (map[string]struct{}, error) { + rids, err := s.users.ListRoleIDs(ctx, grantorUserID) + if err != nil { + return nil, err + } + ids, err := s.roles.ListMenuIDsByRoles(ctx, rids) + if err != nil { + return nil, err + } + m := make(map[string]struct{}, len(ids)) + for _, mid := range ids { + m[mid] = struct{}{} + } + return m, nil +} + +func (s *roleService) assertMenuSubset(ctx context.Context, grantorUserID *string, menuIDs []string) error { + if grantorUserID == nil || *grantorUserID == "" { + return nil + } + allowed, err := s.grantorMenuSet(ctx, *grantorUserID) + if err != nil { + return err + } + for _, mid := range menuIDs { + if _, ok := allowed[mid]; !ok { + return fmt.Errorf("防越权: 不能分配自身未拥有的菜单权限 (menu_id=%s)", mid) + } + } + return nil +} + +func (s *roleService) Create(ctx context.Context, tenantID string, req *CreateRoleRequest, grantorUserID *string) (*entity.Role, error) { + ok, err := s.roles.ExistsCode(ctx, tenantID, req.RoleCode, "") + if err != nil { + return nil, err + } + if ok { + return nil, fmt.Errorf("角色编码已存在") + } + if err := s.assertMenuSubset(ctx, grantorUserID, req.MenuIDs); err != nil { + return nil, err + } + r := &entity.Role{ + ID: id.New(), + TenantID: tenantID, + RoleCode: req.RoleCode, + RoleName: req.RoleName, + DataScope: req.DataScope, + Description: req.Description, + Status: 1, + } + if err := s.roles.Create(ctx, r); err != nil { + return nil, err + } + if len(req.MenuIDs) > 0 { + if err := s.roles.ReplaceRoleMenus(ctx, r.ID, req.MenuIDs); err != nil { + return nil, err + } + } + return r, nil +} + +func (s *roleService) Update(ctx context.Context, tenantID string, rid string, req *UpdateRoleRequest, grantorUserID *string) (*entity.Role, error) { + r, err := s.roles.GetByID(ctx, rid) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + return nil, fmt.Errorf("角色不存在") + } + return nil, err + } + if r.TenantID != tenantID { + return nil, fmt.Errorf("角色不属于当前租户") + } + if r.IsBuiltin { + // 内置角色仅允许改部分字段(MVP:允许改名称与数据范围与菜单需业务再定) + } + if req.RoleName != nil { + r.RoleName = *req.RoleName + } + if req.DataScope != nil { + r.DataScope = *req.DataScope + } + if req.Description != nil { + r.Description = *req.Description + } + if err := s.roles.Update(ctx, r); err != nil { + return nil, err + } + if req.MenuIDs != nil { + if err := s.assertMenuSubset(ctx, grantorUserID, req.MenuIDs); err != nil { + return nil, err + } + if err := s.roles.ReplaceRoleMenus(ctx, r.ID, req.MenuIDs); err != nil { + return nil, err + } + } + return r, nil +} + +func (s *roleService) Delete(ctx context.Context, tenantID string, ids []string) error { + for _, rid := range ids { + r, err := s.roles.GetByID(ctx, rid) + if err != nil { + return err + } + if r.TenantID != tenantID { + return fmt.Errorf("角色 %s 不属于当前租户", rid) + } + if r.IsBuiltin { + return fmt.Errorf("内置角色不可删除") + } + n, err := s.roles.CountUsers(ctx, rid) + if err != nil { + return err + } + if n > 0 { + return fmt.Errorf("角色仍被用户使用") + } + if err := s.roles.Delete(ctx, rid); err != nil { + return err + } + } + return nil +} + +func (s *roleService) Get(ctx context.Context, tenantID string, rid string) (*entity.Role, error) { + r, err := s.roles.GetByID(ctx, rid) + if err != nil { + return nil, err + } + if r.TenantID != tenantID { + return nil, fmt.Errorf("角色不属于当前租户") + } + return r, nil +} + +func (s *roleService) List(ctx context.Context, tenantID string, name, code string, page, pageSize int) (*RoleListResponse, error) { + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + rows, total, err := s.roles.List(ctx, tenantID, name, code, page, pageSize) + if err != nil { + return nil, err + } + tp := int(total) / pageSize + if int(total)%pageSize != 0 { + tp++ + } + return &RoleListResponse{Items: rows, Total: total, Page: page, PageSize: pageSize, TotalPages: tp}, nil +} + +func (s *roleService) AssignMenus(ctx context.Context, tenantID string, roleID string, menuIDs []string, grantorUserID *string) error { + r, err := s.roles.GetByID(ctx, roleID) + if err != nil { + return err + } + if r.TenantID != tenantID { + return fmt.Errorf("角色不属于当前租户") + } + if err := s.assertMenuSubset(ctx, grantorUserID, menuIDs); err != nil { + return err + } + return s.roles.ReplaceRoleMenus(ctx, roleID, menuIDs) +} diff --git a/internal/iam/service/scope.go b/internal/iam/service/scope.go new file mode 100644 index 0000000..d25562d --- /dev/null +++ b/internal/iam/service/scope.go @@ -0,0 +1,17 @@ +package service + +import "giter.top/smart/internal/iam/entity" + +// MergeDataScope 多角色数据范围并集:取最大(PRD:全部 > 本部门及子部门 > 本部门 > 仅本人) +func MergeDataScope(scopes []int16) int16 { + var m int16 + for _, s := range scopes { + if s > m { + m = s + } + } + if m == 0 { + return entity.DataScopeSelf + } + return m +} diff --git a/internal/iam/service/tenant_service.go b/internal/iam/service/tenant_service.go new file mode 100644 index 0000000..18fbc9f --- /dev/null +++ b/internal/iam/service/tenant_service.go @@ -0,0 +1,261 @@ +package service + +import ( + "context" + "errors" + "fmt" + "time" + + "giter.top/smart/internal/iam/entity" + "giter.top/smart/internal/iam/repository" + "giter.top/smart/pkg/utils/id" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +// TenantService 租户 +type TenantService interface { + Create(ctx context.Context, req *CreateTenantRequest) (*entity.Tenant, error) + Update(ctx context.Context, id string, req *UpdateTenantRequest) (*entity.Tenant, error) + Delete(ctx context.Context, ids []string) error + Get(ctx context.Context, id string) (*entity.Tenant, error) + List(ctx context.Context, name, code string, status *int16, page, pageSize int) (*TenantListResponse, error) +} + +type CreateTenantRequest struct { + TenantCode string `json:"tenant_code" binding:"required,max=64"` + TenantName string `json:"tenant_name" binding:"required,max=128"` + AdminUserName string `json:"admin_user_name" binding:"required,max=64"` + AdminPassword string `json:"admin_password" binding:"required,min=6,max=64"` + AdminRealName string `json:"admin_real_name" binding:"max=64"` +} + +type UpdateTenantRequest struct { + TenantName *string `json:"tenant_name"` + TenantCode *string `json:"tenant_code" binding:"omitempty,max=64"` + Status *int16 `json:"status"` + ExpireTime *string `json:"expire_time"` // RFC3339 +} + +type TenantListItem struct { + entity.Tenant + UserCount int64 `json:"user_count"` + DeptCount int64 `json:"dept_count"` +} + +type TenantListResponse struct { + Items []TenantListItem `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` +} + +type tenantService struct { + db *gorm.DB + tenants repository.TenantRepository + depts repository.DeptRepository + users repository.UserRepository + roles repository.RoleRepository + menus repository.MenuRepository +} + +func NewTenantService( + db *gorm.DB, + tenants repository.TenantRepository, + depts repository.DeptRepository, + users repository.UserRepository, + roles repository.RoleRepository, + menus repository.MenuRepository, +) TenantService { + return &tenantService{db: db, tenants: tenants, depts: depts, users: users, roles: roles, menus: menus} +} + +func (s *tenantService) Create(ctx context.Context, req *CreateTenantRequest) (*entity.Tenant, error) { + ok, err := s.tenants.ExistsCode(ctx, req.TenantCode, "") + if err != nil { + return nil, err + } + if ok { + return nil, fmt.Errorf("租户编码已存在") + } + hash, err := bcrypt.GenerateFromPassword([]byte(req.AdminPassword), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + var out *entity.Tenant + err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + t := &entity.Tenant{ + ID: id.New(), + TenantCode: req.TenantCode, + TenantName: req.TenantName, + Status: 1, + } + if err := tx.Create(t).Error; err != nil { + return err + } + var ucount int64 + if err := tx.Model(&entity.User{}).Where("tenant_id = ? AND user_name = ?", t.ID, req.AdminUserName).Count(&ucount).Error; err != nil { + return err + } + if ucount > 0 { + return fmt.Errorf("管理员账号已存在") + } + root := &entity.Dept{ + ID: id.New(), + TenantID: t.ID, + ParentID: "", + DeptName: req.TenantName, + SortOrder: 0, + Status: 1, + } + if err := tx.Create(root).Error; err != nil { + return err + } + path := fmt.Sprintf("/%s/", root.ID) + if err := tx.Model(&entity.Dept{}).Where("id = ?", root.ID).Update("dept_path", path).Error; err != nil { + return err + } + admin := &entity.User{ + ID: id.New(), + TenantID: t.ID, + DeptID: &root.ID, + UserName: req.AdminUserName, + RealName: req.AdminRealName, + PasswordHash: string(hash), + Status: 1, + } + if err := tx.Create(admin).Error; err != nil { + return err + } + if err := tx.Create(&entity.UserDept{ID: id.New(), UserID: admin.ID, DeptID: root.ID, IsPrimary: true}).Error; err != nil { + return err + } + role := &entity.Role{ + ID: id.New(), + TenantID: t.ID, + RoleCode: DefaultTenantAdminRoleCode, + RoleName: "超级管理员", + DataScope: entity.DataScopeAll, + Description: "租户初始化角色", + IsBuiltin: true, + Status: 1, + } + if err := tx.Create(role).Error; err != nil { + return err + } + var allMenus []entity.Menu + if err := tx.Find(&allMenus).Error; err != nil { + return err + } + for _, m := range allMenus { + if err := tx.Create(&entity.RoleMenu{ID: id.New(), RoleID: role.ID, MenuID: m.ID}).Error; err != nil { + return err + } + } + if err := tx.Create(&entity.UserRole{ID: id.New(), UserID: admin.ID, RoleID: role.ID}).Error; err != nil { + return err + } + aid := admin.ID + if err := tx.Model(t).Update("admin_user_id", aid).Error; err != nil { + return err + } + t.AdminUserID = &aid + out = t + return nil + }) + if err != nil { + return nil, err + } + return out, nil +} + +func (s *tenantService) Update(ctx context.Context, id string, req *UpdateTenantRequest) (*entity.Tenant, error) { + t, err := s.tenants.GetByID(ctx, id) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + return nil, fmt.Errorf("租户不存在") + } + return nil, err + } + if req.TenantName != nil && *req.TenantName != "" { + t.TenantName = *req.TenantName + if root, err := s.depts.FindRoot(ctx, t.ID); err == nil { + root.DeptName = *req.TenantName + _ = s.depts.Update(ctx, root) + } + } + if req.TenantCode != nil && *req.TenantCode != "" { + ok, err := s.tenants.ExistsCode(ctx, *req.TenantCode, id) + if err != nil { + return nil, err + } + if ok { + return nil, fmt.Errorf("租户编码已存在") + } + t.TenantCode = *req.TenantCode + } + if req.Status != nil { + t.Status = *req.Status + } + if req.ExpireTime != nil && *req.ExpireTime != "" { + et, err := time.Parse(time.RFC3339, *req.ExpireTime) + if err != nil { + return nil, fmt.Errorf("到期时间格式无效: %w", err) + } + t.ExpireTime = &et + if et.Before(time.Now()) { + t.Status = 0 + } + } + if err := s.tenants.Update(ctx, t); err != nil { + return nil, err + } + return t, nil +} + +func (s *tenantService) Delete(ctx context.Context, ids []string) error { + for _, tid := range ids { + n, err := s.tenants.CountUsers(ctx, tid) + if err != nil { + return err + } + if n > 0 { + return fmt.Errorf("租户 %s 仍存在用户,无法删除", tid) + } + } + for _, tid := range ids { + if err := s.db.WithContext(ctx).Delete(&entity.Tenant{}, "id = ?", tid).Error; err != nil { + return err + } + } + return nil +} + +func (s *tenantService) Get(ctx context.Context, id string) (*entity.Tenant, error) { + return s.tenants.GetByID(ctx, id) +} + +func (s *tenantService) List(ctx context.Context, name, code string, status *int16, page, pageSize int) (*TenantListResponse, error) { + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + rows, total, err := s.tenants.List(ctx, name, code, status, page, pageSize) + if err != nil { + return nil, err + } + items := make([]TenantListItem, 0, len(rows)) + for _, t := range rows { + uc, _ := s.tenants.CountUsers(ctx, t.ID) + dc, _ := s.tenants.CountDepts(ctx, t.ID) + items = append(items, TenantListItem{Tenant: t, UserCount: uc, DeptCount: dc}) + } + tp := int(total) / pageSize + if int(total)%pageSize != 0 { + tp++ + } + return &TenantListResponse{Items: items, Total: total, Page: page, PageSize: pageSize, TotalPages: tp}, nil +} diff --git a/internal/iam/service/user_service.go b/internal/iam/service/user_service.go new file mode 100644 index 0000000..89211b2 --- /dev/null +++ b/internal/iam/service/user_service.go @@ -0,0 +1,240 @@ +package service + +import ( + "context" + "errors" + "fmt" + + "giter.top/smart/internal/iam/entity" + "giter.top/smart/internal/iam/repository" + "giter.top/smart/pkg/utils/id" + "golang.org/x/crypto/bcrypt" +) + +// UserService 用户 +type UserService interface { + Create(ctx context.Context, tenantID string, req *CreateUserRequest) (*entity.User, error) + Update(ctx context.Context, tenantID string, uid string, req *UpdateUserRequest) (*entity.User, error) + Delete(ctx context.Context, tenantID string, ids []string) error + Get(ctx context.Context, tenantID string, uid string) (*entity.User, error) + List(ctx context.Context, tenantID string, q *UserListQuery) (*UserListResponse, error) + DataScopeForUser(ctx context.Context, userID string) (int16, error) +} + +type CreateUserRequest struct { + UserName string `json:"user_name" binding:"required,max=64"` + Password string `json:"password" binding:"required,min=6,max=64"` + RealName string `json:"real_name" binding:"max=64"` + Phone string `json:"phone"` + Email string `json:"email"` + DeptID *string `json:"dept_id"` + DeptIDs []string `json:"dept_ids"` + RoleIDs []string `json:"role_ids"` +} + +type UpdateUserRequest struct { + RealName *string `json:"real_name"` + Phone *string `json:"phone"` + Email *string `json:"email"` + DeptID *string `json:"dept_id"` + DeptIDs []string `json:"dept_ids"` + RoleIDs []string `json:"role_ids"` + Status *int16 `json:"status"` + Password *string `json:"password"` +} + +type UserListQuery struct { + DeptID *string + RoleID *string + Keyword string + Status *int16 + Page int + PageSize int +} + +type UserListResponse struct { + Items []entity.User `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` +} + +type userService struct { + users repository.UserRepository + roles repository.RoleRepository +} + +func NewUserService(users repository.UserRepository, roles repository.RoleRepository) UserService { + return &userService{users: users, roles: roles} +} + +func (s *userService) Create(ctx context.Context, tenantID string, req *CreateUserRequest) (*entity.User, error) { + ok, err := s.users.ExistsUserName(ctx, tenantID, req.UserName, "") + if err != nil { + return nil, err + } + if ok { + return nil, fmt.Errorf("账号已存在") + } + hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + u := &entity.User{ + ID: id.New(), + TenantID: tenantID, + UserName: req.UserName, + RealName: req.RealName, + Phone: req.Phone, + Email: req.Email, + PasswordHash: string(hash), + Status: 1, + } + if req.DeptID != nil { + u.DeptID = req.DeptID + } + if err := s.users.Create(ctx, u); err != nil { + return nil, err + } + depts := req.DeptIDs + primary := "" + if req.DeptID != nil { + primary = *req.DeptID + } + if len(depts) == 0 && primary != "" { + depts = []string{primary} + } + if len(depts) > 0 { + if primary == "" { + primary = depts[0] + } + u.DeptID = &primary + _ = s.users.Update(ctx, u) + if err := s.users.ReplaceUserDepts(ctx, u.ID, primary, depts); err != nil { + return nil, err + } + } + if len(req.RoleIDs) > 0 { + if err := s.users.ReplaceUserRoles(ctx, u.ID, req.RoleIDs); err != nil { + return nil, err + } + } + return u, nil +} + +func (s *userService) Update(ctx context.Context, tenantID string, uid string, req *UpdateUserRequest) (*entity.User, error) { + u, err := s.users.GetByID(ctx, uid) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + return nil, fmt.Errorf("用户不存在") + } + return nil, err + } + if u.TenantID != tenantID { + return nil, fmt.Errorf("用户不属于当前租户") + } + if req.RealName != nil { + u.RealName = *req.RealName + } + if req.Phone != nil { + u.Phone = *req.Phone + } + if req.Email != nil { + u.Email = *req.Email + } + if req.Status != nil { + u.Status = *req.Status + } + if req.Password != nil && *req.Password != "" { + hash, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + u.PasswordHash = string(hash) + } + if err := s.users.Update(ctx, u); err != nil { + return nil, err + } + if req.DeptIDs != nil || req.DeptID != nil { + depts := req.DeptIDs + primary := "" + if req.DeptID != nil { + primary = *req.DeptID + u.DeptID = req.DeptID + _ = s.users.Update(ctx, u) + } + if len(depts) == 0 && primary != "" { + depts = []string{primary} + } + if primary == "" && len(depts) > 0 { + primary = depts[0] + } + if err := s.users.ReplaceUserDepts(ctx, u.ID, primary, depts); err != nil { + return nil, err + } + } + if req.RoleIDs != nil { + if err := s.users.ReplaceUserRoles(ctx, u.ID, req.RoleIDs); err != nil { + return nil, err + } + } + return u, nil +} + +func (s *userService) Delete(ctx context.Context, tenantID string, ids []string) error { + for _, uid := range ids { + u, err := s.users.GetByID(ctx, uid) + if err != nil { + return err + } + if u.TenantID != tenantID { + return fmt.Errorf("用户 %s 不属于当前租户", uid) + } + if err := s.users.Delete(ctx, uid); err != nil { + return err + } + } + return nil +} + +func (s *userService) Get(ctx context.Context, tenantID string, uid string) (*entity.User, error) { + u, err := s.users.GetByID(ctx, uid) + if err != nil { + return nil, err + } + if u.TenantID != tenantID { + return nil, fmt.Errorf("用户不属于当前租户") + } + return u, nil +} + +func (s *userService) List(ctx context.Context, tenantID string, q *UserListQuery) (*UserListResponse, error) { + if q.Page <= 0 { + q.Page = 1 + } + if q.PageSize <= 0 { + q.PageSize = 10 + } + rows, total, err := s.users.List(ctx, tenantID, q.DeptID, q.RoleID, q.Keyword, q.Status, q.Page, q.PageSize) + if err != nil { + return nil, err + } + tp := int(total) / q.PageSize + if int(total)%q.PageSize != 0 { + tp++ + } + return &UserListResponse{Items: rows, Total: total, Page: q.Page, PageSize: q.PageSize, TotalPages: tp}, nil +} + +func (s *userService) DataScopeForUser(ctx context.Context, userID string) (int16, error) { + roles, err := s.roles.ListRolesByUser(ctx, userID) + if err != nil { + return 0, err + } + scopes := make([]int16, 0, len(roles)) + for _, r := range roles { + scopes = append(scopes, r.DataScope) + } + return MergeDataScope(scopes), nil +} diff --git a/internal/iam/wire_provider.go b/internal/iam/wire_provider.go new file mode 100644 index 0000000..9556d4c --- /dev/null +++ b/internal/iam/wire_provider.go @@ -0,0 +1,45 @@ +package iam + +import ( + "giter.top/smart/internal/iam/handler" + "giter.top/smart/internal/iam/repository" + "giter.top/smart/internal/iam/service" + "github.com/google/wire" +) + +// HandlerProviderSet 处理程序提供者集合 +var handlerProviderSet = wire.NewSet( + handler.NewTenantHandler, + handler.NewDeptHandler, + handler.NewRoleHandler, + handler.NewUserHandler, + handler.NewMenuHandler, +) + + +// ServiceProviderSet 服务提供者集合 +var serviceProviderSet = wire.NewSet( + service.NewTenantService, + service.NewDeptService, + service.NewRoleService, + service.NewUserService, + service.NewMenuService, +) + + +// RepositoryProviderSet 仓库提供者集合 +var repositoryProviderSet = wire.NewSet( + repository.NewTenantRepository, + repository.NewDeptRepository, + repository.NewRoleRepository, + repository.NewUserRepository, + repository.NewMenuRepository, +) + +var ProviderSet = wire.NewSet( + handlerProviderSet, + serviceProviderSet, + repositoryProviderSet, + // 路由注册 + NewIamRoutes, +) \ No newline at end of file diff --git a/internal/server/cors.go b/internal/server/cors.go new file mode 100644 index 0000000..d83d216 --- /dev/null +++ b/internal/server/cors.go @@ -0,0 +1,32 @@ +package server + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +// corsLocalDev 允许本机前端(localhost / 127.0.0.1 任意端口)跨域访问 API 与 OAuth;生产同域部署时可关闭或改为配置白名单。 +func corsLocalDev() gin.HandlerFunc { + return func(c *gin.Context) { + o := c.GetHeader("Origin") + if o != "" && isLocalDevOrigin(o) { + c.Writer.Header().Set("Access-Control-Allow-Origin", o) + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Tenant-ID, X-User-ID, X-Grantor-User-ID") + c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + } + if c.Request.Method == http.MethodOptions { + c.AbortWithStatus(http.StatusNoContent) + return + } + c.Next() + } +} + +func isLocalDevOrigin(o string) bool { + return strings.HasPrefix(o, "http://localhost:") || + strings.HasPrefix(o, "http://127.0.0.1:") || + strings.HasPrefix(o, "http://[::1]:") +} diff --git a/internal/server/grpc.go b/internal/server/grpc.go new file mode 100644 index 0000000..545d23e --- /dev/null +++ b/internal/server/grpc.go @@ -0,0 +1,26 @@ +package server + +import ( + "time" + + "giter.top/smart/pkg/config" +) + +type GrpcServer struct { + addr string + timeout time.Duration +} + +func NewGrpcServer(cfg *config.Config) *GrpcServer { + return &GrpcServer{ + addr: cfg.Server.Grpc.Addr, + timeout: cfg.Server.Grpc.Timeout, + } +} +func (s *GrpcServer) Run() error { + return nil +} + +func (s *GrpcServer) Stop() error { + return nil +} \ No newline at end of file diff --git a/internal/server/http.go b/internal/server/http.go new file mode 100644 index 0000000..0fc013d --- /dev/null +++ b/internal/server/http.go @@ -0,0 +1,67 @@ +package server + +import ( + "time" + + "giter.top/smart/internal/auth" + "giter.top/smart/internal/iam" + "giter.top/smart/internal/system" + "giter.top/smart/pkg/config" + "github.com/gin-gonic/gin" +) + +type HttpServer struct { + addr string + timeout time.Duration + engine *gin.Engine +} + +func NewHttpServer(cfg *config.Config, + engine *gin.Engine, + ) *HttpServer { + return &HttpServer{ + addr: cfg.Server.Http.Addr, + timeout: cfg.Server.Http.Timeout, + engine: engine, + } +} +func (s *HttpServer) Run() error { + s.engine.Run(s.addr) + return nil +} + +func (s *HttpServer) Stop() error { + return nil +} + +///////////////////////////////////////////////////////////// +type HttpRoutes interface { + Register(engine *gin.Engine , apiGroup *gin.RouterGroup) +} + +func NewHttpEngine(cfg *config.Config,httpRoutes []HttpRoutes) *gin.Engine { + engine := gin.Default() + engine.Use(corsLocalDev()) + // 健康检查端点,供负载均衡或编排探活。 + engine.GET("/health", func(c *gin.Context) { + c.JSON(200, gin.H{"status": "ok"}) + }) + // 处理注册的路由 + apiGroup := engine.Group("/api/v1") + for _, r := range httpRoutes { + r.Register(engine, apiGroup) + } + return engine +} + +func NewHttpRouteRegistrars( + authRoutes *auth.AuthRoutes, + systemRoutes *system.SystemRoutes, + iamRoutes *iam.IamRoutes, +) []HttpRoutes { + return []HttpRoutes{ + authRoutes, + systemRoutes, + iamRoutes, + } +} \ No newline at end of file diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..87f4127 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,25 @@ +package server + +import ( + "giter.top/smart/pkg/config" + "github.com/gin-gonic/gin" + "github.com/google/wire" +) + +var ProviderSet = wire.NewSet( + NewHttpEngine, + ProvideServers, + NewHttpRouteRegistrars, +) + +type Server interface { + Run() error + Stop() error +} + +func ProvideServers(cfg *config.Config, engine *gin.Engine) []Server { + return []Server{ + NewHttpServer(cfg, engine), + NewGrpcServer(cfg), + } +} \ No newline at end of file diff --git a/internal/system/entity/param_entity.go b/internal/system/entity/param_entity.go new file mode 100644 index 0000000..f90ea00 --- /dev/null +++ b/internal/system/entity/param_entity.go @@ -0,0 +1,96 @@ +package entity + +import ( + "time" +) + +// SystemParam 系统参数实体 +// 用于存储系统运行所需的各种配置参数,支持多种数据类型和分组管理 +type SystemParam struct { + // ID 主键,使用 UUID v4 保证全局唯一性,避免自增 ID 带来的信息泄露风险 + ID string `json:"id" gorm:"column:id;type:varchar(36);primaryKey;not null;comment:主键"` + + // ParamKey 参数键名,全局唯一,用于标识和访问参数值 + // 命名规范:小写字母 + 下划线,如:site_name, max_upload_size + ParamKey string `json:"param_key" gorm:"column:param_key;type:varchar(100);uniqueIndex;not null;comment:参数键"` + + // ParamValue 参数值,存储实际配置内容 + // 根据 ParamType 不同,可能是字符串、数字、布尔值或 JSON 数组 + ParamValue string `json:"param_value" gorm:"column:param_value;type:varchar(1000);not null;comment:参数值"` + + // ParamType 参数类型,决定参数的校验规则和展示方式 + // 可选值:text(文本), number(数字), boolean(布尔), select(下拉选择) + ParamType string `json:"param_type" gorm:"column:param_type;type:varchar(20);not null;default:'text';comment:类型:text,number,boolean,select"` + + // ParamGroup 参数分组,用于对参数进行逻辑分组管理 + // 常见分组:basic(基础), security(安全), business(业务), system(系统) + ParamGroup string `json:"param_group" gorm:"column:param_group;type:varchar(50);not null;default:'default';comment:分组"` + + // ParamDesc 参数描述,说明该参数的用途、取值范围、默认值等信息 + // 建议包含:参数说明、可选值说明、修改影响等 + ParamDesc string `json:"param_desc" gorm:"column:param_desc;type:varchar(500);comment:描述"` + + // CreatorID 创建人 ID,记录创建该参数的用户标识 + // 用于审计追踪,定位参数创建者 + CreatorID string `json:"creator_id" gorm:"column:creator_id;type:varchar(36);not null;default:'';comment:创建人 ID"` + + // CreateTime 创建时间,记录参数创建的时间点 + // 使用指针类型,可以区分"未设置"和"已设置"状态 + // 数据库层面使用 CURRENT_TIMESTAMP 自动填充 + CreateTime *time.Time `json:"create_time" gorm:"column:create_time;type:datetime;default:current_timestamp;comment:创建时间"` + + // LastUpdaterID 最后更新人 ID,记录最后一次修改该参数的用户标识 + // 用于审计追踪,定位参数修改者 + LastUpdaterID string `json:"last_updater_id" gorm:"column:last_updater_id;type:varchar(36);not null;default:'';comment:最后更新人 ID"` + + // UpdateTime 最后更新时间,记录参数最后一次修改的时间点 + // 使用指针类型,可以区分"未设置"和"已设置"状态 + // 数据库层面使用 ON UPDATE CURRENT_TIMESTAMP 自动更新 + UpdateTime *time.Time `json:"update_time" gorm:"column:update_time;type:datetime;default:current_timestamp;on update current_timestamp;comment:最后更新时间"` +} + +// TableName 指定表名为 system_param +// 遵循数据库命名规范:小写字母 + 下划线,复数形式 +func (SystemParam) TableName() string { + return "system_param" +} + +// ParamType 参数类型常量 +// 定义系统支持的参数类型,用于前端展示和后端校验 +type ParamType string + +const ( + // ParamTypeText 文本类型,适用于字符串值 + ParamTypeText ParamType = "text" + // ParamTypeNumber 数字类型,适用于整数值 + ParamTypeNumber ParamType = "number" + // ParamTypeBoolean 布尔类型,适用于 true/false 值 + ParamTypeBoolean ParamType = "boolean" + // ParamTypeSelect 下拉选择类型,适用于预定义选项值 + ParamTypeSelect ParamType = "select" +) + +// ParamGroup 参数分组常量 +// 定义系统参数的逻辑分组,便于分类管理和权限控制 +type ParamGroup string + +const ( + // GroupBasic 基础配置分组,包含系统基本信息 + // 如:站点名称、Logo、联系方式等 + GroupBasic ParamGroup = "basic" + + // GroupSecurity 安全配置分组,包含安全相关参数 + // 如:密码策略、登录限制、Token 有效期等 + GroupSecurity ParamGroup = "security" + + // GroupBusiness 业务配置分组,包含业务逻辑相关参数 + // 如:订单配置、支付参数、业务开关等 + GroupBusiness ParamGroup = "business" + + // GroupSystem 系统配置分组,包含系统运行参数 + // 如:缓存配置、日志级别、性能参数等 + GroupSystem ParamGroup = "system" + + // GroupDefault 默认分组,未明确分组的参数归入此类 + GroupDefault ParamGroup = "default" +) diff --git a/internal/system/handler/param_handler.go b/internal/system/handler/param_handler.go new file mode 100644 index 0000000..f051d5b --- /dev/null +++ b/internal/system/handler/param_handler.go @@ -0,0 +1,177 @@ +package handler + +import ( + "net/http" + "strconv" + + "giter.top/smart/internal/system/service" + "github.com/gin-gonic/gin" +) + +// ParamHandler 系统参数 HTTP 处理器 +type ParamHandler struct { + service service.ParamService +} + +// NewParamHandler 创建参数处理器实例 +func NewParamHandler(svc service.ParamService) *ParamHandler { + return &ParamHandler{service: svc} +} + +// CreateParam 创建系统参数 +// @Summary 创建系统参数 +// @Tags 系统参数 +// @Accept json +// @Produce json +// @Param request body service.CreateParamRequest true "创建参数请求" +// @Success 201 {object} entity.SystemParam +// @Router /api/v1/system/params [post] +func (h *ParamHandler) CreateParam(c *gin.Context) { + var req service.CreateParamRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // TODO: 从上下文获取用户 ID(实际项目中从 JWT token 解析) + creatorID := "system" + param, err := h.service.CreateParam(c.Request.Context(), &req, creatorID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, param) +} + +// UpdateParam 更新系统参数 +// @Summary 更新系统参数 +// @Tags 系统参数 +// @Accept json +// @Produce json +// @Param id path string true "参数 ID" +// @Param request body service.UpdateParamRequest true "更新参数请求" +// @Success 200 {object} entity.SystemParam +// @Router /api/v1/system/params/{id} [put] +func (h *ParamHandler) UpdateParam(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"}) + return + } + + var req service.UpdateParamRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // TODO: 从上下文获取用户 ID + lastUpdaterID := "system" + param, err := h.service.UpdateParam(c.Request.Context(), id, &req, lastUpdaterID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, param) +} + +// DeleteParams 批量删除系统参数 +// @Summary 批量删除系统参数 +// @Tags 系统参数 +// @Accept json +// @Produce json +// @Param request body []string true "参数 ID 列表" +// @Success 204 +// @Router /api/v1/system/params/batch [delete] +func (h *ParamHandler) DeleteParams(c *gin.Context) { + var ids []string + if err := c.ShouldBindJSON(&ids); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.service.DeleteParams(c.Request.Context(), ids); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.Status(http.StatusNoContent) +} + +// GetParam 获取单个系统参数 +// @Summary 获取单个系统参数 +// @Tags 系统参数 +// @Produce json +// @Param id path string true "参数 ID" +// @Success 200 {object} entity.SystemParam +// @Router /api/v1/system/params/{id} [get] +func (h *ParamHandler) GetParam(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"}) + return + } + + param, err := h.service.GetParam(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, param) +} + +// GetParamByKey 根据键获取系统参数 +// @Summary 根据键获取系统参数 +// @Tags 系统参数 +// @Produce json +// @Param key path string true "参数键" +// @Success 200 {object} entity.SystemParam +// @Router /api/v1/system/params/key/{key} [get] +func (h *ParamHandler) GetParamByKey(c *gin.Context) { + key := c.Param("key") + param, err := h.service.GetParamByKey(c.Request.Context(), key) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, param) +} + +// ListParams 获取系统参数列表 +// @Summary 获取系统参数列表 +// @Tags 系统参数 +// @Produce json +// @Param group query string false "分组" +// @Param param_key query string false "参数键(模糊搜索)" +// @Param page query int false "页码" default(1) +// @Param page_size query int false "每页数量" default(10) +// @Success 200 {object} service.ParamListResponse +// @Router /api/v1/system/params [get] +func (h *ParamHandler) ListParams(c *gin.Context) { + group := c.Query("group") + paramKey := c.Query("param_key") + + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) + + response, err := h.service.ListParams(c.Request.Context(), group, paramKey, page, pageSize) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, response) +} + +// GetAllParams 获取所有系统参数 +// @Summary 获取所有系统参数 +// @Tags 系统参数 +// @Produce json +// @Success 200 {object} map[string]entity.SystemParam +// @Router /api/v1/system/params/all [get] +func (h *ParamHandler) GetAllParams(c *gin.Context) { + params, err := h.service.GetAllParams(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, params) +} diff --git a/internal/system/http_register.go b/internal/system/http_register.go new file mode 100644 index 0000000..f1ec566 --- /dev/null +++ b/internal/system/http_register.go @@ -0,0 +1,33 @@ +package system + +import ( + "giter.top/smart/internal/system/handler" + "github.com/gin-gonic/gin" +) + +// SystemRoutes 注册 system 模块的 HTTP 路由。 +type SystemRoutes struct { + paramHandler *handler.ParamHandler +} +// NewSystemRoutes 构造 system 模块的路由注册器,由 Wire 注入。 +func NewSystemRoutes( paramHandler *handler.ParamHandler) *SystemRoutes { + return &SystemRoutes{ + paramHandler: paramHandler, + } +} +// TODO 添加注册信息 +func (s *SystemRoutes) Register(engine *gin.Engine, apiGroup *gin.RouterGroup) { + group := apiGroup.Group("/system") + s.registerParamRoutes(group) +} +// 系统参数路由 +func (s *SystemRoutes) registerParamRoutes(group *gin.RouterGroup) { + paramGroup := group.Group("/param") + { + paramGroup.POST("/create", s.paramHandler.CreateParam) + paramGroup.PUT("/update", s.paramHandler.UpdateParam) + paramGroup.DELETE("/delete-batch", s.paramHandler.DeleteParams) + paramGroup.GET("/get", s.paramHandler.GetParam) + paramGroup.GET("/list", s.paramHandler.ListParams) + } +} \ No newline at end of file diff --git a/internal/system/repository/param_repository.go b/internal/system/repository/param_repository.go new file mode 100644 index 0000000..cd72b72 --- /dev/null +++ b/internal/system/repository/param_repository.go @@ -0,0 +1,139 @@ +package repository + +import ( + "context" + "errors" + + "giter.top/smart/internal/system/entity" + "gorm.io/gorm" +) + +// ErrNotFound 记录未找到 +var ErrNotFound = errors.New("param not found") + +// ParamRepository 系统参数数据访问层 +type ParamRepository interface { + // Create 创建系统参数 + Create(ctx context.Context, param *entity.SystemParam) error + // Update 更新系统参数 + Update(ctx context.Context, param *entity.SystemParam) error + // Delete 删除系统参数 + Delete(ctx context.Context, id string) error + // DeleteBatch 批量删除 + DeleteBatch(ctx context.Context, ids []string) error + // GetByID 根据 ID 获取 + GetByID(ctx context.Context, id string) (*entity.SystemParam, error) + // GetByKey 根据键获取 + GetByKey(ctx context.Context, key string) (*entity.SystemParam, error) + // List 获取列表(支持分页和筛选) + List(ctx context.Context, group string, paramKey string, page, pageSize int) ([]entity.SystemParam, int64, error) + // GetAll 获取所有参数(用于缓存) + GetAll(ctx context.Context) (map[string]entity.SystemParam, error) + // ExistsByKey 检查键是否存在(排除指定 ID) + ExistsByKey(ctx context.Context, key string, excludeID string) (bool, error) +} + +type paramRepository struct { + db *gorm.DB +} + +// NewParamRepository 创建参数仓库实例 +func NewParamRepository(db *gorm.DB) ParamRepository { + return ¶mRepository{db: db} +} + +func (r *paramRepository) Create(ctx context.Context, param *entity.SystemParam) error { + return r.db.WithContext(ctx).Create(param).Error +} + +func (r *paramRepository) Update(ctx context.Context, param *entity.SystemParam) error { + return r.db.WithContext(ctx).Save(param).Error +} + +func (r *paramRepository) Delete(ctx context.Context, id string) error { + return r.db.WithContext(ctx).Where("id = ?", id).Delete(&entity.SystemParam{}).Error +} + +func (r *paramRepository) DeleteBatch(ctx context.Context, ids []string) error { + return r.db.WithContext(ctx).Where("id IN ?", ids).Delete(&entity.SystemParam{}).Error +} + +func (r *paramRepository) GetByID(ctx context.Context, id string) (*entity.SystemParam, error) { + var param entity.SystemParam + err := r.db.WithContext(ctx).Where("id = ?", id).First(¶m).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } + return nil, err + } + return ¶m, nil +} + +func (r *paramRepository) GetByKey(ctx context.Context, key string) (*entity.SystemParam, error) { + var param entity.SystemParam + err := r.db.WithContext(ctx).Where("param_key = ?", key).First(¶m).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } + return nil, err + } + return ¶m, nil +} + +func (r *paramRepository) List(ctx context.Context, group string, paramKey string, page, pageSize int) ([]entity.SystemParam, int64, error) { + var params []entity.SystemParam + var total int64 + query := r.db.WithContext(ctx).Model(&entity.SystemParam{}) + + // 应用筛选条件 + if group != "" { + query = query.Where("param_group = ?", group) + } + if paramKey != "" { + query = query.Where("param_key LIKE ?", "%"+paramKey+"%") + } + + // 获取总数 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 分页查询 + offset := (page - 1) * pageSize + if offset < 0 { + offset = 0 + } + if pageSize <= 0 { + pageSize = 10 + } + err := query.Order("id DESC").Offset(offset).Limit(pageSize).Find(¶ms).Error + if err != nil { + return nil, 0, err + } + return params, total, nil +} + +func (r *paramRepository) GetAll(ctx context.Context) (map[string]entity.SystemParam, error) { + var params []entity.SystemParam + err := r.db.WithContext(ctx).Find(¶ms).Error + if err != nil { + return nil, err + } + result := make(map[string]entity.SystemParam, len(params)) + for _, param := range params { + result[param.ParamKey] = param + } + return result, nil +} + +func (r *paramRepository) ExistsByKey(ctx context.Context, key string, excludeID string) (bool, error) { + query := r.db.WithContext(ctx).Where("param_key = ?", key) + if excludeID != "" { + query = query.Where("id != ?", excludeID) + } + var count int64 + err := query.Model(&entity.SystemParam{}).Count(&count).Error + return count > 0, err +} diff --git a/internal/system/service/param_service.go b/internal/system/service/param_service.go new file mode 100644 index 0000000..4742d64 --- /dev/null +++ b/internal/system/service/param_service.go @@ -0,0 +1,271 @@ +package service + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "giter.top/smart/internal/system/entity" + "giter.top/smart/internal/system/repository" + "giter.top/smart/pkg/utils/id" + "github.com/redis/go-redis/v9" +) + +// ErrInvalidParam 参数无效 +var ErrInvalidParam = errors.New("invalid param") + +// ParamService 系统参数业务逻辑层 +type ParamService interface { + // CreateParam 创建系统参数 + CreateParam(ctx context.Context, req *CreateParamRequest, creatorID string) (*entity.SystemParam, error) + // UpdateParam 更新系统参数 + UpdateParam(ctx context.Context, id string, req *UpdateParamRequest, lastUpdaterID string) (*entity.SystemParam, error) + // DeleteParam 删除系统参数 + DeleteParam(ctx context.Context, id string) error + // DeleteParams 批量删除 + DeleteParams(ctx context.Context, ids []string) error + // GetParam 获取单个参数 + GetParam(ctx context.Context, id string) (*entity.SystemParam, error) + // GetParamByKey 根据键获取参数 + GetParamByKey(ctx context.Context, key string) (*entity.SystemParam, error) + // ListParams 获取参数列表 + ListParams(ctx context.Context, group string, paramKey string, page, pageSize int) (*ParamListResponse, error) + // GetAllParams 获取所有参数(用于缓存) + GetAllParams(ctx context.Context) (map[string]entity.SystemParam, error) + // GetParamValue 获取参数值(便捷方法) + GetParamValue(ctx context.Context, key string) (string, error) + // GetParamValueWithDefault 获取参数值,不存在则返回默认值 + GetParamValueWithDefault(ctx context.Context, key string, defaultValue string) string +} + +// CreateParamRequest 创建参数请求 +type CreateParamRequest struct { + ParamKey string `json:"param_key" binding:"required,max=100"` + ParamValue string `json:"param_value" binding:"required"` + ParamType string `json:"param_type" binding:"required,oneof=text number boolean select"` + ParamGroup string `json:"param_group" binding:"required,max:50"` + ParamDesc string `json:"param_desc" max:"500"` +} + +// UpdateParamRequest 更新参数请求 +type UpdateParamRequest struct { + ParamValue string `json:"param_value"` + ParamType string `json:"param_type" binding:"omitempty,oneof=text number boolean select"` + ParamDesc string `json:"param_desc" max:"500"` +} + +// ParamListResponse 参数列表响应 +type ParamListResponse struct { + Items []entity.SystemParam `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` +} + +type paramService struct { + repo repository.ParamRepository + cache redis.UniversalClient + cacheKey string +} + +// NewParamService 创建参数服务实例(与 cache.NewRedisClient 返回的 redis.UniversalClient 一致,便于 Wire 注入) +func NewParamService(repo repository.ParamRepository, cacheClient redis.UniversalClient) ParamService { + return ¶mService{ + repo: repo, + cache: cacheClient, + cacheKey: "system:params:*", + } +} + +func (s *paramService) CreateParam(ctx context.Context, req *CreateParamRequest, creatorID string) (*entity.SystemParam, error) { + // 生成唯一 ID (UUID v7) + id := id.New() + + // 检查键是否已存在 + exists, err := s.repo.ExistsByKey(ctx, req.ParamKey, "") + if err != nil { + return nil, fmt.Errorf("检查参数键失败:%w", err) + } + if exists { + return nil, fmt.Errorf("参数键 %s 已存在", req.ParamKey) + } + + param := &entity.SystemParam{ + ID: id, + ParamKey: req.ParamKey, + ParamValue: req.ParamValue, + ParamType: req.ParamType, + ParamGroup: req.ParamGroup, + ParamDesc: req.ParamDesc, + CreatorID: creatorID, + LastUpdaterID: creatorID, + } + + if err := s.repo.Create(ctx, param); err != nil { + return nil, fmt.Errorf("创建参数失败:%w", err) + } + + // 刷新缓存 + s.refreshCache(ctx) + return param, nil +} + +func (s *paramService) UpdateParam(ctx context.Context, id string, req *UpdateParamRequest, lastUpdaterID string) (*entity.SystemParam, error) { + // 获取现有参数 + param, err := s.repo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + return nil, fmt.Errorf("参数不存在") + } + return nil, fmt.Errorf("获取参数失败:%w", err) + } + + // 更新字段 + if req.ParamValue != "" { + param.ParamValue = req.ParamValue + } + if req.ParamType != "" { + param.ParamType = req.ParamType + } + if req.ParamDesc != "" { + param.ParamDesc = req.ParamDesc + } + + param.LastUpdaterID = lastUpdaterID + if err := s.repo.Update(ctx, param); err != nil { + return nil, fmt.Errorf("更新参数失败:%w", err) + } + + // 刷新缓存 + s.refreshCache(ctx) + return param, nil +} + +func (s *paramService) DeleteParam(ctx context.Context, id string) error { + if err := s.repo.Delete(ctx, id); err != nil { + return fmt.Errorf("删除参数失败:%w", err) + } + + // 刷新缓存 + s.refreshCache(ctx) + return nil +} + +func (s *paramService) DeleteParams(ctx context.Context, ids []string) error { + if len(ids) == 0 { + return nil + } + if err := s.repo.DeleteBatch(ctx, ids); err != nil { + return fmt.Errorf("批量删除参数失败:%w", err) + } + + // 刷新缓存 + s.refreshCache(ctx) + return nil +} + +func (s *paramService) GetParam(ctx context.Context, id string) (*entity.SystemParam, error) { + param, err := s.repo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + return nil, fmt.Errorf("参数不存在") + } + return nil, err + } + return param, nil +} + +func (s *paramService) GetParamByKey(ctx context.Context, key string) (*entity.SystemParam, error) { + param, err := s.repo.GetByKey(ctx, key) + if err != nil { + if errors.Is(err, repository.ErrNotFound) { + return nil, fmt.Errorf("参数 %s 不存在", key) + } + return nil, err + } + return param, nil +} + +func (s *paramService) ListParams(ctx context.Context, group string, paramKey string, page, pageSize int) (*ParamListResponse, error) { + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + + items, total, err := s.repo.List(ctx, group, paramKey, page, pageSize) + if err != nil { + return nil, fmt.Errorf("获取参数列表失败:%w", err) + } + + totalPages := int(total) / pageSize + if int(total)%pageSize != 0 { + totalPages++ + } + + return &ParamListResponse{ + Items: items, + Total: total, + Page: page, + PageSize: pageSize, + TotalPages: totalPages, + }, nil +} + +func (s *paramService) GetAllParams(ctx context.Context) (map[string]entity.SystemParam, error) { + // 先从缓存获取 + if s.cache != nil { + cached := s.cache.Get(ctx, "system:params:all").Val() + if cached != "" { + var params map[string]entity.SystemParam + if err := json.Unmarshal([]byte(cached), ¶ms); err == nil { + return params, nil + } else { + return nil, fmt.Errorf("解析缓存数据失败:%w", err) + } + } + } + + // 缓存未命中,从数据库获取 + params, err := s.repo.GetAll(ctx) + if err != nil { + return nil, err + } + + // 写入缓存 + if s.cache != nil { + data, _ := json.Marshal(params) + s.cache.Set(ctx, "system:params:all", string(data), 0) // 0 表示永不过期 + } + + return params, nil +} + +func (s *paramService) GetParamValue(ctx context.Context, key string) (string, error) { + param, err := s.GetParamByKey(ctx, key) + if err != nil { + return "", err + } + return param.ParamValue, nil +} + +func (s *paramService) GetParamValueWithDefault(ctx context.Context, key string, defaultValue string) string { + value, err := s.GetParamValue(ctx, key) + if err != nil { + return defaultValue + } + return value +} + +// refreshCache 刷新缓存 +func (s *paramService) refreshCache(ctx context.Context) { + if s.cache == nil { + return + } + + // 删除缓存,让下次请求重新构建 + s.cache.Del(ctx, "system:params:all") +} diff --git a/internal/system/wire_provider.go b/internal/system/wire_provider.go new file mode 100644 index 0000000..e4254d7 --- /dev/null +++ b/internal/system/wire_provider.go @@ -0,0 +1,32 @@ +package system + +import ( + "giter.top/smart/internal/system/handler" + "giter.top/smart/internal/system/repository" + "giter.top/smart/internal/system/service" + "github.com/google/wire" +) + +// HandlerProviderSet 处理程序提供者集合 +var handlerProviderSet = wire.NewSet( + handler.NewParamHandler, +) + + +// ServiceProviderSet 服务提供者集合 +var serviceProviderSet = wire.NewSet( + service.NewParamService, +) + + +// RepositoryProviderSet 仓库提供者集合 +var repositoryProviderSet = wire.NewSet( + repository.NewParamRepository, +) + +var ProviderSet = wire.NewSet( + handlerProviderSet, + serviceProviderSet, + repositoryProviderSet, + NewSystemRoutes, +) \ No newline at end of file diff --git a/migrations/postgres/001_iam.sql b/migrations/postgres/001_iam.sql new file mode 100644 index 0000000..9d7ec17 --- /dev/null +++ b/migrations/postgres/001_iam.sql @@ -0,0 +1,132 @@ +-- IAM 表结构(与 internal/iam/entity 中 GORM 模型一致;PostgreSQL) +-- 执行:psql $DATABASE_URL -f migrations/postgres/001_iam.sql + +BEGIN; + +-- 租户 +CREATE TABLE IF NOT EXISTS iam_tenant ( + id varchar(36) PRIMARY KEY, + tenant_code varchar(64) NOT NULL, + tenant_name varchar(128) NOT NULL, + admin_user_id varchar(36) NULL, + status smallint NOT NULL DEFAULT 1, + expire_time timestamptz NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS uq_iam_tenant_code ON iam_tenant (tenant_code); +CREATE INDEX IF NOT EXISTS idx_iam_tenant_deleted_at ON iam_tenant (deleted_at); + +-- 部门(根部门 parent_id 为空串) +CREATE TABLE IF NOT EXISTS iam_dept ( + id varchar(36) PRIMARY KEY, + tenant_id varchar(36) NOT NULL, + parent_id varchar(36) NOT NULL DEFAULT '', + dept_name varchar(128) NOT NULL, + dept_path text NULL, + leader_id varchar(36) NULL, + sort_order int NOT NULL DEFAULT 0, + status smallint NOT NULL DEFAULT 1, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz NULL +); +CREATE INDEX IF NOT EXISTS idx_dept_tenant ON iam_dept (tenant_id); +CREATE INDEX IF NOT EXISTS idx_dept_parent ON iam_dept (parent_id); +CREATE INDEX IF NOT EXISTS idx_iam_dept_deleted_at ON iam_dept (deleted_at); + +-- 用户 +CREATE TABLE IF NOT EXISTS iam_user ( + id varchar(36) PRIMARY KEY, + tenant_id varchar(36) NOT NULL, + dept_id varchar(36) NULL, + user_name varchar(64) NOT NULL, + real_name varchar(64) NULL, + password_hash varchar(255) NOT NULL, + phone varchar(20) NULL, + email varchar(128) NULL, + avatar varchar(512) NULL, + gender smallint NOT NULL DEFAULT 0, + status smallint NOT NULL DEFAULT 1, + login_attempts int NOT NULL DEFAULT 0, + locked_until timestamptz NULL, + last_login_at timestamptz NULL, + last_login_ip varchar(45) NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz NULL +); +CREATE INDEX IF NOT EXISTS idx_user_tenant ON iam_user (tenant_id); +CREATE INDEX IF NOT EXISTS idx_user_dept ON iam_user (dept_id); +CREATE INDEX IF NOT EXISTS idx_iam_user_deleted_at ON iam_user (deleted_at); + +-- 用户-部门关联 +CREATE TABLE IF NOT EXISTS iam_user_dept ( + id varchar(36) PRIMARY KEY, + user_id varchar(36) NOT NULL, + dept_id varchar(36) NOT NULL, + is_primary boolean NOT NULL DEFAULT false, + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT uk_user_dept UNIQUE (user_id, dept_id) +); + +-- 角色 +CREATE TABLE IF NOT EXISTS iam_role ( + id varchar(36) PRIMARY KEY, + tenant_id varchar(36) NOT NULL, + role_code varchar(64) NOT NULL, + role_name varchar(128) NOT NULL, + data_scope smallint NOT NULL DEFAULT 4, + description varchar(512) NULL, + is_builtin boolean NOT NULL DEFAULT false, + status smallint NOT NULL DEFAULT 1, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz NULL +); +CREATE INDEX IF NOT EXISTS idx_role_tenant ON iam_role (tenant_id); +CREATE INDEX IF NOT EXISTS idx_iam_role_deleted_at ON iam_role (deleted_at); + +-- 菜单(全局) +CREATE TABLE IF NOT EXISTS iam_menu ( + id varchar(36) PRIMARY KEY, + parent_id varchar(36) NOT NULL DEFAULT '', + menu_name varchar(128) NOT NULL, + menu_type smallint NOT NULL, + perms varchar(128) NULL, + path varchar(255) NULL, + component varchar(255) NULL, + icon varchar(64) NULL, + sort_order int NOT NULL DEFAULT 0, + is_visible boolean NOT NULL DEFAULT true, + is_builtin boolean NOT NULL DEFAULT false, + external_link varchar(512) NULL, + status smallint NOT NULL DEFAULT 1, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS uq_iam_menu_perms ON iam_menu (perms); +CREATE INDEX IF NOT EXISTS idx_menu_parent ON iam_menu (parent_id); +CREATE INDEX IF NOT EXISTS idx_iam_menu_deleted_at ON iam_menu (deleted_at); + +-- 用户-角色 +CREATE TABLE IF NOT EXISTS iam_user_role ( + id varchar(36) PRIMARY KEY, + user_id varchar(36) NOT NULL, + role_id varchar(36) NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT uk_user_role UNIQUE (user_id, role_id) +); + +-- 角色-菜单 +CREATE TABLE IF NOT EXISTS iam_role_menu ( + id varchar(36) PRIMARY KEY, + role_id varchar(36) NOT NULL, + menu_id varchar(36) NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT uk_role_menu UNIQUE (role_id, menu_id) +); + +COMMIT; diff --git a/migrations/postgres/002_system.sql b/migrations/postgres/002_system.sql new file mode 100644 index 0000000..d9db20b --- /dev/null +++ b/migrations/postgres/002_system.sql @@ -0,0 +1,25 @@ +-- 系统参数表(与 internal/system/entity/param_entity.go 中 GORM 模型一致;PostgreSQL) +-- 执行:psql $DATABASE_URL -f migrations/postgres/002_system.sql + +BEGIN; + +CREATE TABLE IF NOT EXISTS system_param ( + id varchar(36) PRIMARY KEY, + param_key varchar(100) NOT NULL, + param_value varchar(1000) NOT NULL, + param_type varchar(20) NOT NULL DEFAULT 'text', + param_group varchar(50) NOT NULL DEFAULT 'default', + param_desc varchar(500) NULL, + creator_id varchar(36) NOT NULL DEFAULT '', + create_time timestamptz NULL DEFAULT now(), + last_updater_id varchar(36) NOT NULL DEFAULT '', + update_time timestamptz NULL DEFAULT now() +); + +CREATE UNIQUE INDEX IF NOT EXISTS uq_system_param_key ON system_param (param_key); + +COMMENT ON TABLE system_param IS '系统运行参数(键值)'; +COMMENT ON COLUMN system_param.param_type IS 'text,number,boolean,select'; +COMMENT ON COLUMN system_param.param_group IS 'basic,security,business,system,default 等'; + +COMMIT; diff --git a/migrations/postgres/003_seed_platform_tenant.sql b/migrations/postgres/003_seed_platform_tenant.sql new file mode 100644 index 0000000..1c83fad --- /dev/null +++ b/migrations/postgres/003_seed_platform_tenant.sql @@ -0,0 +1,17 @@ +-- 可选:平台租户初始化(与 entity.PlatformTenantID / TenantCode 约定一致) +-- 需在 001_iam.sql 执行成功后运行 + +BEGIN; + +INSERT INTO iam_tenant (id, tenant_code, tenant_name, status, created_at, updated_at) +VALUES ( + '00000000-0000-0000-0000-000000000001', + 'platform', + '平台', + 1, + now(), + now() +) +ON CONFLICT (tenant_code) DO NOTHING; + +COMMIT; diff --git a/migrations/postgres/004_seed_platform_builtin.sql b/migrations/postgres/004_seed_platform_builtin.sql new file mode 100644 index 0000000..c81fab0 --- /dev/null +++ b/migrations/postgres/004_seed_platform_builtin.sql @@ -0,0 +1,122 @@ +-- 平台租户内置角色 + 平台管理员账号(与 internal/iam/service 中租户初始化逻辑对齐) +-- 依赖:已执行 001_iam.sql、002_system.sql、003_seed_platform_tenant.sql +-- +-- 默认管理员(平台租户 platform): +-- 用户名:admin +-- 密码:Admin@123 (bcrypt DefaultCost,与 golang.org/x/crypto/bcrypt 一致) +-- +-- 内置角色: +-- tenant_admin — 与 DefaultTenantAdminRoleCode 一致,data_scope=4(全部) +-- user — 普通用户占位,data_scope=1(本人) + +BEGIN; + +-- 固定 UUID,便于排查与文档引用 +-- platform_tenant_id = 00000000-0000-0000-0000-000000000001 +-- root_dept_id = 20000000-0000-4000-8000-000000000001 +-- role_admin_id = 20000000-0000-4000-8000-000000000002 +-- role_user_id = 20000000-0000-4000-8000-000000000006 +-- admin_user_id = 20000000-0000-4000-8000-000000000003 + +-- 1) 平台根部门(与 TenantService.Create 中根部门一致) +INSERT INTO iam_dept ( + id, tenant_id, parent_id, dept_name, dept_path, sort_order, status, created_at, updated_at +) VALUES ( + '20000000-0000-4000-8000-000000000001', + '00000000-0000-0000-0000-000000000001', + '', + '平台', + '/20000000-0000-4000-8000-000000000001/', + 0, + 1, + now(), + now() +) +ON CONFLICT (id) DO NOTHING; + +-- 2) 内置角色:租户超级管理员(与新租户初始化角色编码一致) +INSERT INTO iam_role ( + id, tenant_id, role_code, role_name, data_scope, description, is_builtin, status, created_at, updated_at +) VALUES ( + '20000000-0000-4000-8000-000000000002', + '00000000-0000-0000-0000-000000000001', + 'tenant_admin', + '超级管理员', + 4, + '内置:租户内全部数据权限(与 DefaultTenantAdminRoleCode 一致)', + true, + 1, + now(), + now() +) +ON CONFLICT (id) DO NOTHING; + +-- 3) 内置角色:普通用户(占位) +INSERT INTO iam_role ( + id, tenant_id, role_code, role_name, data_scope, description, is_builtin, status, created_at, updated_at +) VALUES ( + '20000000-0000-4000-8000-000000000006', + '00000000-0000-0000-0000-000000000001', + 'user', + '普通用户', + 1, + '内置:本人数据范围(DataScopeSelf)', + true, + 1, + now(), + now() +) +ON CONFLICT (id) DO NOTHING; + +-- 4) 平台管理员用户(密码 Admin@123) +INSERT INTO iam_user ( + id, tenant_id, dept_id, user_name, real_name, password_hash, status, created_at, updated_at +) VALUES ( + '20000000-0000-4000-8000-000000000003', + '00000000-0000-0000-0000-000000000001', + '20000000-0000-4000-8000-000000000001', + 'admin', + '平台管理员', + '$2a$10$8p7lXpy9mr7hhnAiOA8pNOgAU128xIWFxrU90iqw.F4VSw77vDEYO', + 1, + now(), + now() +) +ON CONFLICT (id) DO NOTHING; + +-- 5) 用户-部门(主部门) +INSERT INTO iam_user_dept (id, user_id, dept_id, is_primary, created_at) +VALUES ( + '20000000-0000-4000-8000-000000000004', + '20000000-0000-4000-8000-000000000003', + '20000000-0000-4000-8000-000000000001', + true, + now() +) +ON CONFLICT (id) DO NOTHING; + +-- 6) 用户-角色(绑定 tenant_admin) +INSERT INTO iam_user_role (id, user_id, role_id, created_at) +VALUES ( + '20000000-0000-4000-8000-000000000005', + '20000000-0000-4000-8000-000000000003', + '20000000-0000-4000-8000-000000000002', + now() +) +ON CONFLICT (id) DO NOTHING; + +-- 7) 回写租户管理员 +UPDATE iam_tenant +SET admin_user_id = '20000000-0000-4000-8000-000000000003' +WHERE id = '00000000-0000-0000-0000-000000000001'; + +-- 8) 将「超级管理员」与当前库中全部菜单关联(与 TenantService.Create 一致;无 iam_menu 数据时本步不插入行) +INSERT INTO iam_role_menu (id, role_id, menu_id, created_at) +SELECT gen_random_uuid()::text, + '20000000-0000-4000-8000-000000000002', + m.id, + now() +FROM iam_menu m +ON CONFLICT (role_id, menu_id) DO NOTHING; + +COMMIT; diff --git a/migrations/postgres/005_oauth.sql b/migrations/postgres/005_oauth.sql new file mode 100644 index 0000000..e71db9d --- /dev/null +++ b/migrations/postgres/005_oauth.sql @@ -0,0 +1,66 @@ +-- OAuth2 客户端与令牌表(Authorization Code + PKCE,opaque access/refresh token) +-- 依赖 001_iam.sql + +BEGIN; + +CREATE TABLE IF NOT EXISTS oauth_client ( + id varchar(36) PRIMARY KEY, + client_id varchar(64) NOT NULL UNIQUE, + client_secret_hash varchar(255) NULL, + redirect_uris text NOT NULL, + is_public boolean NOT NULL DEFAULT true, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS oauth_authorization_code ( + id varchar(36) PRIMARY KEY, + code_hash varchar(64) NOT NULL, + client_id varchar(64) NOT NULL, + redirect_uri text NOT NULL, + user_id varchar(36) NOT NULL, + tenant_id varchar(36) NOT NULL, + scope text NOT NULL DEFAULT '', + code_challenge varchar(128) NOT NULL, + code_challenge_method varchar(16) NOT NULL, + expires_at timestamptz NOT NULL, + used boolean NOT NULL DEFAULT false, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE UNIQUE INDEX IF NOT EXISTS uq_oauth_authorization_code_hash ON oauth_authorization_code (code_hash); + +CREATE TABLE IF NOT EXISTS oauth_access_token ( + id varchar(36) PRIMARY KEY, + token_hash varchar(64) NOT NULL UNIQUE, + client_id varchar(64) NOT NULL, + user_id varchar(36) NOT NULL, + tenant_id varchar(36) NOT NULL, + scope text NOT NULL DEFAULT '', + expires_at timestamptz NOT NULL, + revoked_at timestamptz NULL, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS oauth_refresh_token ( + id varchar(36) PRIMARY KEY, + token_hash varchar(64) NOT NULL UNIQUE, + access_token_id varchar(36) NOT NULL, + client_id varchar(64) NOT NULL, + user_id varchar(36) NOT NULL, + tenant_id varchar(36) NOT NULL, + scope text NOT NULL DEFAULT '', + expires_at timestamptz NOT NULL, + revoked_at timestamptz NULL, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_oauth_refresh_access ON oauth_refresh_token (access_token_id); + +-- 开发用公开客户端(PKCE,无 secret);redirect 按实际前端修改 +INSERT INTO oauth_client (id, client_id, client_secret_hash, redirect_uris, is_public) VALUES ( + '30000000-0000-4000-8000-000000000001', + 'spa', + NULL, + '["http://localhost:5173/callback","http://127.0.0.1:5173/callback","http://localhost:3000/callback"]', + true +) ON CONFLICT (client_id) DO NOTHING; + +COMMIT; diff --git a/migrations/postgres/006_seed_iam_menu_full.sql b/migrations/postgres/006_seed_iam_menu_full.sql new file mode 100644 index 0000000..d09a534 --- /dev/null +++ b/migrations/postgres/006_seed_iam_menu_full.sql @@ -0,0 +1,209 @@ +-- 全量业务菜单种子(与当前后端能力对齐:IAM 租户/部门/角色/用户/菜单 + System 参数) +-- 依赖:001_iam.sql;建议在 004_seed_platform_builtin.sql 之后执行(以便角色已存在时可挂接权限) +-- +-- 约定: +-- menu_type: 1=目录 2=菜单 3=按钮(本脚本仅录入目录+菜单,便于侧栏 /nav) +-- 「概览」(/dashboard) 不入库:全员可进,由前端壳层固定展示,不参与 iam_menu 配置与角色授权 +-- path 与 Next 管理端路由对齐(可按实际前端调整) +-- +-- 固定菜单 id 前缀 31000000-0000-4000-8000-* 便于识别与文档引用 + +BEGIN; + +INSERT INTO iam_menu ( + id, parent_id, menu_name, menu_type, perms, path, component, icon, sort_order, + is_visible, is_builtin, status, created_at, updated_at +) VALUES + -- 工作台(目录) + ( + '31000000-0000-4000-8000-000000000001', + '', + '工作台', + 1, + 'workspace:root', + '', + '', + '⌂', + 10, + true, + true, + 1, + now(), + now() + ), + ( + '31000000-0000-4000-8000-000000000003', + '31000000-0000-4000-8000-000000000001', + '个人中心', + 2, + 'account:profile', + '/dashboard/account', + '', + '👤', + 20, + true, + true, + 1, + now(), + now() + ), + + -- 系统:/api/v1/system/param/* + ( + '31000000-0000-4000-8000-000000000010', + '', + '系统管理', + 1, + 'system:module', + '', + '', + '⚙', + 40, + true, + true, + 1, + now(), + now() + ), + ( + '31000000-0000-4000-8000-000000000011', + '31000000-0000-4000-8000-000000000010', + '参数配置', + 2, + 'system:param:list', + '/dashboard/system/param', + '', + '🔧', + 10, + true, + true, + 1, + now(), + now() + ), + + -- IAM:/api/v1/iam/* + ( + '31000000-0000-4000-8000-000000000020', + '', + '权限管理', + 1, + 'iam:module', + '', + '', + '🛡', + 50, + true, + true, + 1, + now(), + now() + ), + ( + '31000000-0000-4000-8000-000000000021', + '31000000-0000-4000-8000-000000000020', + '租户管理', + 2, + 'iam:tenant:list', + '/dashboard/iam/tenant', + '', + '🏢', + 10, + true, + true, + 1, + now(), + now() + ), + ( + '31000000-0000-4000-8000-000000000022', + '31000000-0000-4000-8000-000000000020', + '部门管理', + 2, + 'iam:dept:tree', + '/dashboard/iam/dept', + '', + '🌳', + 20, + true, + true, + 1, + now(), + now() + ), + ( + '31000000-0000-4000-8000-000000000023', + '31000000-0000-4000-8000-000000000020', + '角色管理', + 2, + 'iam:role:list', + '/dashboard/iam/role', + '', + '👥', + 30, + true, + true, + 1, + now(), + now() + ), + ( + '31000000-0000-4000-8000-000000000024', + '31000000-0000-4000-8000-000000000020', + '用户管理', + 2, + 'iam:user:list', + '/dashboard/iam/user', + '', + '👤', + 40, + true, + true, + 1, + now(), + now() + ), + ( + '31000000-0000-4000-8000-000000000025', + '31000000-0000-4000-8000-000000000020', + '资源(菜单)', + 2, + 'iam:menu:tree', + '/dashboard/iam/resource', + '', + '📋', + 50, + true, + true, + 1, + now(), + now() + ) +ON CONFLICT (id) DO NOTHING; + +-- 将上述菜单授权给平台「超级管理员」角色(与 004 中 role id 一致) +INSERT INTO iam_role_menu (id, role_id, menu_id, created_at) +SELECT gen_random_uuid()::text, + '20000000-0000-4000-8000-000000000002', + m.id, + now() +FROM iam_menu m +WHERE m.id IN ( + '31000000-0000-4000-8000-000000000001', + '31000000-0000-4000-8000-000000000003', + '31000000-0000-4000-8000-000000000010', + '31000000-0000-4000-8000-000000000011', + '31000000-0000-4000-8000-000000000020', + '31000000-0000-4000-8000-000000000021', + '31000000-0000-4000-8000-000000000022', + '31000000-0000-4000-8000-000000000023', + '31000000-0000-4000-8000-000000000024', + '31000000-0000-4000-8000-000000000025' + ) +ON CONFLICT (role_id, menu_id) DO NOTHING; + +-- 若曾执行过含「概览」菜单的旧版脚本,可手工清理(避免侧栏与前端固定入口重复): +-- DELETE FROM iam_role_menu WHERE menu_id = '31000000-0000-4000-8000-000000000002'; +-- DELETE FROM iam_menu WHERE id = '31000000-0000-4000-8000-000000000002'; + +COMMIT; diff --git a/migrations/postgres/README.md b/migrations/postgres/README.md new file mode 100644 index 0000000..ca2996a --- /dev/null +++ b/migrations/postgres/README.md @@ -0,0 +1,27 @@ +# PostgreSQL 脚本 + +与 `internal/iam/entity`、`internal/system/entity` 中的 GORM 模型对齐,便于手工建库或对照 AutoMigrate 结果。 + +**建议顺序** + +1. `001_iam.sql` — IAM 表 +2. `002_system.sql` — `system_param` +3. `003_seed_platform_tenant.sql`(可选)— 平台租户一行 +4. `004_seed_platform_builtin.sql`(可选)— 内置角色(`tenant_admin`、`user`)+ 平台租户下默认管理员 `admin` / `Admin@123`,并为 `tenant_admin` 绑定当前 `iam_menu` 中全部菜单(与 `TenantService.Create` 行为一致) +5. `005_oauth.sql`(可选)— OAuth2 客户端与令牌表,并插入开发用公开客户端 `spa` +6. `006_seed_iam_menu_full.sql`(可选)— 与当前后端模块对齐的侧栏菜单(不含「概览」:`/dashboard` 由前端壳层固定展示、全员可进);含工作台下「个人中心」、系统参数、IAM 子模块;并为平台超级管理员补 `iam_role_menu`(`ON CONFLICT DO NOTHING`) + +**执行示例** + +```bash +psql "$DATABASE_URL" -f migrations/postgres/001_iam.sql +psql "$DATABASE_URL" -f migrations/postgres/002_system.sql +psql "$DATABASE_URL" -f migrations/postgres/003_seed_platform_tenant.sql +psql "$DATABASE_URL" -f migrations/postgres/004_seed_platform_builtin.sql +psql "$DATABASE_URL" -f migrations/postgres/005_oauth.sql +psql "$DATABASE_URL" -f migrations/postgres/006_seed_iam_menu_full.sql +``` + +生产环境请在首次登录后修改默认密码;若需更换哈希,可用与业务相同的 `bcrypt.DefaultCost` 重新生成 `password_hash` 再更新 `iam_user`。 + +若已使用 GORM `AutoMigrate`,可将本目录脚本作为文档或与迁移工具对照,避免重复执行冲突。 diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go new file mode 100644 index 0000000..659aabd --- /dev/null +++ b/pkg/cache/cache.go @@ -0,0 +1,184 @@ +package cache + +import ( + "context" + "errors" + "fmt" + "strings" + + "giter.top/smart/pkg/config" + + "github.com/redis/go-redis/v9" +) + +// NewRedis 根据配置创建 Redis 客户端,支持单机、哨兵、集群三种模式。 +func NewRedis(cfg *config.Config) (redis.UniversalClient) { + if cfg == nil { + panic("cache: config is nil") + } + r := cfg.Data.Redis + mode := strings.ToLower(strings.TrimSpace(r.Mode)) + if mode == "" { + mode = "standalone" + } + + switch mode { + case "standalone": + addr, err := standaloneAddr(cfg) + if err != nil { + panic(err) + } + opt := &redis.Options{ + Addr: addr, + DB: r.DB, + } + applyCommonToClient(opt, cfg) + return redis.NewClient(opt) + + case "sentinel": + if strings.TrimSpace(r.MasterName) == "" { + panic("cache: redis sentinel requires master_name") + } + if len(r.Addrs) == 0 { + panic("cache: redis sentinel requires addrs (sentinel 节点列表)") + } + opt := &redis.FailoverOptions{ + MasterName: r.MasterName, + SentinelAddrs: r.Addrs, + DB: r.DB, + } + applyCommonToFailover(opt, cfg) + return redis.NewFailoverClient(opt) + + case "cluster": + if len(r.Addrs) == 0 { + panic("cache: redis cluster requires addrs") + } + opt := &redis.ClusterOptions{ + Addrs: r.Addrs, + } + applyCommonToCluster(opt, cfg) + return redis.NewClusterClient(opt) + + default: + panic(fmt.Sprintf("cache: unsupported redis mode %q", r.Mode)) + } +} + +func standaloneAddr(cfg *config.Config) (string, error) { + r := cfg.Data.Redis + if strings.TrimSpace(r.Addr) != "" { + return r.Addr, nil + } + if len(r.Addrs) > 0 && strings.TrimSpace(r.Addrs[0]) != "" { + return r.Addrs[0], nil + } + return "", errors.New("cache: redis standalone requires addr or addrs[0]") +} + +// Ping 用于启动时探测连接是否可用。 +func Ping(ctx context.Context, c redis.UniversalClient) error { + if c == nil { + return errors.New("cache: redis client is nil") + } + return c.Ping(ctx).Err() +} + +func applyCommonToClient(opt *redis.Options, cfg *config.Config) { + r := cfg.Data.Redis + opt.Username = r.Username + opt.Password = r.Password + if r.PoolSize > 0 { + opt.PoolSize = r.PoolSize + } + if r.MinIdleConns > 0 { + opt.MinIdleConns = r.MinIdleConns + } + if r.MaxRetries != 0 { + opt.MaxRetries = r.MaxRetries + } + if r.RetryDelay > 0 { + opt.MinRetryBackoff = r.RetryDelay + } + if r.RetryMaxDelay > 0 { + opt.MaxRetryBackoff = r.RetryMaxDelay + } + if r.DialTimeout > 0 { + opt.DialTimeout = r.DialTimeout + } + if r.ReadTimeout > 0 { + opt.ReadTimeout = r.ReadTimeout + } + if r.WriteTimeout > 0 { + opt.WriteTimeout = r.WriteTimeout + } + if r.IdleTimeout > 0 { + opt.ConnMaxIdleTime = r.IdleTimeout + } +} + +func applyCommonToFailover(opt *redis.FailoverOptions, cfg *config.Config) { + r := cfg.Data.Redis + opt.Username = r.Username + opt.Password = r.Password + if r.PoolSize > 0 { + opt.PoolSize = r.PoolSize + } + if r.MinIdleConns > 0 { + opt.MinIdleConns = r.MinIdleConns + } + if r.MaxRetries != 0 { + opt.MaxRetries = r.MaxRetries + } + if r.RetryDelay > 0 { + opt.MinRetryBackoff = r.RetryDelay + } + if r.RetryMaxDelay > 0 { + opt.MaxRetryBackoff = r.RetryMaxDelay + } + if r.DialTimeout > 0 { + opt.DialTimeout = r.DialTimeout + } + if r.ReadTimeout > 0 { + opt.ReadTimeout = r.ReadTimeout + } + if r.WriteTimeout > 0 { + opt.WriteTimeout = r.WriteTimeout + } + if r.IdleTimeout > 0 { + opt.ConnMaxIdleTime = r.IdleTimeout + } +} + +func applyCommonToCluster(opt *redis.ClusterOptions, cfg *config.Config) { + r := cfg.Data.Redis + opt.Username = r.Username + opt.Password = r.Password + if r.PoolSize > 0 { + opt.PoolSize = r.PoolSize + } + if r.MinIdleConns > 0 { + opt.MinIdleConns = r.MinIdleConns + } + if r.MaxRetries != 0 { + opt.MaxRetries = r.MaxRetries + } + if r.RetryDelay > 0 { + opt.MinRetryBackoff = r.RetryDelay + } + if r.RetryMaxDelay > 0 { + opt.MaxRetryBackoff = r.RetryMaxDelay + } + if r.DialTimeout > 0 { + opt.DialTimeout = r.DialTimeout + } + if r.ReadTimeout > 0 { + opt.ReadTimeout = r.ReadTimeout + } + if r.WriteTimeout > 0 { + opt.WriteTimeout = r.WriteTimeout + } + if r.IdleTimeout > 0 { + opt.ConnMaxIdleTime = r.IdleTimeout + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..280e2d8 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,84 @@ +package config + +import ( + "time" + + "github.com/spf13/viper" +) + +type Config struct { + Server struct { + Http struct { + Addr string `mapstructure:"addr"` + Timeout time.Duration `mapstructure:"timeout"` + } `mapstructure:"http"` + Grpc struct { + Addr string `mapstructure:"addr"` + Timeout time.Duration `mapstructure:"timeout"` + } `mapstructure:"grpc"` + } `mapstructure:"server"` + Data struct { + Database struct { + Driver string `mapstructure:"driver"` + DSN string `mapstructure:"dsn"` + } `mapstructure:"database"` + Redis struct { + // Mode: standalone(单机)、sentinel(哨兵)、cluster(集群) + Mode string `mapstructure:"mode"` + Addr string `mapstructure:"addr"` + Addrs []string `mapstructure:"addrs"` + Password string `mapstructure:"password"` + Username string `mapstructure:"username"` + DB int `mapstructure:"db"` + MasterName string `mapstructure:"master_name"` + PoolSize int `mapstructure:"pool_size"` + // MinIdleConns 最小空闲连接数 + MinIdleConns int `mapstructure:"min_idle_conns"` + MaxRetries int `mapstructure:"max_retries"` + RetryDelay time.Duration `mapstructure:"retry_delay"` + RetryMaxDelay time.Duration `mapstructure:"retry_max_delay"` + DialTimeout time.Duration `mapstructure:"dial_timeout"` + ReadTimeout time.Duration `mapstructure:"read_timeout"` + WriteTimeout time.Duration `mapstructure:"write_timeout"` + // IdleTimeout 映射为 go-redis ConnMaxIdleTime + IdleTimeout time.Duration `mapstructure:"idle_timeout"` + } `mapstructure:"redis"` + } `mapstructure:"data"` + // Auth 认证域(OAuth2、会话等);PublicBaseURL 为浏览器可访问的后端根 URL(用于登录回跳拼接 /oauth/authorize) + Auth struct { + PublicBaseURL string `mapstructure:"public_base_url"` + OAuth2 struct { + FrontendLoginURL string `mapstructure:"frontend_login_url"` + AuthCodeTTL time.Duration `mapstructure:"auth_code_ttl"` + AccessTokenTTL time.Duration `mapstructure:"access_token_ttl"` + RefreshTokenTTL time.Duration `mapstructure:"refresh_token_ttl"` + } `mapstructure:"oauth2"` + Session struct { + CookieName string `mapstructure:"cookie_name"` + CookieDomain string `mapstructure:"cookie_domain"` + CookieSecure bool `mapstructure:"cookie_secure"` + SameSite string `mapstructure:"same_site"` // lax, strict, none + TTL time.Duration `mapstructure:"ttl"` + } `mapstructure:"session"` + // RateLimit 登录与令牌端点限流(进程内按 IP;多实例需网关或 Redis 限流) + RateLimit struct { + Enabled bool `mapstructure:"enabled"` + LoginPerMinute int `mapstructure:"login_per_minute"` + TokenPerMinute int `mapstructure:"token_per_minute"` + } `mapstructure:"rate_limit"` + } `mapstructure:"auth"` +} + +// 加载配置文件 +func Load(path string) (*Config, error) { + v := viper.New() + v.SetConfigFile(path) + if err := v.ReadInConfig(); err != nil { + return nil, err + } + var config Config + if err := v.Unmarshal(&config); err != nil { + return nil, err + } + return &config, nil +} diff --git a/pkg/db/db.go b/pkg/db/db.go new file mode 100644 index 0000000..a7e6e8f --- /dev/null +++ b/pkg/db/db.go @@ -0,0 +1,27 @@ +package db + +import ( + "giter.top/smart/pkg/config" + "gorm.io/gorm" +) + +func NewDB(cfg *config.Config) *gorm.DB { + driver := cfg.Data.Database.Driver + var db *gorm.DB + var err error + switch driver { + case "mysql": + // db, err = NewMySQLDB(cfg) + case "postgres": + db, err = NewPgSQLDB(cfg) + case "sqlite": + // return NewSQLiteDB(cfg) + default: + panic("unsupported driver") + } + + if err != nil { + panic(err) + } + return db +} \ No newline at end of file diff --git a/pkg/db/mysql_db.go b/pkg/db/mysql_db.go new file mode 100644 index 0000000..10060e9 --- /dev/null +++ b/pkg/db/mysql_db.go @@ -0,0 +1 @@ +package db \ No newline at end of file diff --git a/pkg/db/pg_db.go b/pkg/db/pg_db.go new file mode 100644 index 0000000..cf3bc7b --- /dev/null +++ b/pkg/db/pg_db.go @@ -0,0 +1,18 @@ +package db + +import ( + "errors" + + "giter.top/smart/pkg/config" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +// pg sql 数据库连接 +func NewPgSQLDB(cfg *config.Config) (*gorm.DB , error) { + db, err := gorm.Open(postgres.Open(cfg.Data.Database.DSN), &gorm.Config{}) + if err != nil { + return nil, errors.New("failed to connect to postgres database") + } + return db, nil +} \ No newline at end of file diff --git a/pkg/security/token.go b/pkg/security/token.go new file mode 100644 index 0000000..b7f009a --- /dev/null +++ b/pkg/security/token.go @@ -0,0 +1,15 @@ +package security + +import ( + "crypto/rand" + "encoding/base64" +) + +// RandomURLSafe 生成 URL-safe 随机串(用于 opaque token、authorization code 等)。 +func RandomURLSafe(nBytes int) (string, error) { + b := make([]byte, nBytes) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +} diff --git a/pkg/utils/codec/bcrypt.go b/pkg/utils/codec/bcrypt.go new file mode 100644 index 0000000..663bafc --- /dev/null +++ b/pkg/utils/codec/bcrypt.go @@ -0,0 +1,19 @@ +package codec + +import "golang.org/x/crypto/bcrypt" + +// HashPassword 将明文密码生成为 bcrypt 哈希字符串(与业务中 bcrypt.DefaultCost 一致)。 +func HashPassword(password string) (string, error) { + b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(b), nil +} + +// VerifyPassword 校验明文是否与 bcrypt 哈希匹配。 +// password 明文密码 +// hashedPassword 哈希密码 +func VerifyPassword(password,hashedPassword string) error { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} diff --git a/pkg/utils/id/id.go b/pkg/utils/id/id.go new file mode 100644 index 0000000..aa89c67 --- /dev/null +++ b/pkg/utils/id/id.go @@ -0,0 +1,21 @@ +package id + +import ( + "sync" +) + +var ( + generator IDGenerator + once sync.Once +) + +type IDGenerator interface { + generate() string +} + +func New() string { + once.Do(func() { + generator = NewUUIDGenerator() + }) + return generator.generate() +} diff --git a/pkg/utils/id/uuid_generator.go b/pkg/utils/id/uuid_generator.go new file mode 100644 index 0000000..3a2a21c --- /dev/null +++ b/pkg/utils/id/uuid_generator.go @@ -0,0 +1,15 @@ +package id + +import "github.com/google/uuid" + +type UUIDGenerator struct { +} + +func NewUUIDGenerator() IDGenerator { + return &UUIDGenerator{} +} + +func (g *UUIDGenerator) generate() string { + id, _ := uuid.NewV7() + return id.String() +} diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..be132eb --- /dev/null +++ b/web/.env.example @@ -0,0 +1,7 @@ +# Go HTTP 根地址(无尾部斜杠,不含 /api/v1) +NEXT_PUBLIC_API_ORIGIN=http://127.0.0.1:8000 + +# OAuth 公开客户端(与 Go 种子 client 一致) +NEXT_PUBLIC_OAUTH_CLIENT_ID=spa +# 换 token 时须与登录请求 redirect_uri 一致(本机开发) +NEXT_PUBLIC_OAUTH_REDIRECT_URI=http://localhost:3000/oauth/callback diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..7b8da95 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/web/.prettierignore b/web/.prettierignore new file mode 100644 index 0000000..e28a4e9 --- /dev/null +++ b/web/.prettierignore @@ -0,0 +1,6 @@ +.next +node_modules +out +build +package-lock.json +tsconfig.tsbuildinfo diff --git a/web/.prettierrc b/web/.prettierrc new file mode 100644 index 0000000..cbd1fe3 --- /dev/null +++ b/web/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..32d7450 --- /dev/null +++ b/web/README.md @@ -0,0 +1,19 @@ +# Web(Next.js) + +对接 [`../docs/auth-api.md`](../docs/auth-api.md) 与 Go `/api/v1`。 + +## 本地开发 + +```bash +cp .env.example .env.local +npm install +npm run dev +``` + +`NEXT_PUBLIC_API_ORIGIN` 指向 Go 监听地址(默认开发 `http://127.0.0.1:8000`)。 + +## 目录约定 + +- `lib/api/` — HTTP 客户端与各领域 API +- `stores/` — Zustand 状态 +- `lib/env.ts` — 公共环境变量读取 diff --git a/web/app/(main)/(iam)/(devops)/resource/page.tsx b/web/app/(main)/(iam)/(devops)/resource/page.tsx new file mode 100644 index 0000000..4cc71f7 --- /dev/null +++ b/web/app/(main)/(iam)/(devops)/resource/page.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { IamSectionCard } from '@/components/iam/IamSectionCard'; +import { MenuTreeView } from '@/components/iam/MenuTreeView'; +import { useApi } from '@/lib/hooks/use-api'; +import { iamMenu } from '@/lib/api/iam'; +import type { MenuNode } from '@/lib/api/types/menu'; + +export default function IamResourcePage() { + const { data, loading, error } = useApi(() => iamMenu.tree()); + + return ( + + {loading ?

加载中…

: null} + {error ?

{error}

: null} + {!loading && !error && data ? : null} +
+ ); +} diff --git a/web/app/(main)/(iam)/(devops)/tenant/page.tsx b/web/app/(main)/(iam)/(devops)/tenant/page.tsx new file mode 100644 index 0000000..6ea4780 --- /dev/null +++ b/web/app/(main)/(iam)/(devops)/tenant/page.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { IamSectionCard } from '@/components/iam/IamSectionCard'; +import { useApi } from '@/lib/hooks/use-api'; +import { iamTenant } from '@/lib/api/iam'; +import type { IamTenant } from '@/lib/api/types/tenant'; + +export default function IamTenantPage() { + const { data, loading, error } = useApi(() => iamTenant.list({ page: '1', page_size: '100' })); + const rows = data?.items ?? []; + + return ( + + {loading ?

加载中…

: null} + {error ?

{error}

: null} + {!loading && !error ? ( +
+ + + + + + + + + + + {rows.map((r) => ( + + + + + + + ))} + +
租户名称编码状态ID
{r.tenant_name ?? '—'}{r.tenant_code ?? '—'}{r.status ?? '—'}{r.id}
+ {!rows.length ?

暂无租户

: null} +
+ ) : null} +
+ ); +} diff --git a/web/app/(main)/(iam)/dept/page.tsx b/web/app/(main)/(iam)/dept/page.tsx new file mode 100644 index 0000000..6e25b22 --- /dev/null +++ b/web/app/(main)/(iam)/dept/page.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { DeptTreeView } from '@/components/iam/DeptTreeView'; +import { IamSectionCard } from '@/components/iam/IamSectionCard'; +import { useApi } from '@/lib/hooks/use-api'; +import { iamDept } from '@/lib/api/iam'; +import type { DeptNode } from '@/lib/api/types/dept'; + +export default function IamDeptPage() { + const { data, loading, error } = useApi(() => iamDept.tree()); + + return ( + + {loading ?

加载中…

: null} + {error ?

{error}

: null} + {!loading && !error && data ? : null} +
+ ); +} diff --git a/web/app/(main)/(iam)/role/page.tsx b/web/app/(main)/(iam)/role/page.tsx new file mode 100644 index 0000000..295948f --- /dev/null +++ b/web/app/(main)/(iam)/role/page.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { IamSectionCard } from '@/components/iam/IamSectionCard'; +import { useApi } from '@/lib/hooks/use-api'; +import { iamRole } from '@/lib/api/iam'; +import type { IamRole } from '@/lib/api/types/role'; + +export default function IamRolePage() { + const { data, loading, error } = useApi(() => iamRole.list({ page: '1', page_size: '50' })); + const rows = data?.items ?? []; + + return ( + + {loading ?

加载中…

: null} + {error ?

{error}

: null} + {!loading && !error ? ( +
+ + + + + + + + + + + {rows.map((r) => ( + + + + + + + ))} + +
角色名编码数据范围ID
{r.role_name ?? '—'}{r.role_code ?? '—'}{r.data_scope ?? '—'}{r.id}
+ {!rows.length ?

暂无角色

: null} +
+ ) : null} +
+ ); +} diff --git a/web/app/(main)/(iam)/user/page.tsx b/web/app/(main)/(iam)/user/page.tsx new file mode 100644 index 0000000..0d7cde7 --- /dev/null +++ b/web/app/(main)/(iam)/user/page.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { IamSectionCard } from '@/components/iam/IamSectionCard'; +import { useApi } from '@/lib/hooks/use-api'; +import { iamUser } from '@/lib/api/iam'; +import type { IamUser } from '@/lib/api/types/user'; + +export default function IamUserPage() { + const { data, loading, error } = useApi(() => iamUser.list({ page: '1', page_size: '20' })); + const rows = data?.items ?? []; + + return ( + + {loading ?

加载中…

: null} + {error ?

{error}

: null} + {!loading && !error ? ( +
+ + + + + + + + + + + {rows.map((r) => ( + + + + + + + ))} + +
用户名姓名状态ID
{r.user_name ?? '—'}{r.real_name ?? '—'}{r.status ?? '—'}{r.id}
+ {!rows.length ?

暂无用户

: null} +
+ ) : null} +
+ ); +} diff --git a/web/app/(main)/dashboard/account/page.tsx b/web/app/(main)/dashboard/account/page.tsx new file mode 100644 index 0000000..8a5da68 --- /dev/null +++ b/web/app/(main)/dashboard/account/page.tsx @@ -0,0 +1,8 @@ +export default function AccountPage() { + return ( +
+

个人中心

+

资料与密码修改等功能可在此页对接 IAM。

+
+ ); +} diff --git a/web/app/(main)/dashboard/page.tsx b/web/app/(main)/dashboard/page.tsx new file mode 100644 index 0000000..4926b16 --- /dev/null +++ b/web/app/(main)/dashboard/page.tsx @@ -0,0 +1,8 @@ +export default function DashboardPage() { + return ( +
+

概览

+

后续在此接 Tabs + 业务页。

+
+ ); +} diff --git a/web/app/(main)/layout.tsx b/web/app/(main)/layout.tsx new file mode 100644 index 0000000..1054706 --- /dev/null +++ b/web/app/(main)/layout.tsx @@ -0,0 +1,9 @@ +import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout'; + +/** + * 单一布局包裹 /dashboard 与 /user 等路由,避免 Tab 在二者间切换时卸载布局、 + * 导致 AppChrome 重挂载并重复请求侧栏菜单。 + */ +export default function MainLayout({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/web/app/favicon.ico b/web/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/web/app/globals.css b/web/app/globals.css new file mode 100644 index 0000000..793b3cd --- /dev/null +++ b/web/app/globals.css @@ -0,0 +1,55 @@ +@import 'tailwindcss'; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +body { + background: var(--background); + color: var(--foreground); + font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif; +} + +/* 收起态侧栏:一级悬停展开的二级浮层入场(略慢、缓出更柔和) */ +@keyframes cascade-flyout-in { + from { + opacity: 0; + transform: translateX(-6px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.cascade-flyout-panel { + animation: cascade-flyout-in 0.4s cubic-bezier(0.33, 1, 0.68, 1) both; +} + +@keyframes cascade-flyout-sub-in { + from { + opacity: 0; + transform: translateX(-4px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.cascade-flyout-sub { + animation: cascade-flyout-sub-in 0.32s cubic-bezier(0.33, 1, 0.68, 1) both; +} + +@media (prefers-reduced-motion: reduce) { + .cascade-flyout-panel, + .cascade-flyout-sub { + animation: none; + } +} diff --git a/web/app/layout.tsx b/web/app/layout.tsx new file mode 100644 index 0000000..7f9db8e --- /dev/null +++ b/web/app/layout.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from 'next'; +import { Geist, Geist_Mono } from 'next/font/google'; +import { AppProviders } from '@/components/providers/AppProviders'; +import './globals.css'; + +const geistSans = Geist({ + variable: '--font-geist-sans', + subsets: ['latin'], +}); + +const geistMono = Geist_Mono({ + variable: '--font-geist-mono', + subsets: ['latin'], +}); + +export const metadata: Metadata = { + title: 'Smart Go', + description: 'Smart Go 管理平台', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/web/app/login/page.tsx b/web/app/login/page.tsx new file mode 100644 index 0000000..c0a00dd --- /dev/null +++ b/web/app/login/page.tsx @@ -0,0 +1,128 @@ +'use client'; + +import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Suspense, useEffect, useState } from 'react'; +import { safeReturnPath } from '@/lib/navigation/safe-return'; +import { useAuthStore } from '@/stores/auth-store'; + +function LoginForm() { + const router = useRouter(); + const searchParams = useSearchParams(); + const accessToken = useAuthStore((s) => s.accessToken); + const login = useAuthStore((s) => s.login); + const [mounted, setMounted] = useState(false); + const [user, setUser] = useState(''); + const [pass, setPass] = useState(''); + const [tenant, setTenant] = useState(''); + const [err, setErr] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted || !accessToken) { + return; + } + const next = safeReturnPath(searchParams.get('from'), '/dashboard'); + router.replace(next); + }, [mounted, accessToken, router, searchParams]); + + if (!mounted) { + return ( +
+ 加载中… +
+ ); + } + + if (accessToken) { + return ( +
+ 正在进入后台… +
+ ); + } + + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + setErr(null); + setLoading(true); + try { + await login(user, pass, tenant || undefined); + const next = safeReturnPath(searchParams.get('from'), '/dashboard'); + router.replace(next); + } catch (ex) { + setErr(ex instanceof Error ? ex.message : String(ex)); + } finally { + setLoading(false); + } + } + + return ( +
+
+

登录

+ + + + {err ?

{err}

: null} + +
+ + 返回首页 + +
+ ); +} + +export default function LoginPage() { + return ( + + 加载中… + + } + > + + + ); +} diff --git a/web/app/oauth/callback/page.tsx b/web/app/oauth/callback/page.tsx new file mode 100644 index 0000000..a75d466 --- /dev/null +++ b/web/app/oauth/callback/page.tsx @@ -0,0 +1,67 @@ +'use client'; + +import { exchangeCodeForTokens } from '@/lib/api/auth'; +import { getOAuthClientId, getOAuthRedirectUri } from '@/lib/env'; +import { takeStoredPkceVerifier } from '@/lib/oauth/browser'; +import { useAuthStore } from '@/stores/auth-store'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Suspense, useEffect, useState } from 'react'; + +function CallbackInner() { + const sp = useSearchParams(); + const router = useRouter(); + const [msg, setMsg] = useState('处理中…'); + + useEffect(() => { + const code = sp.get('code'); + const err = sp.get('error'); + if (err) { + setMsg(sp.get('error_description') || err); + return; + } + if (!code) { + setMsg('缺少授权码'); + return; + } + const verifier = takeStoredPkceVerifier(); + if (!verifier) { + setMsg('缺少 PKCE verifier,请从授权入口重新登录'); + return; + } + (async () => { + try { + const pair = await exchangeCodeForTokens({ + code, + codeVerifier: verifier, + clientId: getOAuthClientId(), + redirectUri: getOAuthRedirectUri(), + }); + useAuthStore.getState().setTokens(pair.accessToken, pair.refreshToken); + setMsg('登录成功,正在跳转…'); + router.replace('/dashboard'); + } catch (e) { + setMsg(e instanceof Error ? e.message : String(e)); + } + })(); + }, [sp, router]); + + return ( +
+

{msg}

+
+ ); +} + +export default function OAuthCallbackPage() { + return ( + +

加载中…

+ + } + > + +
+ ); +} diff --git a/web/app/page.tsx b/web/app/page.tsx new file mode 100644 index 0000000..dcd302e --- /dev/null +++ b/web/app/page.tsx @@ -0,0 +1,23 @@ +import Link from 'next/link'; + +export default function Home() { + return ( +
+

Smart Go · Web

+

+ 对接 Go `NEXT_PUBLIC_API_ORIGIN`,支持账号密码 + PKCE 换 token,或浏览器 OAuth 授权码流程。 +

+
+ + 登录 + + + 工作台 + +
+
+ ); +} diff --git a/web/components/auth/LoginModal.tsx b/web/components/auth/LoginModal.tsx new file mode 100644 index 0000000..1a7dc37 --- /dev/null +++ b/web/components/auth/LoginModal.tsx @@ -0,0 +1,118 @@ +'use client'; + +import * as Dialog from '@radix-ui/react-dialog'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useAuthStore } from '@/stores/auth-store'; +import { useAuthUiStore } from '@/stores/auth-ui-store'; + +export function LoginModal() { + const open = useAuthUiStore((s) => s.loginModalOpen); + const close = useAuthUiStore((s) => s.closeLoginModal); + const hint = useAuthUiStore((s) => s.loginHint); + const login = useAuthStore((s) => s.login); + const router = useRouter(); + + const [user, setUser] = useState(''); + const [pass, setPass] = useState(''); + const [tenant, setTenant] = useState(''); + const [err, setErr] = useState(null); + const [loading, setLoading] = useState(false); + + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + setErr(null); + setLoading(true); + try { + await login(user, pass, tenant || undefined); + close(); + setUser(''); + setPass(''); + setTenant(''); + router.refresh(); + } catch (ex) { + setErr(ex instanceof Error ? ex.message : String(ex)); + } finally { + setLoading(false); + } + } + + return ( + { + if (!next) { + close(); + } + }} + > + + + e.preventDefault()} + > + + 重新登录 + + {hint ? ( + {hint} + ) : ( + 请输入用户名与密码以继续操作。 + )} +
+ + + + {err ?

{err}

: null} +
+ + +
+
+
+
+
+ ); +} diff --git a/web/components/feedback/ToastHost.tsx b/web/components/feedback/ToastHost.tsx new file mode 100644 index 0000000..e903837 --- /dev/null +++ b/web/components/feedback/ToastHost.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { useToastStore } from '@/stores/toast-store'; + +export function ToastHost() { + const toasts = useToastStore((s) => s.toasts); + const dismiss = useToastStore((s) => s.dismiss); + + if (!toasts.length) { + return null; + } + + return ( +
+ {toasts.map((t) => ( +
+
+ {t.message} + +
+
+ ))} +
+ ); +} diff --git a/web/components/iam/DeptTreeView.tsx b/web/components/iam/DeptTreeView.tsx new file mode 100644 index 0000000..1ada43c --- /dev/null +++ b/web/components/iam/DeptTreeView.tsx @@ -0,0 +1,23 @@ +'use client'; + +import type { DeptNode } from '@/lib/api/types/dept'; + +function DeptNodes({ nodes, depth }: { nodes: DeptNode[]; depth: number }) { + return ( +
    + {nodes.map((n) => ( +
  • + {n.dept_name} + {n.children?.length ? : null} +
  • + ))} +
+ ); +} + +export function DeptTreeView(props: { tree: DeptNode[] }) { + if (!props.tree.length) { + return

暂无数据

; + } + return ; +} diff --git a/web/components/iam/IamSectionCard.tsx b/web/components/iam/IamSectionCard.tsx new file mode 100644 index 0000000..f2ec49f --- /dev/null +++ b/web/components/iam/IamSectionCard.tsx @@ -0,0 +1,15 @@ +export function IamSectionCard(props: { + title: string; + description?: string; + children: React.ReactNode; +}) { + return ( +
+

{props.title}

+ {props.description ? ( +

{props.description}

+ ) : null} +
{props.children}
+
+ ); +} diff --git a/web/components/iam/MenuTreeView.tsx b/web/components/iam/MenuTreeView.tsx new file mode 100644 index 0000000..bc3cd42 --- /dev/null +++ b/web/components/iam/MenuTreeView.tsx @@ -0,0 +1,29 @@ +'use client'; + +import type { MenuNode } from '@/lib/api/types/menu'; + +function MenuNodes({ nodes, depth }: { nodes: MenuNode[]; depth: number }) { + return ( +
    + {nodes.map((n) => ( +
  • + {n.menu_name} + {n.path ? ( + {n.path} + ) : null} + {n.perms ? ( + [{n.perms}] + ) : null} + {n.children?.length ? : null} +
  • + ))} +
+ ); +} + +export function MenuTreeView(props: { tree: MenuNode[] }) { + if (!props.tree.length) { + return

暂无数据

; + } + return ; +} diff --git a/web/components/layout/AppChrome.tsx b/web/components/layout/AppChrome.tsx new file mode 100644 index 0000000..8712317 --- /dev/null +++ b/web/components/layout/AppChrome.tsx @@ -0,0 +1,179 @@ +'use client'; + +import * as Dialog from '@radix-ui/react-dialog'; +import Link from 'next/link'; +import { useEffect, useRef, useState } from 'react'; +import { ClassicCollapsedSidebar } from '@/components/layout/ClassicCollapsedSidebar'; +import { IconSidebarLayout } from '@/components/layout/IconSidebarLayout'; +import { NavTooltip } from '@/components/layout/nav-tooltip'; +import { SidebarNav } from '@/components/layout/SidebarNav'; +import { TenantSwitcher } from '@/components/layout/TenantSwitcher'; +import { UserMenu } from '@/components/layout/UserMenu'; +import { useMenuNavigation } from '@/lib/hooks/use-menu-navigation'; +import { useNavMenu } from '@/lib/hooks/use-nav-menu'; +import { redirectToAuthorize } from '@/lib/oauth/browser'; +import { useAuthStore } from '@/stores/auth-store'; +import { useLayoutStore } from '@/stores/layout-store'; + +export function AppChrome({ children }: { children: React.ReactNode }) { + const authed = useAuthStore((s) => Boolean(s.accessToken)); + const sidebarMode = useLayoutStore((s) => s.sidebarMode); + const classicNavRailCollapsed = useLayoutStore((s) => s.classicNavRailCollapsed); + const toggleClassicNavRail = useLayoutStore((s) => s.toggleClassicNavRail); + + const { items: nav, loading: navLoading, error: navError } = useNavMenu(); + const onMenuNavigate = useMenuNavigation(); + + const [mobileNavOpen, setMobileNavOpen] = useState(false); + + const [showClassicNarrowUi, setShowClassicNarrowUi] = useState(classicNavRailCollapsed); + const classicAsideRef = useRef(null); + + useEffect(() => { + const mq = window.matchMedia('(min-width: 768px)'); + const onChange = () => { + if (mq.matches) setMobileNavOpen(false); + }; + mq.addEventListener('change', onChange); + return () => mq.removeEventListener('change', onChange); + }, []); + + useEffect(() => { + if (!classicNavRailCollapsed) setShowClassicNarrowUi(false); + }, [classicNavRailCollapsed]); + + useEffect(() => { + if (!classicNavRailCollapsed || showClassicNarrowUi) return; + const el = classicAsideRef.current; + const finish = () => { + if (useLayoutStore.getState().classicNavRailCollapsed) setShowClassicNarrowUi(true); + }; + const onEnd = (e: TransitionEvent) => { + if (e.propertyName === 'width') { + finish(); + el?.removeEventListener('transitionend', onEnd); + } + }; + el?.addEventListener('transitionend', onEnd); + const t = window.setTimeout(finish, 600); + return () => { + el?.removeEventListener('transitionend', onEnd); + window.clearTimeout(t); + }; + }, [classicNavRailCollapsed, showClassicNarrowUi]); + + const classicAsideMotion = + 'min-w-0 shrink-0 transition-[width] duration-500 ease-in-out motion-reduce:transition-none'; + + const asideClass = + sidebarMode === 'icon' + ? 'hidden min-h-0 shrink-0 overflow-hidden border-r border-neutral-200 bg-white md:flex md:flex-col' + : `relative hidden h-full min-h-0 flex-col overflow-visible border-r border-neutral-200 bg-white md:flex ${classicAsideMotion}`; + + const classicAsideStyle = + sidebarMode !== 'icon' ? { width: classicNavRailCollapsed ? '72px' : '14rem' } : undefined; + + const showExpandedClassicChrome = !classicNavRailCollapsed || !showClassicNarrowUi; + + const sidebarProps = { + items: nav, + loading: navLoading, + error: navError, + authed, + onMenuNavigate, + }; + + return ( +
+
+
+ + + Smart Admin + + {authed ? : null} +
+
+ {authed ? ( + + ) : ( + <> + + 账号登录 + + + + )} +
+
+ +
+ + +
{children}
+
+ + + + + + 导航菜单 + 浏览并选择页面,关闭按钮在右上角。 + +
+ setMobileNavOpen(false)} /> +
+
+
+
+
+ ); +} diff --git a/web/components/layout/AuthenticatedLayout.tsx b/web/components/layout/AuthenticatedLayout.tsx new file mode 100644 index 0000000..d75d5d4 --- /dev/null +++ b/web/components/layout/AuthenticatedLayout.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { AppChrome } from '@/components/layout/AppChrome'; +import { RequireAuth } from '@/components/layout/RequireAuth'; +import { TabStrip } from '@/components/layout/TabStrip'; + +/** 登录后带侧栏 + 多标签主工作区布局(dashboard 与 (iam) 等业务页共用) */ +export function AuthenticatedLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ + {/* 与 TabStrip 间距及距底 12px(Tailwind p-3) */} +
+ {children} +
+
+
+
+ ); +} diff --git a/web/components/layout/ClassicCascadeFlyout.tsx b/web/components/layout/ClassicCascadeFlyout.tsx new file mode 100644 index 0000000..aa60f1d --- /dev/null +++ b/web/components/layout/ClassicCascadeFlyout.tsx @@ -0,0 +1,214 @@ +'use client'; + +import Link from 'next/link'; +import { createPortal } from 'react-dom'; +import { useCallback, useEffect, useState } from 'react'; +import type { MenuNode } from '@/lib/api/types/menu'; +import { NavTooltip } from '@/components/layout/nav-tooltip'; +import { + isLayoutOverviewNode, + normalizeHref, + isActivePath, + visibleChildren, + linkClass, +} from '@/components/layout/nav-shared'; + +function CascadeFolderPanel({ + title, + nodes, + pathname, + onMenuNavigate, + depth = 0, +}: { + title: string; + nodes: MenuNode[]; + pathname: string; + onMenuNavigate?: (path: string, title: string) => void; + depth?: number; +}) { + const [openId, setOpenId] = useState(null); + + const handleFolderKey = useCallback( + (e: React.KeyboardEvent, nodeId: string) => { + if (e.key === 'Enter' || e.key === 'ArrowRight') { + e.preventDefault(); + setOpenId(nodeId); + } else if (e.key === 'Escape') { + setOpenId(null); + } + }, + [], + ); + + if (nodes.length === 0) { + return null; + } + + return ( +
+
{title}
+
+ {nodes.map((node) => { + if (node.menu_type === 3) { + return null; + } + const kids = visibleChildren(node); + const hasKids = kids.length > 0; + const pad = 8; + + const label = ( + + {node.icon ? ( + + {node.icon} + + ) : null} + {node.menu_name} + + ); + + if (hasKids) { + return ( +
setOpenId(node.id)} + > + + + + {openId === node.id ? ( +
+ +
+ ) : null} +
+ ); + } + + const href = normalizeHref(node); + const external = Boolean(node.external_link); + const active = isLayoutOverviewNode(node) + ? (pathname.replace(/\/$/, '') || '/') === '/dashboard' + : isActivePath(pathname, href); + + if (href === '#') { + return ( + +
+ {label} +
+
+ ); + } + + if (external) { + return ( + + + {label} + + + ); + } + + return ( + + onMenuNavigate?.(href, node.menu_name)} + aria-current={active ? 'page' : undefined} + > + {label} + + + ); + })} +
+
+ ); +} + +/** L2 起:fixed 根与一级行顶对齐;更深层级在面板内 absolute,与对应行顶对齐;无内部滚动 */ +export function ClassicCascadeFlyoutPortal({ + rootNode, + anchorRect, + pathname, + onMenuNavigate, + onHoverEnter, + onHoverLeave, +}: { + rootNode: MenuNode; + anchorRect: DOMRectReadOnly; + pathname: string; + onMenuNavigate?: (path: string, title: string) => void; + onHoverEnter: () => void; + onHoverLeave: () => void; +}) { + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + + if (!mounted || typeof document === 'undefined') { + return null; + } + + const nodes = visibleChildren(rootNode); + if (nodes.length === 0) { + return null; + } + + /** 左缘与一级导航右缘对齐,不向左叠在窄轨上(不再用负 overlap) */ + const left = anchorRect.right; + + return createPortal( +
+
+ +
+
, + document.body, + ); +} diff --git a/web/components/layout/ClassicCollapsedSidebar.tsx b/web/components/layout/ClassicCollapsedSidebar.tsx new file mode 100644 index 0000000..db3533d --- /dev/null +++ b/web/components/layout/ClassicCollapsedSidebar.tsx @@ -0,0 +1,190 @@ +'use client'; + +import { useMemo } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import type { MenuNode } from '@/lib/api/types/menu'; +import { ClassicCascadeFlyoutPortal } from '@/components/layout/ClassicCascadeFlyout'; +import { NavTooltip } from '@/components/layout/nav-tooltip'; +import { + NavIconOrLabel, + NavStateGuard, + normalizeHref, + layoutNavRootsFromApi, + subtreeContainsActivePath, + visibleChildren, +} from '@/components/layout/nav-shared'; +import { useFlyoutState } from '@/lib/hooks/use-flyout-state'; +import { useMenuNavigation } from '@/lib/hooks/use-menu-navigation'; +import { useLayoutStore } from '@/stores/layout-store'; + +const RAIL_W = 'w-[72px]'; + +export function ClassicCollapsedSidebar({ + items, + loading, + error, + authed, +}: { + items: MenuNode[]; + loading: boolean; + error: string | null; + authed: boolean; + onMenuNavigate?: (path: string, title: string) => void; +}) { + const pathname = usePathname() ?? '/'; + const roots = useMemo(() => layoutNavRootsFromApi(items), [items]); + const toggleClassicNavRail = useLayoutStore((s) => s.toggleClassicNavRail); + const onMenuNavigate = useMenuNavigation(); + + const { + flyoutRoot, + l1AnchorRect, + openFlyout, + scheduleCloseFlyout, + clearCloseTimer, + closeFlyoutNow, + toggleFlyoutClick, + railScrollRef, + } = useFlyoutState(pathname); + + const railShell = (inner: React.ReactNode) => ( +
+ {inner} +
+ + + +
+
+ ); + + if (!authed || loading || error) { + return ( +
+ {railShell( + , + )} +
+ ); + } + + return ( +
+ {railShell( +
+ +
, + )} + + {flyoutRoot && l1AnchorRect && visibleChildren(flyoutRoot).length > 0 ? ( + + ) : null} + +
+ ); +} diff --git a/web/components/layout/IconSidebarLayout.tsx b/web/components/layout/IconSidebarLayout.tsx new file mode 100644 index 0000000..cb650c8 --- /dev/null +++ b/web/components/layout/IconSidebarLayout.tsx @@ -0,0 +1,180 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import type { MenuNode } from '@/lib/api/types/menu'; +import { NavTooltip } from '@/components/layout/nav-tooltip'; +import { + NavIconOrLabel, + NavStateGuard, + NavTreeItem, + normalizeHref, + layoutNavRootsFromApi, + subtreeContainsActivePath, + visibleChildren, +} from '@/components/layout/nav-shared'; +import { useMenuNavigation } from '@/lib/hooks/use-menu-navigation'; + +export function IconSidebarLayout({ + items, + loading, + error, + authed, + onCloseMobile, +}: { + items: MenuNode[]; + loading: boolean; + error: string | null; + authed: boolean; + onMenuNavigate?: (path: string, title: string) => void; + onCloseMobile?: () => void; +}) { + const pathname = usePathname() ?? '/'; + const roots = useMemo(() => layoutNavRootsFromApi(items), [items]); + const onMenuNavigate = useMenuNavigation(); + + const findFlyoutRootForPath = useCallback( + (path: string): MenuNode | null => { + for (const r of roots) { + const kids = visibleChildren(r); + if (kids.length === 0) continue; + if (subtreeContainsActivePath(r, path)) return r; + } + return null; + }, + [roots], + ); + + const [flyoutRoot, setFlyoutRoot] = useState(null); + + useEffect(() => { + const next = findFlyoutRootForPath(pathname); + setFlyoutRoot((prev) => { + if (next) return next; + if (prev && subtreeContainsActivePath(prev, pathname)) return prev; + return null; + }); + }, [pathname, findFlyoutRootForPath]); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setFlyoutRoot(null); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, []); + + const toggleFlyout = (root: MenuNode) => { + setFlyoutRoot((r) => (r?.id === root.id ? null : root)); + }; + + const railShell = (inner: React.ReactNode) => ( +
+ {inner} +
+ ); + + if (!authed || loading || error) { + return railShell( + , + ); + } + + return ( +
+
+
+ +
+
+ + {flyoutRoot && visibleChildren(flyoutRoot).length > 0 ? ( +
+
+
{flyoutRoot.menu_name}
+
+
+ {visibleChildren(flyoutRoot).map((c) => ( + + ))} +
+
+ ) : null} +
+ ); +} diff --git a/web/components/layout/NavHeader.tsx b/web/components/layout/NavHeader.tsx new file mode 100644 index 0000000..8b8396c --- /dev/null +++ b/web/components/layout/NavHeader.tsx @@ -0,0 +1,18 @@ +'use client'; + +export function NavHeader({ onCloseMobile }: { onCloseMobile?: () => void }) { + return ( +
+ {onCloseMobile ? ( + + ) : null} +
+ ); +} diff --git a/web/components/layout/RequireAuth.tsx b/web/components/layout/RequireAuth.tsx new file mode 100644 index 0000000..49ccbad --- /dev/null +++ b/web/components/layout/RequireAuth.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { usePathname, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { useAuthStore } from '@/stores/auth-store'; + +export function RequireAuth({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const pathname = usePathname(); + const accessToken = useAuthStore((s) => s.accessToken); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted) { + return; + } + if (!accessToken) { + const from = pathname || '/dashboard'; + router.replace(`/login?from=${encodeURIComponent(from)}`); + } + }, [mounted, accessToken, pathname, router]); + + if (!mounted) { + return ( +
+ 加载中… +
+ ); + } + + if (!accessToken) { + return ( +
+ 跳转登录… +
+ ); + } + + return <>{children}; +} diff --git a/web/components/layout/SidebarNav.tsx b/web/components/layout/SidebarNav.tsx new file mode 100644 index 0000000..6fcbdd1 --- /dev/null +++ b/web/components/layout/SidebarNav.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { usePathname } from 'next/navigation'; +import type { MenuNode } from '@/lib/api/types/menu'; +import { + isLayoutOverviewNode, + NavTreeItem, + layoutNavRootsFromApi, +} from '@/components/layout/nav-shared'; + +export function SidebarNav(props: { + items: MenuNode[]; + loading: boolean; + error: string | null; + authed: boolean; + onInternalNavigate?: () => void; + onMenuNavigate?: (path: string, title: string) => void; +}) { + const pathname = usePathname() || ''; + + if (!props.authed) { + return
登录后加载侧栏菜单
; + } + + if (props.loading) { + return
菜单加载中…
; + } + + if (props.error) { + const roots = layoutNavRootsFromApi([]); + return ( + + ); + } + + const roots = layoutNavRootsFromApi(props.items); + const onlyOverview = roots.length === 1 && isLayoutOverviewNode(roots[0]); + + return ( + + ); +} diff --git a/web/components/layout/TabStrip.tsx b/web/components/layout/TabStrip.tsx new file mode 100644 index 0000000..fe8ab93 --- /dev/null +++ b/web/components/layout/TabStrip.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { usePathname, useRouter } from 'next/navigation'; +import { useCallback, useEffect, useRef } from 'react'; +import { useTabStore } from '@/stores/tab-store'; + +/** 多页签条:与路由联动;左右滚动占位(后续可接完整滚动实现)。 */ +export function TabStrip() { + const router = useRouter(); + const pathname = usePathname() ?? ''; + const scrollRef = useRef(null); + const { tabs, activeId, close, activate } = useTabStore(); + + useEffect(() => { + if (!pathname.startsWith('/dashboard')) { + return; + } + useTabStore.getState().syncFromPath(pathname); + }, [pathname]); + + const onTabClick = useCallback( + (id: string, path: string) => { + activate(id); + router.push(path); + }, + [activate, router], + ); + + const scrollBy = (delta: number) => { + scrollRef.current?.scrollBy({ left: delta, behavior: 'smooth' }); + }; + + return ( +
+ +
+ {tabs.map((t) => { + const isActive = activeId === t.id; + return ( + + ); + })} +
+ +
+ ); +} diff --git a/web/components/layout/TenantSwitcher.tsx b/web/components/layout/TenantSwitcher.tsx new file mode 100644 index 0000000..cec44ac --- /dev/null +++ b/web/components/layout/TenantSwitcher.tsx @@ -0,0 +1,101 @@ +'use client'; + +import * as Select from '@radix-ui/react-select'; +import { useCallback, useEffect, useState } from 'react'; +import { iamTenant } from '@/lib/api/iam'; +import type { IamTenant } from '@/lib/api/types/tenant'; +import { useTenantStore } from '@/stores/tenant-store'; +import { useTabStore } from '@/stores/tab-store'; + +/** Radix Select 不允许空字符串作 value,用哨兵表示「默认租户」 */ +const DEFAULT_TENANT_VALUE = '__tenant_default__'; + +export function TenantSwitcher() { + const tenantId = useTenantStore((s) => s.tenantId); + const setTenantId = useTenantStore((s) => s.setTenantId); + const resetTabsForTenantSwitch = useTabStore((s) => s.resetForTenantSwitch); + const [rows, setRows] = useState(null); + const [loadErr, setLoadErr] = useState(false); + + const load = useCallback(async () => { + try { + const data = await iamTenant.list(); + setRows(data.items ?? []); + setLoadErr(false); + } catch { + setLoadErr(true); + setRows([]); + } + }, []); + + useEffect(() => { + void load(); + }, [load]); + + const short = tenantId ? (tenantId.length > 10 ? `${tenantId.slice(0, 8)}…` : tenantId) : '默认'; + + if (loadErr || !rows || rows.length === 0) { + return ( +
+ 租户 {short} +
+ ); + } + + return ( +
+ + 租户 + + { + setTenantId(v === DEFAULT_TENANT_VALUE ? null : v); + resetTabsForTenantSwitch(); + }} + > + + + + + + + + + + + 默认 + + {rows.map((r) => ( + + + {r.tenant_name || r.tenant_code || r.id} + + + ))} + + + + +
+ ); +} diff --git a/web/components/layout/UserMenu.tsx b/web/components/layout/UserMenu.tsx new file mode 100644 index 0000000..6e8c46a --- /dev/null +++ b/web/components/layout/UserMenu.tsx @@ -0,0 +1,127 @@ +'use client'; + +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import Link from 'next/link'; +import { useState } from 'react'; +import { avatarInitials, useUserProfile } from '@/lib/hooks/use-user-profile'; +import { useMenuNavigation } from '@/lib/hooks/use-menu-navigation'; +import { useAuthStore } from '@/stores/auth-store'; +import { useLayoutStore } from '@/stores/layout-store'; +import { useTabStore } from '@/stores/tab-store'; + +export function UserMenu() { + const logout = useAuthStore((s) => s.logout); + const sidebarMode = useLayoutStore((s) => s.sidebarMode); + const setSidebarMode = useLayoutStore((s) => s.setSidebarMode); + const { profile, userSub, loading, label } = useUserProfile(); + const [open, setOpen] = useState(false); + const onMenuNavigate = useMenuNavigation(); + const initials = avatarInitials(profile, userSub); + + return ( + + + + + + + +
+

{label}

+ {profile?.email ? ( +

{profile.email}

+ ) : null} + {!profile?.email && userSub ? ( +

+ ID {userSub} +

+ ) : null} +
+ + + onMenuNavigate('/dashboard', '概览')} + > + 工作台 + + + + onMenuNavigate('/dashboard/account', '个人中心')} + > + 个人中心 + + + + + +
+

侧栏布局

+
+ + +
+
+ + { + void logout(); + }} + > + 退出登录 + +
+
+
+ ); +} diff --git a/web/components/layout/nav-shared.tsx b/web/components/layout/nav-shared.tsx new file mode 100644 index 0000000..5f97804 --- /dev/null +++ b/web/components/layout/nav-shared.tsx @@ -0,0 +1,264 @@ +'use client'; + +import * as Collapsible from '@radix-ui/react-collapsible'; +import Link from 'next/link'; +import type { MenuNode } from '@/lib/api/types/menu'; +import { NavTooltip } from '@/components/layout/nav-tooltip'; + +/** 布局固定「概览」,与 IAM nav 拼成同一棵树(不入库) */ +export const LAYOUT_OVERVIEW_MENU_ID = '__layout_overview__'; + +export function isLayoutOverviewNode(node: MenuNode): boolean { + return node.id === LAYOUT_OVERVIEW_MENU_ID; +} + +export const layoutOverviewMenuNode: MenuNode = { + id: LAYOUT_OVERVIEW_MENU_ID, + parent_id: '', + menu_name: '概览', + menu_type: 2, + path: '/dashboard', + icon: '📊', +}; + +/** 将布局「概览」与 IAM nav 合并为侧栏顶层列表,便于组好完整结构再一次性渲染 */ +export function layoutNavRootsFromApi(items: MenuNode[]): MenuNode[] { + return [layoutOverviewMenuNode, ...items.filter((n) => n.menu_type !== 3)]; +} + +export function normalizeHref(node: MenuNode): string { + if (node.external_link) { + return node.external_link; + } + const p = node.path?.trim(); + if (!p) { + return '#'; + } + if (p.startsWith('http://') || p.startsWith('https://')) { + return p; + } + return p.startsWith('/') ? p : `/${p}`; +} + +export function isActivePath(pathname: string, href: string): boolean { + if (!href || href === '#' || href.startsWith('http://') || href.startsWith('https://')) { + return false; + } + if (pathname === href) { + return true; + } + if (href !== '/' && pathname.startsWith(`${href}/`)) { + return true; + } + return false; +} + +export function linkClass(active: boolean): string { + return [ + 'block rounded-md px-2 py-2 text-sm', + active + ? 'bg-neutral-200 font-medium text-neutral-900' + : 'text-neutral-800 hover:bg-neutral-100', + ].join(' '); +} + +/** 经典树:目录可展开,叶子为链接(用于经典侧栏与图标模式右侧浮层) */ +export function NavTreeItem({ + node, + depth, + pathname, + onInternalNavigate, + onMenuNavigate, + /** 图标模式浮层:二级及以下项之间额外增加 2px(space-y-0.5) */ + iconFlyout = false, +}: { + node: MenuNode; + depth: number; + pathname: string; + onInternalNavigate?: () => void; + onMenuNavigate?: (path: string, title: string) => void; + iconFlyout?: boolean; +}) { + if (node.menu_type === 3) { + return null; + } + + const children = (node.children ?? []).filter((c) => c.menu_type !== 3); + const hasChildren = children.length > 0; + const pad = 10 + depth * 10; + + const label = ( + + {node.icon ? ( + + {node.icon} + + ) : null} + {node.menu_name} + + ); + + if (hasChildren) { + return ( + + + + + +
+
+ {children.map((c) => ( + + ))} +
+
+
+
+ ); + } + + const href = normalizeHref(node); + const external = Boolean(node.external_link); + const active = isLayoutOverviewNode(node) + ? (pathname.replace(/\/$/, '') || '/') === '/dashboard' + : isActivePath(pathname, href); + + if (href === '#') { + return ( +
+ {label} +
+ ); + } + + if (external) { + return ( + + {label} + + ); + } + + return ( + { + onMenuNavigate?.(href, node.menu_name); + onInternalNavigate?.(); + }} + aria-current={active ? 'page' : undefined} + > + {label} + + ); +} + +export function visibleChildren(node: MenuNode): MenuNode[] { + return (node.children ?? []).filter((c) => c.menu_type !== 3); +} + +/** 当前路径是否落在该节点子树内(用于图标模式自动展开浮层) */ +export function subtreeContainsActivePath(node: MenuNode, pathname: string): boolean { + if (isLayoutOverviewNode(node)) { + const p = pathname.replace(/\/$/, '') || '/'; + return p === '/dashboard'; + } + const href = normalizeHref(node); + if (!node.external_link && href !== '#' && isActivePath(pathname, href)) { + return true; + } + for (const c of visibleChildren(node)) { + if (subtreeContainsActivePath(c, pathname)) { + return true; + } + } + return false; +} + +/** 窄轨图标/文字切换:有 icon 显示 icon,否则显示菜单名 */ +export function NavIconOrLabel({ node, className }: { node: MenuNode; className?: string }) { + if (node.icon) { + return ( + + {node.icon} + + ); + } + return {node.menu_name}; +} + +/** 侧栏未认证/加载中/出错三态守卫 */ +export function NavStateGuard({ + authed, + loading, + error, + onMenuNavigate, + children, +}: { + authed: boolean; + loading: boolean; + error: string | null; + onMenuNavigate: (path: string, title: string) => void; + children?: React.ReactNode; +}) { + if (!authed) { + return
登录后加载菜单
; + } + if (loading) { + return
菜单加载中…
; + } + if (error) { + return ( +
+ + onMenuNavigate('/dashboard', '概览')} + > + + 📊 + + 概览 + + + +

菜单失败

+
+
+ ); + } + return children; +} diff --git a/web/components/layout/nav-tooltip.tsx b/web/components/layout/nav-tooltip.tsx new file mode 100644 index 0000000..0f2c120 --- /dev/null +++ b/web/components/layout/nav-tooltip.tsx @@ -0,0 +1,37 @@ +'use client'; + +import * as Tooltip from '@radix-ui/react-tooltip'; +import type { ReactElement } from 'react'; + +type NavTooltipProps = { + label: string; + children: ReactElement; + /** 窄轨叶子项可设 0,与原先即时提示接近 */ + delayDuration?: number; + side?: 'top' | 'right' | 'bottom' | 'left'; +}; + +/** + * 侧栏图标/收起态等:替代原生 `title`,统一键盘可达与延时(受全局 TooltipProvider 影响可被 Root 覆盖)。 + */ +export function NavTooltip({ label, children, delayDuration = 300, side = 'right' }: NavTooltipProps) { + if (!label.trim()) { + return children; + } + + return ( + + {children} + + + {label} + + + + + ); +} diff --git a/web/components/providers/AppProviders.tsx b/web/components/providers/AppProviders.tsx new file mode 100644 index 0000000..bc633c3 --- /dev/null +++ b/web/components/providers/AppProviders.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { TooltipProvider } from '@radix-ui/react-tooltip'; +import { usePathname, useRouter } from 'next/navigation'; +import { useEffect } from 'react'; +import { LoginModal } from '@/components/auth/LoginModal'; +import { ToastHost } from '@/components/feedback/ToastHost'; +import { ErrorBoundary } from '@/components/providers/ErrorBoundary'; +import { registerAuthEvents } from '@/lib/notify/auth-events'; +import { subscribeRemoteLogout } from '@/lib/sync/logout-broadcast'; +import { useAuthStore } from '@/stores/auth-store'; +import { useAuthUiStore } from '@/stores/auth-ui-store'; +import { useToastStore } from '@/stores/toast-store'; + +export function AppProviders({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const pathname = usePathname() ?? ''; + + useEffect(() => { + registerAuthEvents({ + on401: (msg) => { + useAuthUiStore.getState().openLoginModal(msg); + }, + on403: (msg) => { + useToastStore.getState().show(msg || '禁止访问', 'error'); + }, + }); + }, []); + + useEffect(() => { + return subscribeRemoteLogout(() => { + useAuthStore.getState().setTokens(null, null); + useToastStore.getState().show('已在其他标签退出登录', 'info'); + if (pathname.startsWith('/dashboard')) { + router.replace(`/login?from=${encodeURIComponent(pathname)}`); + } + }); + }, [pathname, router]); + + return ( + + {children} + + + + ); +} diff --git a/web/components/providers/ErrorBoundary.tsx b/web/components/providers/ErrorBoundary.tsx new file mode 100644 index 0000000..9b44cb6 --- /dev/null +++ b/web/components/providers/ErrorBoundary.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { Component, type ReactNode } from 'react'; + +type Props = { + children: ReactNode; + fallback?: ReactNode; +}; + +type State = { + hasError: boolean; + error: Error | null; +}; + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, info: React.ErrorInfo) { + console.error('[ErrorBoundary]', error, info.componentStack); + } + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + return ( +
+

页面出现异常

+ +
+ ); + } + return this.props.children; + } +} diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs new file mode 100644 index 0000000..025f5d9 --- /dev/null +++ b/web/eslint.config.mjs @@ -0,0 +1,21 @@ +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { FlatCompat } from '@eslint/eslintrc'; +import eslintConfigPrettier from 'eslint-config-prettier'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends('next/core-web-vitals', 'next/typescript'), + eslintConfigPrettier, + { + ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'], + }, +]; + +export default eslintConfig; diff --git a/web/lib/api/auth.ts b/web/lib/api/auth.ts new file mode 100644 index 0000000..3980313 --- /dev/null +++ b/web/lib/api/auth.ts @@ -0,0 +1,184 @@ +import { getOAuthClientId, getOAuthRedirectUri, getPublicApiOrigin } from '@/lib/env'; +import { createPKCEPair } from '@/lib/oauth/pkce'; +import type { ApiEnvelope } from '@/lib/api/types'; + +export type TokenPair = { + accessToken: string; + refreshToken: string; + expiresIn: number; +}; + +/** 用户名密码 + PKCE 换 authorization_code,再 POST /oauth/token。 */ +export async function loginWithPassword(params: { + userName: string; + password: string; + tenantId?: string; + state?: string; +}): Promise { + const origin = getPublicApiOrigin(); + const clientId = getOAuthClientId(); + const redirectUri = getOAuthRedirectUri(); + const { verifier, challenge } = await createPKCEPair(); + + const res = await fetch(`${origin}/api/v1/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + tenant_id: params.tenantId ?? '', + user_name: params.userName, + password: params.password, + client_id: clientId, + redirect_uri: redirectUri, + code_challenge: challenge, + code_challenge_method: 'S256', + state: params.state ?? '', + scope: 'openid', + }), + }); + + const json = (await res.json()) as ApiEnvelope<{ + authorization_code?: string; + state?: string; + }>; + + if (res.status === 401) { + throw new Error(json.msg || '用户名或密码错误'); + } + if (res.status === 403) { + throw new Error(json.msg || '无权限'); + } + if (!res.ok) { + throw new Error(json.msg || `HTTP ${res.status}`); + } + if (json.code !== 200 || !json.data?.authorization_code) { + throw new Error(json.msg || '登录失败'); + } + + return exchangeCodeForTokens({ + code: json.data.authorization_code, + codeVerifier: verifier, + clientId, + redirectUri, + }); +} + +export async function exchangeCodeForTokens(params: { + code: string; + codeVerifier: string; + clientId: string; + redirectUri: string; +}): Promise { + const origin = getPublicApiOrigin(); + const body = new URLSearchParams({ + grant_type: 'authorization_code', + code: params.code, + redirect_uri: params.redirectUri, + client_id: params.clientId, + code_verifier: params.codeVerifier, + }); + + const res = await fetch(`${origin}/oauth/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + }); + + const data = (await res.json()) as { + access_token?: string; + refresh_token?: string; + expires_in?: number; + error?: string; + error_description?: string; + }; + + if (!res.ok || !data.access_token) { + throw new Error(data.error_description || data.error || '换取 access_token 失败'); + } + + return { + accessToken: data.access_token, + refreshToken: data.refresh_token ?? '', + expiresIn: typeof data.expires_in === 'number' ? data.expires_in : 0, + }; +} + +export async function refreshAccessToken(refreshToken: string): Promise { + const origin = getPublicApiOrigin(); + const clientId = getOAuthClientId(); + const body = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id: clientId, + }); + + const res = await fetch(`${origin}/oauth/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + }); + + const data = (await res.json()) as { + access_token?: string; + refresh_token?: string; + expires_in?: number; + error?: string; + error_description?: string; + }; + + if (!res.ok || !data.access_token) { + throw new Error(data.error_description || data.error || '刷新 token 失败'); + } + + return { + accessToken: data.access_token, + refreshToken: data.refresh_token ?? refreshToken, + expiresIn: typeof data.expires_in === 'number' ? data.expires_in : 0, + }; +} + +export async function logoutRequest(): Promise { + const origin = getPublicApiOrigin(); + const res = await fetch(`${origin}/api/v1/auth/logout`, { + method: 'POST', + credentials: 'include', + }); + if (!res.ok) { + const j = (await res.json().catch(() => ({}))) as { msg?: string }; + throw new Error(j.msg || '登出失败'); + } +} + +/** RFC 7662 风格,用于解析 opaque access_token 的 `sub`(用户 id) */ +export async function introspectAccessToken(accessToken: string): Promise<{ + active: boolean; + sub?: string; + scope?: string; + exp?: number; + client_id?: string; +}> { + const origin = getPublicApiOrigin(); + const body = new URLSearchParams({ + token: accessToken, + token_type_hint: 'access_token', + }); + const res = await fetch(`${origin}/oauth/introspect`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + }); + const data = (await res.json()) as { + active?: boolean; + sub?: string; + scope?: string; + exp?: number; + client_id?: string; + }; + return { + active: Boolean(data.active), + sub: data.sub, + scope: data.scope, + exp: data.exp, + client_id: data.client_id, + }; +} diff --git a/web/lib/api/client.ts b/web/lib/api/client.ts new file mode 100644 index 0000000..b6dd9a2 --- /dev/null +++ b/web/lib/api/client.ts @@ -0,0 +1,111 @@ +import { getPublicApiOrigin } from '@/lib/env'; +import { emit401Unauthorized, emit403Forbidden } from '@/lib/notify/auth-events'; +import { refreshTokensShared } from '@/lib/api/refresh-flight'; +import { ApiError, type ApiEnvelope } from '@/lib/api/types'; + +type GetTokens = () => { accessToken: string | null; refreshToken: string | null }; +type SetTokens = (access: string | null, refresh: string | null) => void; + +let tokenBridge: { get: GetTokens; set: SetTokens } | null = null; + +/** 由 auth store 在客户端挂载时注册,避免循环依赖。 */ +export function registerTokenBridge(get: GetTokens, set: SetTokens) { + tokenBridge = { get, set }; +} + +let tenantIdGetter: () => string | null = () => null; + +/** 由 tenant store 注册,请求 `/api/v1` 时附加 `X-Tenant-ID`(可选)。 */ +export function registerTenantHeaderBridge(get: () => string | null) { + tenantIdGetter = get; +} + +function authHeader(): HeadersInit { + const t = tokenBridge?.get().accessToken; + return t ? { Authorization: `Bearer ${t}` } : {}; +} + +function tenantHeader(): HeadersInit { + const tid = tenantIdGetter?.(); + return tid ? { 'X-Tenant-ID': tid } : {}; +} + +/** 业务 JSON:HTTP 200 时解析 envelope;401 时共享 refresh 后重试一次(登录请求勿走此逻辑)。 */ +export async function apiJson( + path: string, + init: RequestInit & { skipRefreshRetry?: boolean } = {} +): Promise { + const { skipRefreshRetry, ...req } = init; + const url = `${getPublicApiOrigin()}${path.startsWith('/') ? path : `/${path}`}`; + const method = (req.method || 'GET').toUpperCase(); + const hasBody = req.body != null && req.body !== ''; + const jsonHeaders: HeadersInit = + method !== 'GET' && method !== 'HEAD' && hasBody ? { 'Content-Type': 'application/json' } : {}; + const baseHeaders = { + ...jsonHeaders, + ...authHeader(), + ...tenantHeader(), + ...(req.headers || {}), + }; + + const first = await fetch(url, { + ...req, + headers: baseHeaders, + credentials: 'include', + }); + + if (first.status === 401 && !skipRefreshRetry && tokenBridge) { + const ok = await refreshTokensShared( + () => tokenBridge!.get().refreshToken, + (a, r) => tokenBridge!.set(a, r) + ); + if (ok) { + const second = await fetch(url, { + ...req, + headers: { + ...jsonHeaders, + ...authHeader(), + ...tenantHeader(), + ...(req.headers || {}), + }, + credentials: 'include', + }); + return handleEnvelope(second); + } + } + + return handleEnvelope(first); +} + +async function handleEnvelope(res: Response): Promise { + const text = await res.text(); + let body: unknown = null; + try { + body = text ? JSON.parse(text) : null; + } catch { + throw new ApiError('响应不是合法 JSON', res.status, text); + } + + if (res.status === 401) { + const msg = (body as { msg?: string })?.msg || '未授权'; + emit401Unauthorized(msg); + throw new ApiError(msg, 401, body); + } + if (res.status === 403) { + const msg = (body as { msg?: string })?.msg || '禁止访问'; + emit403Forbidden(msg); + throw new ApiError(msg, 403, body); + } + if (!res.ok) { + throw new ApiError(`HTTP ${res.status}`, res.status, body); + } + + const env = body as ApiEnvelope; + if (typeof env?.code !== 'number') { + return body as T; + } + if (env.code !== 200) { + throw new ApiError(env.msg || '业务失败', res.status, env); + } + return env.data as T; +} diff --git a/web/lib/api/iam.ts b/web/lib/api/iam.ts new file mode 100644 index 0000000..62417cb --- /dev/null +++ b/web/lib/api/iam.ts @@ -0,0 +1,99 @@ +import { apiJson } from '@/lib/api/client'; +import { API_V1 } from '@/lib/api/paths'; +import type { DeptNode, IamDept } from '@/lib/api/types/dept'; +import type { MenuNode } from '@/lib/api/types/menu'; +import type { IamRole } from '@/lib/api/types/role'; +import type { IamTenant } from '@/lib/api/types/tenant'; +import type { IamUser } from '@/lib/api/types/user'; + +const B = `${API_V1}/iam`; + +/** 列表响应信封 */ +type ListEnvelope = { items: T[]; total: number }; + +/** 租户 */ +export const iamTenant = { + create: (body: Partial) => + apiJson(`${B}/tenant/create`, { method: 'POST', body: JSON.stringify(body) }), + update: (id: string, body: Partial) => + apiJson(`${B}/tenant/update/${encodeURIComponent(id)}`, { + method: 'PUT', + body: JSON.stringify(body), + }), + deleteBatch: (ids: string[]) => + apiJson(`${B}/tenant/delete-batch`, { method: 'DELETE', body: JSON.stringify(ids) }), + get: (id: string) => apiJson(`${B}/tenant/get/${encodeURIComponent(id)}`), + list: (query?: Record) => { + const q = query ? new URLSearchParams(query).toString() : 'page=1&page_size=200'; + return apiJson>(`${B}/tenant/list?${q}`); + }, +}; + +/** 部门 */ +export const iamDept = { + create: (body: Partial) => + apiJson(`${B}/dept/create`, { method: 'POST', body: JSON.stringify(body) }), + update: (id: string, body: Partial) => + apiJson(`${B}/dept/update/${encodeURIComponent(id)}`, { + method: 'PUT', + body: JSON.stringify(body), + }), + deleteBatch: (ids: string[]) => + apiJson(`${B}/dept/delete-batch`, { method: 'DELETE', body: JSON.stringify(ids) }), + get: (id: string) => apiJson(`${B}/dept/get/${encodeURIComponent(id)}`), + tree: () => apiJson(`${B}/dept/tree`), +}; + +/** 角色 */ +export const iamRole = { + create: (body: Partial) => + apiJson(`${B}/role/create`, { method: 'POST', body: JSON.stringify(body) }), + update: (id: string, body: Partial) => + apiJson(`${B}/role/update/${encodeURIComponent(id)}`, { + method: 'PUT', + body: JSON.stringify(body), + }), + deleteBatch: (ids: string[]) => + apiJson(`${B}/role/delete-batch`, { method: 'DELETE', body: JSON.stringify(ids) }), + get: (id: string) => apiJson(`${B}/role/get/${encodeURIComponent(id)}`), + list: (query?: Record) => { + const q = query ? new URLSearchParams(query).toString() : 'page=1&page_size=50'; + return apiJson>(`${B}/role/list?${q}`); + }, +}; + +/** 用户 */ +export const iamUser = { + create: (body: Partial) => + apiJson(`${B}/user/create`, { method: 'POST', body: JSON.stringify(body) }), + update: (id: string, body: Partial) => + apiJson(`${B}/user/update/${encodeURIComponent(id)}`, { + method: 'PUT', + body: JSON.stringify(body), + }), + deleteBatch: (ids: string[]) => + apiJson(`${B}/user/delete-batch`, { method: 'DELETE', body: JSON.stringify(ids) }), + get: (id: string) => apiJson(`${B}/user/get/${encodeURIComponent(id)}`), + list: (query?: Record) => { + const q = query ? new URLSearchParams(query).toString() : 'page=1&page_size=20'; + return apiJson>(`${B}/user/list?${q}`); + }, +}; + +/** 菜单 */ +export const iamMenu = { + create: (body: Partial) => + apiJson(`${B}/menu/create`, { method: 'POST', body: JSON.stringify(body) }), + update: (id: string, body: Partial) => + apiJson(`${B}/menu/update/${encodeURIComponent(id)}`, { + method: 'PUT', + body: JSON.stringify(body), + }), + deleteBatch: (ids: string[]) => + apiJson(`${B}/menu/delete-batch`, { method: 'DELETE', body: JSON.stringify(ids) }), + get: (id: string) => apiJson(`${B}/menu/get/${encodeURIComponent(id)}`), + tree: () => apiJson(`${B}/menu/tree`), + /** 当前用户可见导航树(需 Bearer,后端从 token 解析 user_id) */ + nav: () => apiJson(`${B}/menu/nav`), + perms: () => apiJson<{ perms: string[] }>(`${B}/menu/perms`), +}; diff --git a/web/lib/api/paths.ts b/web/lib/api/paths.ts new file mode 100644 index 0000000..bf3b08a --- /dev/null +++ b/web/lib/api/paths.ts @@ -0,0 +1,2 @@ +/** 与 Go `apiGroup` 前缀一致 */ +export const API_V1 = '/api/v1'; diff --git a/web/lib/api/refresh-flight.ts b/web/lib/api/refresh-flight.ts new file mode 100644 index 0000000..dc57371 --- /dev/null +++ b/web/lib/api/refresh-flight.ts @@ -0,0 +1,33 @@ +import { refreshAccessToken } from '@/lib/api/auth'; + +let inFlight: Promise | null = null; + +/** + * 并发 401 时共享同一次 refresh,避免重复 POST /oauth/token。 + * refresh 失败会清空 token(由调用方传入的 setTokens 执行)。 + */ +export function refreshTokensShared( + getRefreshToken: () => string | null, + setTokens: (access: string | null, refresh: string | null) => void +): Promise { + if (inFlight) { + return inFlight; + } + inFlight = (async () => { + try { + const rt = getRefreshToken(); + if (!rt) { + return false; + } + const pair = await refreshAccessToken(rt); + setTokens(pair.accessToken, pair.refreshToken); + return true; + } catch { + setTokens(null, null); + return false; + } finally { + inFlight = null; + } + })(); + return inFlight; +} diff --git a/web/lib/api/system-param.ts b/web/lib/api/system-param.ts new file mode 100644 index 0000000..530d664 --- /dev/null +++ b/web/lib/api/system-param.ts @@ -0,0 +1,27 @@ +import { apiJson } from '@/lib/api/client'; +import { API_V1 } from '@/lib/api/paths'; +import type { SystemParam } from '@/lib/api/types/system-param'; + +const B = `${API_V1}/system/param`; + +type ListEnvelope = { items: T[]; total: number }; + +export const systemParam = { + create: (body: Partial) => + apiJson(`${B}/create`, { method: 'POST', body: JSON.stringify(body) }), + update: (id: string, body: Partial) => + apiJson(`${B}/update/${encodeURIComponent(id)}`, { + method: 'PUT', + body: JSON.stringify(body), + }), + deleteBatch: (ids: string[]) => + apiJson(`${B}/delete-batch`, { method: 'DELETE', body: JSON.stringify(ids) }), + get: (query: Record) => { + const q = new URLSearchParams(query).toString(); + return apiJson(`${B}/get?${q}`); + }, + list: (query?: Record) => { + const q = query ? new URLSearchParams(query).toString() : ''; + return apiJson>(q ? `${B}/list?${q}` : `${B}/list`); + }, +}; diff --git a/web/lib/api/types.ts b/web/lib/api/types.ts new file mode 100644 index 0000000..1e9de08 --- /dev/null +++ b/web/lib/api/types.ts @@ -0,0 +1,17 @@ +/** 与 Go `/api/v1` JSON 信封一致(见 docs/auth-api.md) */ +export type ApiEnvelope = { + code: number; + msg: string; + data: T | null; +}; + +export class ApiError extends Error { + constructor( + message: string, + public readonly httpStatus: number, + public readonly body?: unknown + ) { + super(message); + this.name = 'ApiError'; + } +} diff --git a/web/lib/api/types/dept.ts b/web/lib/api/types/dept.ts new file mode 100644 index 0000000..2bd2e94 --- /dev/null +++ b/web/lib/api/types/dept.ts @@ -0,0 +1,18 @@ +/** 与 Go `iam/entity.Dept` JSON 对齐 */ +export type IamDept = { + id: string; + tenant_id: string; + parent_id: string; + dept_name: string; + dept_path: string; + leader_id?: string | null; + sort_order: number; + status: number; + created_at: string; + updated_at: string; +}; + +/** 与 Go `iam/service.DeptNode` JSON 对齐(递归树) */ +export type DeptNode = IamDept & { + children?: DeptNode[]; +}; diff --git a/web/lib/api/types/menu.ts b/web/lib/api/types/menu.ts new file mode 100644 index 0000000..d900c8f --- /dev/null +++ b/web/lib/api/types/menu.ts @@ -0,0 +1,15 @@ +/** 与 Go `iam/service.MenuNode` / `entity.Menu` JSON 对齐 */ +export type MenuNode = { + id: string; + parent_id: string; + menu_name: string; + menu_type: number; + perms?: string; + path: string; + component?: string; + icon?: string; + sort_order?: number; + is_visible?: boolean; + external_link?: string; + children?: MenuNode[]; +}; diff --git a/web/lib/api/types/role.ts b/web/lib/api/types/role.ts new file mode 100644 index 0000000..98c2783 --- /dev/null +++ b/web/lib/api/types/role.ts @@ -0,0 +1,13 @@ +/** 与 Go `iam/entity.Role` JSON 对齐 */ +export type IamRole = { + id: string; + tenant_id: string; + role_code: string; + role_name: string; + data_scope: number; + description: string; + is_builtin: boolean; + status: number; + created_at: string; + updated_at: string; +}; diff --git a/web/lib/api/types/system-param.ts b/web/lib/api/types/system-param.ts new file mode 100644 index 0000000..322c3f6 --- /dev/null +++ b/web/lib/api/types/system-param.ts @@ -0,0 +1,13 @@ +/** 与 Go `system/entity.SystemParam` JSON 对齐 */ +export type SystemParam = { + id: string; + param_key: string; + param_value: string; + param_type: string; + param_group: string; + param_desc: string; + creator_id: string; + create_time?: string | null; + last_updater_id: string; + update_time?: string | null; +}; diff --git a/web/lib/api/types/tenant.ts b/web/lib/api/types/tenant.ts new file mode 100644 index 0000000..cb56382 --- /dev/null +++ b/web/lib/api/types/tenant.ts @@ -0,0 +1,11 @@ +/** 与 Go `iam/entity.Tenant` JSON 对齐 */ +export type IamTenant = { + id: string; + tenant_code: string; + tenant_name: string; + admin_user_id?: string | null; + status: number; + expire_time?: string | null; + created_at: string; + updated_at: string; +}; diff --git a/web/lib/api/types/user.ts b/web/lib/api/types/user.ts new file mode 100644 index 0000000..9c05c7b --- /dev/null +++ b/web/lib/api/types/user.ts @@ -0,0 +1,13 @@ +/** 与后端 iam_user JSON 对齐(展示用字段子集) */ +export type IamUser = { + id: string; + tenant_id: string; + dept_id?: string | null; + user_name: string; + real_name?: string; + phone?: string; + email?: string; + avatar?: string; + gender?: number; + status?: number; +}; diff --git a/web/lib/env.ts b/web/lib/env.ts new file mode 100644 index 0000000..975d60f --- /dev/null +++ b/web/lib/env.ts @@ -0,0 +1,30 @@ +/** 浏览器可读的 API 根(scheme + host + port),不含路径。生产环境务必配置 NEXT_PUBLIC_API_ORIGIN。 */ +export function getPublicApiOrigin(): string { + const o = + process.env.NEXT_PUBLIC_API_ORIGIN || + (process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:8000' : ''); + if (!o) { + throw new Error('NEXT_PUBLIC_API_ORIGIN is not set'); + } + return o.replace(/\/$/, ''); +} + +export function getOAuthClientId(): string { + const id = + process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID || + (process.env.NODE_ENV === 'development' ? 'spa' : ''); + if (!id) { + throw new Error('NEXT_PUBLIC_OAUTH_CLIENT_ID is not set'); + } + return id; +} + +export function getOAuthRedirectUri(): string { + const u = + process.env.NEXT_PUBLIC_OAUTH_REDIRECT_URI || + (process.env.NODE_ENV === 'development' ? 'http://localhost:3000/oauth/callback' : ''); + if (!u) { + throw new Error('NEXT_PUBLIC_OAUTH_REDIRECT_URI is not set'); + } + return u; +} diff --git a/web/lib/hooks/use-api.ts b/web/lib/hooks/use-api.ts new file mode 100644 index 0000000..3e9c0a1 --- /dev/null +++ b/web/lib/hooks/use-api.ts @@ -0,0 +1,60 @@ +'use client'; + +import { useCallback, useEffect, useRef, useState } from 'react'; + +type UseApiState = { + data: T | null; + loading: boolean; + error: string | null; +}; + +/** + * 通用数据获取 hook,封装 loading / error / data 状态 + AbortController 取消。 + * + * @param fetcher 返回 Promise 的数据获取函数,接收 AbortSignal + * @param deps 依赖数组,变化时重新请求 + */ +export function useApi( + fetcher: (signal?: AbortSignal) => Promise, + deps: unknown[] = [], +): UseApiState & { refetch: () => void } { + const [state, setState] = useState>({ + data: null, + loading: true, + error: null, + }); + + const abortRef = useRef(null); + const fetcherRef = useRef(fetcher); + fetcherRef.current = fetcher; + + const load = useCallback(() => { + abortRef.current?.abort(); + const ac = new AbortController(); + abortRef.current = ac; + + setState((s) => ({ ...s, loading: true, error: null })); + + fetcherRef + .current(ac.signal) + .then((data) => { + if (!ac.signal.aborted) { + setState({ data, loading: false, error: null }); + } + }) + .catch((e: unknown) => { + if (!ac.signal.aborted) { + setState({ data: null, loading: false, error: e instanceof Error ? e.message : String(e) }); + } + }); + }, deps); + + useEffect(() => { + load(); + return () => { + abortRef.current?.abort(); + }; + }, [load]); + + return { ...state, refetch: load }; +} diff --git a/web/lib/hooks/use-flyout-state.ts b/web/lib/hooks/use-flyout-state.ts new file mode 100644 index 0000000..96131d1 --- /dev/null +++ b/web/lib/hooks/use-flyout-state.ts @@ -0,0 +1,129 @@ +'use client'; + +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import type { MenuNode } from '@/lib/api/types/menu'; + +const HOVER_LEAVE_MS = 280; + +type FlyoutState = { + flyoutRoot: MenuNode | null; + l1AnchorRect: DOMRect | null; + openFlyout: (node: MenuNode, anchorEl: HTMLDivElement) => void; + scheduleCloseFlyout: () => void; + clearCloseTimer: () => void; + closeFlyoutNow: () => void; + toggleFlyoutClick: (node: MenuNode, wrapper: HTMLDivElement | null) => void; + l1AnchorElRef: React.RefObject; + railScrollRef: React.RefObject; +}; + +/** 从 ClassicCollapsedSidebar 提取的 flyout 状态管理逻辑 */ +export function useFlyoutState(pathname: string): FlyoutState { + const [flyoutRoot, setFlyoutRoot] = useState(null); + const [l1AnchorRect, setL1AnchorRect] = useState(null); + const l1AnchorElRef = useRef(null); + const railScrollRef = useRef(null); + const closeTimerRef = useRef | null>(null); + + const clearCloseTimer = useCallback(() => { + if (closeTimerRef.current) { + clearTimeout(closeTimerRef.current); + closeTimerRef.current = null; + } + }, []); + + const closeFlyout = useCallback(() => { + setFlyoutRoot(null); + setL1AnchorRect(null); + l1AnchorElRef.current = null; + }, []); + + const scheduleCloseFlyout = useCallback(() => { + clearCloseTimer(); + closeTimerRef.current = setTimeout(() => { + closeFlyout(); + closeTimerRef.current = null; + }, HOVER_LEAVE_MS); + }, [clearCloseTimer, closeFlyout]); + + const closeFlyoutNow = useCallback(() => { + clearCloseTimer(); + closeFlyout(); + }, [clearCloseTimer, closeFlyout]); + + const syncL1Anchor = useCallback(() => { + const el = l1AnchorElRef.current; + if (el) setL1AnchorRect(el.getBoundingClientRect()); + }, []); + + useLayoutEffect(() => { + if (flyoutRoot) syncL1Anchor(); + }, [flyoutRoot, syncL1Anchor]); + + useEffect(() => { + if (!flyoutRoot) return; + const onResize = () => syncL1Anchor(); + window.addEventListener('resize', onResize); + const rs = railScrollRef.current; + rs?.addEventListener('scroll', onResize, { passive: true }); + return () => { + window.removeEventListener('resize', onResize); + rs?.removeEventListener('scroll', onResize); + }; + }, [flyoutRoot, syncL1Anchor]); + + useEffect(() => { + closeFlyout(); + }, [pathname, closeFlyout]); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') closeFlyout(); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [closeFlyout]); + + useEffect(() => clearCloseTimer, [clearCloseTimer]); + + const openFlyout = useCallback( + (node: MenuNode, anchorEl: HTMLDivElement) => { + clearCloseTimer(); + l1AnchorElRef.current = anchorEl; + setL1AnchorRect(anchorEl.getBoundingClientRect()); + setFlyoutRoot(node); + }, + [clearCloseTimer], + ); + + const toggleFlyoutClick = useCallback( + (node: MenuNode, wrapper: HTMLDivElement | null) => { + setFlyoutRoot((prev) => { + if (prev?.id === node.id) { + setL1AnchorRect(null); + l1AnchorElRef.current = null; + return null; + } + if (wrapper) { + l1AnchorElRef.current = wrapper; + setL1AnchorRect(wrapper.getBoundingClientRect()); + return node; + } + return prev; + }); + }, + [], + ); + + return { + flyoutRoot, + l1AnchorRect, + openFlyout, + scheduleCloseFlyout, + clearCloseTimer, + closeFlyoutNow, + toggleFlyoutClick, + l1AnchorElRef, + railScrollRef, + }; +} diff --git a/web/lib/hooks/use-menu-navigation.ts b/web/lib/hooks/use-menu-navigation.ts new file mode 100644 index 0000000..af8e77f --- /dev/null +++ b/web/lib/hooks/use-menu-navigation.ts @@ -0,0 +1,14 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useTabStore } from '@/stores/tab-store'; + +/** 站内导航 + 多标签联动,替代 onMenuNavigate prop drilling */ +export function useMenuNavigation() { + const router = useRouter(); + + return (path: string, title: string) => { + useTabStore.getState().openOrActivate({ path, title }); + router.push(path); + }; +} diff --git a/web/lib/hooks/use-nav-menu.ts b/web/lib/hooks/use-nav-menu.ts new file mode 100644 index 0000000..3abb872 --- /dev/null +++ b/web/lib/hooks/use-nav-menu.ts @@ -0,0 +1,70 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { iamMenu } from '@/lib/api/iam'; +import type { MenuNode } from '@/lib/api/types/menu'; +import { useAuthStore } from '@/stores/auth-store'; +import { useLayoutStore } from '@/stores/layout-store'; +import { useTenantStore } from '@/stores/tenant-store'; + +type NavMenuState = { + items: MenuNode[]; + loading: boolean; + error: string | null; + authed: boolean; + permissions: string[]; +}; + +/** 从 AppChrome 提取的导航菜单数据获取逻辑 */ +export function useNavMenu(): NavMenuState { + const accessToken = useAuthStore((s) => s.accessToken); + const authed = Boolean(accessToken); + const tenantId = useTenantStore((s) => s.tenantId); + const setPerms = useLayoutStore((s) => s.setPerms); + + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [permissions, setPermissions] = useState([]); + + useEffect(() => { + if (!accessToken) { + setItems([]); + setError(null); + setPerms([]); + setPermissions([]); + return; + } + let cancelled = false; + setLoading(true); + setError(null); + Promise.all([ + iamMenu.nav().catch(() => [] as MenuNode[]), + iamMenu.perms().catch(() => ({ perms: [] as string[] })), + ]) + .then(([tree, pr]) => { + if (cancelled) return; + const navItems = Array.isArray(tree) ? tree : []; + const perms = Array.isArray(pr?.perms) ? pr.perms : []; + setItems(navItems); + setPerms(perms); + setPermissions(perms); + }) + .catch((e: unknown) => { + if (!cancelled) { + setError(e instanceof Error ? e.message : String(e)); + setItems([]); + setPerms([]); + setPermissions([]); + } + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, [accessToken, tenantId, setPerms]); + + return { items, loading, error, authed, permissions }; +} diff --git a/web/lib/hooks/use-user-profile.ts b/web/lib/hooks/use-user-profile.ts new file mode 100644 index 0000000..ae0702c --- /dev/null +++ b/web/lib/hooks/use-user-profile.ts @@ -0,0 +1,97 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { introspectAccessToken } from '@/lib/api/auth'; +import { iamUser } from '@/lib/api/iam'; +import type { IamUser } from '@/lib/api/types/user'; +import { useAuthStore } from '@/stores/auth-store'; +import { useTenantStore } from '@/stores/tenant-store'; + +type UserProfileState = { + profile: IamUser | null; + userSub: string | null; + loading: boolean; + label: string; +}; + +function displayLabel(profile: IamUser | null, userSub: string | null, loading: boolean): string { + if (loading) return '加载中…'; + if (profile) { + const n = profile.real_name?.trim() || profile.user_name?.trim(); + if (n) return n; + } + if (userSub) return userSub.length > 12 ? `${userSub.slice(0, 10)}…` : userSub; + return '用户'; +} + +/** 从 UserMenu 提取的用户资料获取逻辑 */ +export function useUserProfile(): UserProfileState { + const accessToken = useAuthStore((s) => s.accessToken); + const [profile, setProfile] = useState(null); + const [userSub, setUserSub] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (!accessToken) { + setProfile(null); + setUserSub(null); + setLoading(false); + return; + } + let cancelled = false; + setLoading(true); + setProfile(null); + setUserSub(null); + + (async () => { + try { + const intro = await introspectAccessToken(accessToken); + if (cancelled) return; + if (!intro.active || !intro.sub) { + setUserSub(null); + setLoading(false); + return; + } + setUserSub(intro.sub); + try { + const u = await iamUser.get(intro.sub); + if (!cancelled) { + setProfile(u); + useTenantStore.getState().hydrateFromUserTenant(u.tenant_id); + } + } catch { + if (!cancelled) setProfile(null); + } + } catch { + if (!cancelled) { + setUserSub(null); + setProfile(null); + } + } finally { + if (!cancelled) setLoading(false); + } + })(); + + return () => { + cancelled = true; + }; + }, [accessToken]); + + return { + profile, + userSub, + loading, + label: displayLabel(profile, userSub, loading), + }; +} + +export function avatarInitials(profile: IamUser | null, userSub: string | null): string { + const name = profile?.real_name?.trim() || profile?.user_name?.trim(); + if (name) { + const arr = [...name]; + if (arr.length >= 2) return (arr[0] + arr[1]).toUpperCase(); + return name.slice(0, 2).toUpperCase(); + } + if (userSub) return userSub.replace(/-/g, '').slice(0, 2).toUpperCase() || '?'; + return '?'; +} diff --git a/web/lib/iam/list-helpers.ts b/web/lib/iam/list-helpers.ts new file mode 100644 index 0000000..9fa13fb --- /dev/null +++ b/web/lib/iam/list-helpers.ts @@ -0,0 +1,9 @@ +/** 解析 Go 侧常见列表响应 `{ items, total, ... }` */ +export function extractListItems(data: unknown): T[] { + if (!data || typeof data !== 'object') { + return []; + } + const o = data as { items?: unknown; Items?: unknown }; + const arr = o.items ?? o.Items; + return Array.isArray(arr) ? (arr as T[]) : []; +} diff --git a/web/lib/navigation/safe-return.ts b/web/lib/navigation/safe-return.ts new file mode 100644 index 0000000..d02294b --- /dev/null +++ b/web/lib/navigation/safe-return.ts @@ -0,0 +1,11 @@ +/** 仅允许站内相对路径,防止开放重定向 */ +export function safeReturnPath(from: string | null | undefined, fallback = '/dashboard'): string { + if (from == null || typeof from !== 'string') { + return fallback; + } + const t = from.trim(); + if (!t.startsWith('/') || t.startsWith('//')) { + return fallback; + } + return t; +} diff --git a/web/lib/notify/auth-events.ts b/web/lib/notify/auth-events.ts new file mode 100644 index 0000000..243b1d2 --- /dev/null +++ b/web/lib/notify/auth-events.ts @@ -0,0 +1,26 @@ +/** 供 `apiJson` 调用,避免直接依赖 React;由 AppProviders 注册实现。 */ +type AuthEventsImpl = { + on401: (msg: string) => void; + on403: (msg: string) => void; +}; + +let impl: Partial = {}; +let last401At = 0; + +export function registerAuthEvents(next: Partial) { + impl = next; +} + +/** 同一秒内合并多次 401,避免并发请求重复弹窗 */ +export function emit401Unauthorized(msg: string) { + const now = Date.now(); + if (now - last401At < 800) { + return; + } + last401At = now; + impl.on401?.(msg); +} + +export function emit403Forbidden(msg: string) { + impl.on403?.(msg); +} diff --git a/web/lib/oauth/browser.ts b/web/lib/oauth/browser.ts new file mode 100644 index 0000000..6ea2a39 --- /dev/null +++ b/web/lib/oauth/browser.ts @@ -0,0 +1,39 @@ +'use client'; + +import { getOAuthClientId, getOAuthRedirectUri, getPublicApiOrigin } from '@/lib/env'; +import { createPKCEPair } from '@/lib/oauth/pkce'; + +const VERIFIER_KEY = 'smart_oauth_pkce_verifier'; + +/** 跳转浏览器授权页(与 GET /oauth/authorize 一致);PKCE verifier 写入 sessionStorage,回调页取出换 token。 */ +export async function redirectToAuthorize(): Promise { + const { verifier, challenge } = await createPKCEPair(); + if (typeof window !== 'undefined') { + sessionStorage.setItem(VERIFIER_KEY, verifier); + } + const params = new URLSearchParams({ + response_type: 'code', + client_id: getOAuthClientId(), + redirect_uri: getOAuthRedirectUri(), + scope: 'openid', + code_challenge: challenge, + code_challenge_method: 'S256', + state: + typeof crypto !== 'undefined' && 'randomUUID' in crypto + ? crypto.randomUUID() + : String(Date.now()), + }); + const url = `${getPublicApiOrigin()}/oauth/authorize?${params.toString()}`; + window.location.href = url; +} + +export function takeStoredPkceVerifier(): string | null { + if (typeof window === 'undefined') { + return null; + } + const v = sessionStorage.getItem(VERIFIER_KEY); + if (v) { + sessionStorage.removeItem(VERIFIER_KEY); + } + return v; +} diff --git a/web/lib/oauth/pkce.ts b/web/lib/oauth/pkce.ts new file mode 100644 index 0000000..c68522e --- /dev/null +++ b/web/lib/oauth/pkce.ts @@ -0,0 +1,19 @@ +/** RFC 7636:生成 code_verifier 与 S256 code_challenge(与 Go VerifyPKCES256 一致)。 */ + +function base64Url(buf: ArrayBuffer): string { + const bytes = new Uint8Array(buf); + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]!); + } + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +export async function createPKCEPair(): Promise<{ verifier: string; challenge: string }> { + const arr = new Uint8Array(32); + crypto.getRandomValues(arr); + const verifier = base64Url(arr.buffer); + const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)); + const challenge = base64Url(hash); + return { verifier, challenge }; +} diff --git a/web/lib/sync/logout-broadcast.ts b/web/lib/sync/logout-broadcast.ts new file mode 100644 index 0000000..1423526 --- /dev/null +++ b/web/lib/sync/logout-broadcast.ts @@ -0,0 +1,58 @@ +const CH = 'smart-auth'; +const LS_KEY = 'smart_logout_ping'; + +/** 当前标签页主动登出时通知其它标签 */ +export function broadcastLogout(): void { + if (typeof window === 'undefined') { + return; + } + try { + if (typeof BroadcastChannel !== 'undefined') { + const bc = new BroadcastChannel(CH); + bc.postMessage({ type: 'logout', t: Date.now() }); + bc.close(); + } + } catch { + /* ignore */ + } + try { + localStorage.setItem(LS_KEY, String(Date.now())); + } catch { + /* ignore */ + } +} + +export type LogoutListener = () => void; + +/** 其它标签登出时回调(需在客户端挂载一次) */ +export function subscribeRemoteLogout(onLogout: LogoutListener): () => void { + if (typeof window === 'undefined') { + return () => {}; + } + + let bc: BroadcastChannel | null = null; + try { + if (typeof BroadcastChannel !== 'undefined') { + bc = new BroadcastChannel(CH); + bc.onmessage = (ev: MessageEvent<{ type?: string }>) => { + if (ev.data?.type === 'logout') { + onLogout(); + } + }; + } + } catch { + bc = null; + } + + const onStorage = (e: StorageEvent) => { + if (e.key === LS_KEY && e.newValue) { + onLogout(); + } + }; + window.addEventListener('storage', onStorage); + + return () => { + window.removeEventListener('storage', onStorage); + bc?.close(); + }; +} diff --git a/web/next.config.ts b/web/next.config.ts new file mode 100644 index 0000000..5e891cf --- /dev/null +++ b/web/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..715057d --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,8064 @@ +{ + "name": "web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.1.0", + "dependencies": { + "@radix-ui/react-accessible-icon": "^1.1.8", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.8", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-toolbar": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@radix-ui/react-visually-hidden": "^1.2.4", + "next": "15.5.15", + "react": "19.1.0", + "react-dom": "19.1.0", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.5.15", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.8.3", + "tailwindcss": "^4", + "typescript": "^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/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.15.tgz", + "integrity": "sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.15.tgz", + "integrity": "sha512-ExQoBfyKMjAUQ2nuF39ryQsG26H374ZfH13dlOZqf6TaE9ycRbIm+qUbUFCliU4BtQhiqtS7cnGA1yWfPMQ+jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.15.tgz", + "integrity": "sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.15.tgz", + "integrity": "sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.15.tgz", + "integrity": "sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.15.tgz", + "integrity": "sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.15.tgz", + "integrity": "sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.15.tgz", + "integrity": "sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.15.tgz", + "integrity": "sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.15.tgz", + "integrity": "sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "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/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "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-accessible-icon": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.8.tgz", + "integrity": "sha512-1k/SvTk5yW2x0eqepOxVjZyG8GBuYyj7z4/R5c9FYox7zb6vV08fNj6Wwv+TsHWZfPUd0tMlzM/6OguZO7F1eQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "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-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@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-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "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-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@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-alert-dialog/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-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-aspect-ratio": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.8.tgz", + "integrity": "sha512-5nZrJTF7gH+e0nZS7/QxFz6tJV4VimhQb1avEgtsJxvvIp5JilL+c58HICsKzPxghdwaDt48hEfPM1au4zGy+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-aspect-ratio/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-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "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-presence": "1.1.5", + "@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-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "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-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.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-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@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-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-collection/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-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.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-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "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-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-dialog/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-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-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "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-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "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-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "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-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "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-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "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-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "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-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-label/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-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@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-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-menu/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-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@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-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@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-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "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-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "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-navigation-menu/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "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-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-popover/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-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@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-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "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-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@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-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.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-primitive/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-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.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-progress/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-progress/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-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "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-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@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-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@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-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@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-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.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-select/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-select/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-separator/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-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@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-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@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-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-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "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-toolbar/node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "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-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "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-tooltip/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-tooltip/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-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-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "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-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-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "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-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/@radix-ui/react-visually-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.4.tgz", + "integrity": "sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.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-visually-hidden/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/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", + "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "postcss": "^8.5.6", + "tailwindcss": "4.2.2" + } + }, + "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/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "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/@typescript-eslint/eslint-plugin": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", + "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/type-utils": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", + "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", + "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.2", + "@typescript-eslint/types": "^8.58.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", + "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", + "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", + "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2", + "@typescript-eslint/utils": "8.58.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", + "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", + "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.2", + "@typescript-eslint/tsconfig-utils": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/visitor-keys": "8.58.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", + "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.2", + "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/typescript-estree": "8.58.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", + "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.3.tgz", + "integrity": "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "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/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.15.tgz", + "integrity": "sha512-mI5KIONOIosjF3jK2z9a8fY2LePNeW5C4lRJ+XZoJHAKkwx2MQjMPQ2/kL7tsMRPcQPZc/UBtCfqxElluL1CBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.5.15", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/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/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/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/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "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.4" + }, + "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/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/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/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "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-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.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-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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==", + "dev": true, + "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/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "15.5.15", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz", + "integrity": "sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.15", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.15", + "@next/swc-darwin-x64": "15.5.15", + "@next/swc-linux-arm64-gnu": "15.5.15", + "@next/swc-linux-arm64-musl": "15.5.15", + "@next/swc-linux-x64-gnu": "15.5.15", + "@next/swc-linux-x64-musl": "15.5.15", + "@next/swc-win32-arm64-msvc": "15.5.15", + "@next/swc-win32-x64-msvc": "15.5.15", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/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/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-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "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/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "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.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.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/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "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/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/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/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/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/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-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.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==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..749f29f --- /dev/null +++ b/web/package.json @@ -0,0 +1,60 @@ +{ + "name": "web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build --turbopack", + "start": "next start", + "lint": "eslint", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, + "dependencies": { + "@radix-ui/react-accessible-icon": "^1.1.8", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.8", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-toolbar": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@radix-ui/react-visually-hidden": "^1.2.4", + "next": "15.5.15", + "react": "19.1.0", + "react-dom": "19.1.0", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.5.15", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.8.3", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 0000000..9340fd5 --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,5412 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@radix-ui/react-accessible-icon': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-accordion': + specifier: ^1.2.12 + version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-aspect-ratio': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-collapsible': + specifier: ^1.1.12 + version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-context-menu': + specifier: ^2.2.16 + version: 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.16 + version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-hover-card': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-label': + specifier: ^2.1.8 + version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-menubar': + specifier: ^1.1.16 + version: 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-navigation-menu': + specifier: ^1.2.14 + version: 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-progress': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-radio-group': + specifier: ^1.3.8 + version: 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-scroll-area': + specifier: ^1.2.10 + version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-separator': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slider': + specifier: ^1.3.6 + version: 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': + specifier: ^1.2.4 + version: 1.2.4(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-toggle': + specifier: ^1.1.10 + version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-toggle-group': + specifier: ^1.1.11 + version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-toolbar': + specifier: ^1.1.11 + version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-visually-hidden': + specifier: ^1.2.4 + version: 1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: + specifier: 15.5.15 + version: 15.5.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) + zustand: + specifier: ^5.0.12 + version: 5.0.12(@types/react@19.2.14)(react@19.1.0) + devDependencies: + '@eslint/eslintrc': + specifier: ^3 + version: 3.3.5 + '@tailwindcss/postcss': + specifier: ^4 + version: 4.2.2 + '@types/node': + specifier: ^20 + version: 20.19.39 + '@types/react': + specifier: ^19 + version: 19.2.14 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.14) + eslint: + specifier: ^9 + version: 9.39.4(jiti@2.6.1) + eslint-config-next: + specifier: 15.5.15 + version: 15.5.15(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.39.4(jiti@2.6.1)) + prettier: + specifier: ^3.8.3 + version: 3.8.3 + tailwindcss: + specifier: ^4 + version: 4.2.2 + typescript: + specifier: ^5 + version: 5.9.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@next/env@15.5.15': + resolution: {integrity: sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==} + + '@next/eslint-plugin-next@15.5.15': + resolution: {integrity: sha512-ExQoBfyKMjAUQ2nuF39ryQsG26H374ZfH13dlOZqf6TaE9ycRbIm+qUbUFCliU4BtQhiqtS7cnGA1yWfPMQ+jA==} + + '@next/swc-darwin-arm64@15.5.15': + resolution: {integrity: sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.5.15': + resolution: {integrity: sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.5.15': + resolution: {integrity: sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@15.5.15': + resolution: {integrity: sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@15.5.15': + resolution: {integrity: sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@15.5.15': + resolution: {integrity: sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@15.5.15': + resolution: {integrity: sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.5.15': + resolution: {integrity: sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accessible-icon@1.1.8': + resolution: {integrity: sha512-1k/SvTk5yW2x0eqepOxVjZyG8GBuYyj7z4/R5c9FYox7zb6vV08fNj6Wwv+TsHWZfPUd0tMlzM/6OguZO7F1eQ==} + 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 + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + 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 + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + 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 + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + 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 + + '@radix-ui/react-aspect-ratio@1.1.8': + resolution: {integrity: sha512-5nZrJTF7gH+e0nZS7/QxFz6tJV4VimhQb1avEgtsJxvvIp5JilL+c58HICsKzPxghdwaDt48hEfPM1au4zGy+w==} + 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 + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + 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 + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + 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 + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + 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 + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + 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 + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.3': + resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + 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 + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + 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 + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + 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 + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + 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 + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + 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 + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.8': + resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} + 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 + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + 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 + + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + 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 + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + 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 + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + 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 + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + 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 + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + 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 + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + 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 + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + 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 + + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + 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 + + '@radix-ui/react-progress@1.1.8': + resolution: {integrity: sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==} + 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 + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + 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 + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + 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 + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + 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 + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + 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 + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + 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 + + '@radix-ui/react-separator@1.1.8': + resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} + 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 + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + 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 + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + 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 + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + 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 + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + 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 + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + 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 + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + 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 + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + 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 + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + 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 + + '@radix-ui/react-visually-hidden@1.2.4': + resolution: {integrity: sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==} + 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 + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/eslint-patch@1.16.1': + resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.2.2': + resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + + '@tailwindcss/oxide-android-arm64@4.2.2': + resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.2': + resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.2': + resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.2.2': + resolution: {integrity: sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@20.19.39': + resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@typescript-eslint/eslint-plugin@8.58.2': + resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.58.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.58.2': + resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.58.2': + resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.58.2': + resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.58.2': + resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.58.2': + resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.58.2': + resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.58.2': + resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.58.2': + resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.58.2': + resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.11.3: + resolution: {integrity: sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001788: + resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.3.2: + resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@15.5.15: + resolution: {integrity: sha512-mI5KIONOIosjF3jK2z9a8fY2LePNeW5C4lRJ+XZoJHAKkwx2MQjMPQ2/kL7tsMRPcQPZc/UBtCfqxElluL1CBg==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next@15.5.15: + resolution: {integrity: sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@2.0.0-next.6: + resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + engines: {node: '>=6'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zustand@5.0.12: + resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': + dependencies: + eslint: 9.39.4(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@floating-ui/utils@0.2.11': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.10.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@next/env@15.5.15': {} + + '@next/eslint-plugin-next@15.5.15': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@15.5.15': + optional: true + + '@next/swc-darwin-x64@15.5.15': + optional: true + + '@next/swc-linux-arm64-gnu@15.5.15': + optional: true + + '@next/swc-linux-arm64-musl@15.5.15': + optional: true + + '@next/swc-linux-x64-gnu@15.5.15': + optional: true + + '@next/swc-linux-x64-musl@15.5.15': + optional: true + + '@next/swc-win32-arm64-msvc@15.5.15': + optional: true + + '@next/swc-win32-x64-msvc@15.5.15': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accessible-icon@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-aspect-ratio@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context@1.1.3(@types/react@19.2.14)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-progress@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-context': 1.1.3(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.1.0)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-visually-hidden@1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/rect@1.1.1': {} + + '@rtsao/scc@1.1.0': {} + + '@rushstack/eslint-patch@1.16.1': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.2.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.2 + + '@tailwindcss/oxide-android-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide@4.2.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-x64': 4.2.2 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + + '@tailwindcss/postcss@4.2.2': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + postcss: 8.5.10 + tailwindcss: 4.2.2 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@20.19.39': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/type-utils': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.2 + eslint: 9.39.4(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.2 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.58.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3) + '@typescript-eslint/types': 8.58.2 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.58.2': + dependencies: + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/visitor-keys': 8.58.2 + + '@typescript-eslint/tsconfig-utils@8.58.2(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.58.2': {} + + '@typescript-eslint/typescript-estree@8.58.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.58.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@5.9.3) + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/visitor-keys': 8.58.2 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.58.2': + dependencies: + '@typescript-eslint/types': 8.58.2 + eslint-visitor-keys: 5.0.1 + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.11.3: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001788: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + client-only@0.0.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.2 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.3.2: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.3 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escape-string-regexp@4.0.0: {} + + eslint-config-next@15.5.15(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 15.5.15 + '@rushstack/eslint-patch': 1.16.1 + '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.39.4(jiti@2.6.1)) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + + eslint-import-resolver-node@0.3.10: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 2.0.0-next.6 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + get-tsconfig: 4.14.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.16 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.4(jiti@2.6.1) + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + hasown: 2.0.3 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.11.3 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.4(jiti@2.6.1) + hasown: 2.0.3 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + + eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.6.1)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.2 + eslint: 9.39.4(jiti@2.6.1) + estraverse: 5.3.0 + hasown: 2.0.3 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.6 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.3 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + get-intrinsic@1.3.0: + 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.3 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.3 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.4 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.3 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + next@15.5.15(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@next/env': 15.5.15 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001788 + postcss: 8.4.31 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + styled-jsx: 5.1.6(react@19.1.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.5.15 + '@next/swc-darwin-x64': 15.5.15 + '@next/swc-linux-arm64-gnu': 15.5.15 + '@next/swc-linux-arm64-musl': 15.5.15 + '@next/swc-linux-x64-gnu': 15.5.15 + '@next/swc-linux-x64-musl': 15.5.15 + '@next/swc-win32-arm64-msvc': 15.5.15 + '@next/swc-win32-x64-msvc': 15.5.15 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.10: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.8.3: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + + react-is@16.13.1: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.1.0): + dependencies: + react: 19.1.0 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.1.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.1.0): + dependencies: + react: 19.1.0 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.1.0) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.1.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.1.0) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.1.0) + optionalDependencies: + '@types/react': 19.2.14 + + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.1.0): + dependencies: + get-nonce: 1.0.1 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react@19.1.0: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@2.0.0-next.6: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.26.0: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + source-map-js@1.2.1: {} + + stable-hash@0.0.5: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.6(react@19.1.0): + dependencies: + client-only: 0.0.1 + react: 19.1.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@4.2.2: {} + + tapable@2.3.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.1.0): + dependencies: + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.1.0): + dependencies: + detect-node-es: 1.1.0 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} + + zustand@5.0.12(@types/react@19.2.14)(react@19.1.0): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.1.0 diff --git a/web/postcss.config.mjs b/web/postcss.config.mjs new file mode 100644 index 0000000..ba720fe --- /dev/null +++ b/web/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ['@tailwindcss/postcss'], +}; + +export default config; diff --git a/web/public/file.svg b/web/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/web/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/globe.svg b/web/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/web/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/next.svg b/web/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/web/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/vercel.svg b/web/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/web/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/window.svg b/web/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/web/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/stores/auth-store.ts b/web/stores/auth-store.ts new file mode 100644 index 0000000..89357c4 --- /dev/null +++ b/web/stores/auth-store.ts @@ -0,0 +1,78 @@ +'use client'; + +import { create } from 'zustand'; +import { loginWithPassword, logoutRequest, type TokenPair } from '@/lib/api/auth'; +import { registerTokenBridge } from '@/lib/api/client'; +import { broadcastLogout } from '@/lib/sync/logout-broadcast'; + +const STORAGE_KEY = 'smart_auth_tokens'; + +function loadFromSession(): Pick { + if (typeof window === 'undefined') { + return { accessToken: null, refreshToken: null }; + } + try { + const raw = sessionStorage.getItem(STORAGE_KEY); + if (!raw) { + return { accessToken: null, refreshToken: null }; + } + const p = JSON.parse(raw) as { accessToken?: string; refreshToken?: string }; + return { accessToken: p.accessToken ?? null, refreshToken: p.refreshToken ?? null }; + } catch { + return { accessToken: null, refreshToken: null }; + } +} + +type AuthState = { + accessToken: string | null; + refreshToken: string | null; + setTokens: (access: string | null, refresh: string | null) => void; + login: (userName: string, password: string, tenantId?: string) => Promise; + logout: () => Promise; +}; + +export const useAuthStore = create((set) => { + const initial = loadFromSession(); + + const persist = (accessToken: string | null, refreshToken: string | null) => { + set({ accessToken, refreshToken }); + if (typeof window !== 'undefined') { + if (accessToken || refreshToken) { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ accessToken, refreshToken })); + } else { + sessionStorage.removeItem(STORAGE_KEY); + } + } + }; + + return { + accessToken: initial.accessToken, + refreshToken: initial.refreshToken, + + setTokens: (access, refresh) => { + persist(access, refresh); + }, + + login: async (userName, password, tenantId) => { + const pair: TokenPair = await loginWithPassword({ userName, password, tenantId }); + persist(pair.accessToken, pair.refreshToken); + }, + + logout: async () => { + try { + await logoutRequest(); + } finally { + persist(null, null); + broadcastLogout(); + } + }, + }; +}); + +registerTokenBridge( + () => ({ + accessToken: useAuthStore.getState().accessToken, + refreshToken: useAuthStore.getState().refreshToken, + }), + (access, refresh) => useAuthStore.getState().setTokens(access, refresh) +); diff --git a/web/stores/auth-ui-store.ts b/web/stores/auth-ui-store.ts new file mode 100644 index 0000000..0d79da2 --- /dev/null +++ b/web/stores/auth-ui-store.ts @@ -0,0 +1,17 @@ +'use client'; + +import { create } from 'zustand'; + +type AuthUiState = { + loginModalOpen: boolean; + loginHint: string | null; + openLoginModal: (hint?: string | null) => void; + closeLoginModal: () => void; +}; + +export const useAuthUiStore = create((set) => ({ + loginModalOpen: false, + loginHint: null, + openLoginModal: (hint) => set({ loginModalOpen: true, loginHint: hint ?? null }), + closeLoginModal: () => set({ loginModalOpen: false, loginHint: null }), +})); diff --git a/web/stores/layout-store.ts b/web/stores/layout-store.ts new file mode 100644 index 0000000..fb58182 --- /dev/null +++ b/web/stores/layout-store.ts @@ -0,0 +1,84 @@ +'use client'; + +import { create } from 'zustand'; + +export type SidebarMode = 'classic' | 'icon'; + +const MODE_KEY = 'smart_sidebar_mode'; +const RAIL_COLLAPSED_KEY = 'smart_classic_nav_rail_collapsed'; + +function loadRailCollapsed(): boolean { + if (typeof window === 'undefined') { + return false; + } + try { + return localStorage.getItem(RAIL_COLLAPSED_KEY) === '1'; + } catch { + return false; + } +} + +function persistRailCollapsed(collapsed: boolean) { + if (typeof window === 'undefined') { + return; + } + try { + localStorage.setItem(RAIL_COLLAPSED_KEY, collapsed ? '1' : '0'); + } catch { + /* ignore */ + } +} + +function loadMode(): SidebarMode { + if (typeof window === 'undefined') { + return 'classic'; + } + try { + const v = localStorage.getItem(MODE_KEY); + return v === 'icon' ? 'icon' : 'classic'; + } catch { + return 'classic'; + } +} + +type LayoutStoreState = { + sidebarMode: SidebarMode; + /** 经典布局:窄轨仅一级 + 悬停浮层(true);false 为完整侧栏树 */ + classicNavRailCollapsed: boolean; + perms: string[]; + setSidebarMode: (mode: SidebarMode) => void; + setClassicNavRailCollapsed: (collapsed: boolean) => void; + toggleClassicNavRail: () => void; + setPerms: (perms: string[]) => void; +}; + +export const useLayoutStore = create((set) => ({ + sidebarMode: typeof window !== 'undefined' ? loadMode() : 'classic', + classicNavRailCollapsed: typeof window !== 'undefined' ? loadRailCollapsed() : false, + perms: [], + + setSidebarMode: (mode) => { + set({ sidebarMode: mode }); + if (typeof window !== 'undefined') { + try { + localStorage.setItem(MODE_KEY, mode); + } catch { + /* ignore */ + } + } + }, + + setClassicNavRailCollapsed: (collapsed) => { + persistRailCollapsed(collapsed); + set({ classicNavRailCollapsed: collapsed }); + }, + + toggleClassicNavRail: () => + set((s) => { + const next = !s.classicNavRailCollapsed; + persistRailCollapsed(next); + return { classicNavRailCollapsed: next }; + }), + + setPerms: (perms) => set({ perms }), +})); diff --git a/web/stores/tab-store.ts b/web/stores/tab-store.ts new file mode 100644 index 0000000..f8402aa --- /dev/null +++ b/web/stores/tab-store.ts @@ -0,0 +1,119 @@ +'use client'; + +import { create } from 'zustand'; + +export type AppTab = { + id: string; + title: string; + path: string; + pinned?: boolean; +}; + +type TabStoreState = { + tabs: AppTab[]; + activeId: string; + open: (tab: Omit & { id?: string }) => void; + /** 同 path 则仅激活,否则新开 */ + openOrActivate: (tab: { path: string; title: string }) => void; + /** 浏览器地址变化时同步当前页签 */ + syncFromPath: (path: string) => void; + /** 切换租户:保留固定「概览」并重置页签 */ + resetForTenantSwitch: () => void; + close: (id: string) => string | null; + activate: (id: string) => void; +}; + +let seq = 0; + +const overview: AppTab = { + id: 'overview', + title: '概览', + path: '/dashboard', + pinned: true, +}; + +function titleFromPath(path: string): string { + const parts = path.split('/').filter(Boolean); + const last = parts[parts.length - 1]; + return last ? decodeURIComponent(last) : path; +} + +export const useTabStore = create((set, get) => ({ + tabs: [overview], + activeId: 'overview', + + open: (tab) => { + const id = tab.id ?? `t-${++seq}`; + set((s) => ({ + tabs: [...s.tabs, { ...tab, id }], + activeId: id, + })); + }, + + openOrActivate: ({ path, title }) => { + const normalized = path.startsWith('/') ? path : `/${path}`; + set((s) => { + const hit = s.tabs.find((t) => t.path === normalized); + if (hit) { + return { activeId: hit.id }; + } + const id = `t-${++seq}`; + return { + tabs: [...s.tabs, { id, path: normalized, title: title || titleFromPath(normalized) }], + activeId: id, + }; + }); + }, + + syncFromPath: (path) => { + const normalized = path.startsWith('/') ? path : `/${path}`; + if (!normalized.startsWith('/dashboard')) { + return; + } + set((s) => { + const hit = s.tabs.find((t) => t.path === normalized); + if (hit) { + return { activeId: hit.id }; + } + const id = `t-${++seq}`; + return { + tabs: [ + ...s.tabs, + { + id, + path: normalized, + title: titleFromPath(normalized), + }, + ], + activeId: id, + }; + }); + }, + + resetForTenantSwitch: () => { + set({ + tabs: [overview], + activeId: 'overview', + }); + }, + + close: (id) => { + const { tabs, activeId } = get(); + const t = tabs.find((x) => x.id === id); + if (!t || t.pinned) { + return null; + } + const nextTabs = tabs.filter((x) => x.id !== id); + let nextActive = activeId; + if (activeId === id) { + const idx = tabs.findIndex((x) => x.id === id); + const neighbor = tabs[idx - 1] ?? tabs[idx + 1]; + nextActive = neighbor?.id ?? 'overview'; + } + set({ tabs: nextTabs, activeId: nextActive }); + const activeTab = get().tabs.find((x) => x.id === nextActive); + return activeTab?.path ?? '/dashboard'; + }, + + activate: (id) => set({ activeId: id }), +})); diff --git a/web/stores/tenant-store.ts b/web/stores/tenant-store.ts new file mode 100644 index 0000000..06afee4 --- /dev/null +++ b/web/stores/tenant-store.ts @@ -0,0 +1,59 @@ +'use client'; + +import { create } from 'zustand'; +import { registerTenantHeaderBridge } from '@/lib/api/client'; + +const KEY = 'smart_tenant_id'; + +type TenantState = { + /** 显式选择的租户;null 表示不额外传 X-Tenant-ID,由 Bearer 默认租户生效 */ + tenantId: string | null; + setTenantId: (id: string | null) => void; + /** 从当前用户资料同步(登录后首次) */ + hydrateFromUserTenant: (tenantId: string | undefined | null) => void; +}; + +function load(): string | null { + if (typeof window === 'undefined') { + return null; + } + try { + const raw = sessionStorage.getItem(KEY); + if (raw === '' || raw === 'null') { + return null; + } + return raw; + } catch { + return null; + } +} + +export const useTenantStore = create((set, get) => ({ + tenantId: typeof window !== 'undefined' ? load() : null, + + setTenantId: (id) => { + set({ tenantId: id }); + if (typeof window !== 'undefined') { + if (id) { + sessionStorage.setItem(KEY, id); + } else { + sessionStorage.removeItem(KEY); + } + } + }, + + hydrateFromUserTenant: (tid) => { + if (!tid) { + return; + } + const cur = get().tenantId; + if (cur == null || cur === '') { + set({ tenantId: tid }); + if (typeof window !== 'undefined') { + sessionStorage.setItem(KEY, tid); + } + } + }, +})); + +registerTenantHeaderBridge(() => useTenantStore.getState().tenantId); diff --git a/web/stores/toast-store.ts b/web/stores/toast-store.ts new file mode 100644 index 0000000..b5b45ce --- /dev/null +++ b/web/stores/toast-store.ts @@ -0,0 +1,31 @@ +'use client'; + +import { create } from 'zustand'; + +export type ToastItem = { id: string; message: string; variant: 'error' | 'info' }; + +type ToastState = { + toasts: ToastItem[]; + show: (message: string, variant?: ToastItem['variant']) => void; + dismiss: (id: string) => void; +}; + +export const useToastStore = create((set, get) => ({ + toasts: [], + + show: (message, variant = 'info') => { + const id = + typeof crypto !== 'undefined' && 'randomUUID' in crypto + ? crypto.randomUUID() + : String(Date.now()); + set((s) => ({ toasts: [...s.toasts, { id, message, variant }] })); + const duration = variant === 'error' ? 5200 : 3600; + if (typeof window !== 'undefined') { + window.setTimeout(() => { + get().dismiss(id); + }, duration); + } + }, + + dismiss: (id) => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })), +})); diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..d8b9323 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}