diff --git a/.gitignore b/.gitignore index 0e3c5b3..a888769 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,12 @@ # Build outputs **/dist/ +!heicode/web/default/dist/ +heicode/web/default/dist/* +!heicode/web/default/dist/index.html +!heicode/web/classic/dist/ +heicode/web/classic/dist/* +!heicode/web/classic/dist/index.html **/.next/ **/out/ **/.rsbuild/ diff --git a/docs/2026-05-02-daily-summary.txt b/docs/2026-05-02-daily-summary.txt index 0219b5c..08806df 100644 --- a/docs/2026-05-02-daily-summary.txt +++ b/docs/2026-05-02-daily-summary.txt @@ -143,5 +143,5 @@ Heicode 今日工作总结与后续计划 六、安全说明 - 本次文档和代码未写入任何真实密码、Token、Redis 密钥、PostgreSQL 密码、SSH 密码或 Git 密码。 -- 文档示例全部使用占位符或 vault:// secret_ref。 +- 文档示例使用占位符或 Azure Key Vault `azkv://...` secret_ref。 - 后续部署与推送仍需继续遵守:密钥只走环境变量、Secret Store、交互式凭据或平台安全注入,不进入 Git。 diff --git a/docs/README.md b/docs/README.md index ed4517a..7fe4f97 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,7 @@ |------|------| | [`heicode.md`](./heicode.md) | Heicode 当前产品定位、系统边界和架构共识 | | [`plan.md`](./plan.md) | 按当前共识拆出的实施计划 | -| [`heicode-runtime-auth-newapi-secret-design.md`](./heicode-runtime-auth-newapi-secret-design.md) | 用户输入、登录用户复用、NewAPI 扣费映射、OpenBao 短期凭证注入边界 | +| [`heicode-runtime-auth-newapi-secret-design.md`](./heicode-runtime-auth-newapi-secret-design.md) | 用户输入、登录用户复用、NewAPI 扣费映射、Azure Key Vault 凭证托管与短期凭证注入边界 | | [`integration/Heicode-登录接口对接文档.md`](./integration/Heicode-登录接口对接文档.md) | 已上线登录接口对接文档 | | [`integration/agnet-platform-request-contract.md`](./integration/agnet-platform-request-contract.md) | Manager 请求 Agnet 平台时携带的部署、日志、监控、事件与审计接口参数 | | [`deployment/azure-production-deploy-guardrails.md`](./deployment/azure-production-deploy-guardrails.md) | Azure VM / PostgreSQL / Redis / Agnet / NewAPI 生产部署前的安全守卫、环境变量注入和验证计划 | diff --git a/docs/deployment/azure-production-deploy-guardrails.md b/docs/deployment/azure-production-deploy-guardrails.md index 6add47e..64cb11e 100644 --- a/docs/deployment/azure-production-deploy-guardrails.md +++ b/docs/deployment/azure-production-deploy-guardrails.md @@ -2,7 +2,7 @@ 本文用于 Heicode Manager / Agnet / NewAPI 相关生产发布前的人工执行检查。它只描述安全命令、环境变量名和验证项,不保存任何真实地址、账号、密码、Token、连接串、SSH key 或云访问密钥。 -适用范围:Azure VM、Azure PostgreSQL、Azure Redis、Git 同步、Nginx 统一入口、Heicode Manager 容器、OpenBao 内网密钥保管、Agnet 平台联调、NewAPI 网关能力验证。 +适用范围:Azure VM、Azure PostgreSQL、Azure Redis、Git 同步、Nginx 统一入口、Heicode Manager 容器、Azure Key Vault 密钥保管、Agnet 平台联调、NewAPI 网关能力验证。 ## 1. 执行原则 @@ -12,7 +12,7 @@ | Git 发布 | 仅允许快进同步已审核提交;禁止在生产 VM 上提交代码或保存临时补丁。 | | 数据库/Redis | Azure PostgreSQL / Redis 连接串只写入 VM 本地 `.env` 或 Secret Store;验证时只打印变量名和连通性结果,不打印值。 | | Agnet / NewAPI | Manager 只传 `secret_ref`、部署计划、资源授权和审计上下文;禁止把明文云账号、数据库密码、模型 Key 放入请求体。 | -| OpenBao | 只允许 Manager/Agnet 受控网络访问;如果 Manager 提供客户端验证和绑定接口,OpenBao 不暴露公网路由。 | +| Azure Key Vault | 只允许 Manager/Agnet 通过 Managed Identity 和受控网络访问;如果 Manager 提供客户端验证和绑定接口,Key Vault 不暴露普通公网访问。 | | 生产动作 | 执行 `up -d`、迁移、重启、回滚前必须记录当前镜像/提交和健康检查 URL;失败时停止扩大变更。 | ## 2. 本地发布前检查 @@ -69,6 +69,7 @@ REDIS_CONN_STRING= SESSION_SECRET= HEICODE_ROOT_EMAILS= HEICODE_ADMIN_EMAILS= +AZURE_KEY_VAULT_URL= ``` 验证只打印 key,不打印 value: @@ -96,8 +97,8 @@ ssh "$REMOTE" "cd '$REMOTE_DIR/heicode' && docker compose -f docker-compose.azur - 对外域名统一使用 `heicode.xinghanlab.com`。 - Nginx 负责按路由转发 Manager 与 NewAPI,例如 Manager 主站、NewAPI 受控 API 或健康检查路由。 -- OpenBao 仅供 Manager/Agnet 服务端访问,不通过 `heicode.xinghanlab.com` 暴露给浏览器用户。 -- 若需要 OpenBao 运维 UI,也必须走临时 SSH tunnel、VPN、内网跳板或单独受保护管理入口,不走普通 SaaS 用户路由。 +- Azure Key Vault 仅供 Manager/Agnet 服务端通过 Managed Identity 访问,不通过 `heicode.xinghanlab.com` 暴露给浏览器用户。 +- Key Vault 应优先使用 Private Endpoint;如临时启用公网访问,必须限制来源并在验证后关闭。 构建和启动: @@ -156,7 +157,7 @@ curl -fsS -X POST "$AGNET_BASE_URL/api/agnet/deployments" \ |---|---|---| | 服务健康 | `curl -fsS http://127.0.0.1:3000/api/status` | 返回 `success=true`。 | | Nginx 公网入口 | `curl -fsS https://heicode.xinghanlab.com/api/status` | 返回 Manager 健康状态;DNS 需解析到生产入口。 | -| OpenBao 暴露面 | `curl -fsSI https://heicode.xinghanlab.com/v1/sys/health` | 普通公网入口不应返回 OpenBao 健康信息;预期为无路由、403 或 404。 | +| Key Vault 暴露面 | Azure Portal/CLI 检查 Key Vault networking | 生产优先 `Public network access = Disabled`,通过 Private Endpoint 访问。 | | 容器状态 | `docker compose ... ps heicode` | `heicode` 为 running/healthy。 | | Git 版本 | `git rev-parse --short HEAD` | 与已审核提交一致。 | | DB/Redis 注入 | `awk -F= ... .env` | 只打印 key,包含 `SQL_DSN`、`REDIS_CONN_STRING`。 | diff --git a/docs/heicode-runtime-auth-newapi-secret-design.md b/docs/heicode-runtime-auth-newapi-secret-design.md index b31ea22..501be25 100644 --- a/docs/heicode-runtime-auth-newapi-secret-design.md +++ b/docs/heicode-runtime-auth-newapi-secret-design.md @@ -2,7 +2,7 @@ 日期:2026-05-04 -本文修正 Manager、Agnet 平台、NewAPI 与 OpenBao 之间的运行时边界。若本文与旧文档中 `tenant`、`project` 或平台侧批准描述冲突,以本文为准。 +本文修正 Manager、Agnet 平台、NewAPI 与 Azure Key Vault 之间的运行时边界。若本文与旧文档中 `tenant`、`project` 或平台侧批准描述冲突,以本文为准。 ## 一、用户输入在哪里 @@ -60,7 +60,7 @@ JWT 中也包含: Manager 可以有本地 user cache,但 canonical user identity 应来自登录接口返回的用户信息。除非未来产品明确引入企业组织、空间或项目账本,否则不要把 tenant/project 作为认证和扣费主轴。 -## 三、高危操作审批与 OpenBao 密钥注入 +## 三、高危操作审批与 Azure Key Vault 密钥注入 高危操作审批只在客户端完成。用户在客户端明确批准后,Manager/Agnet 平台才可以执行对应动作。Agnet 平台不是审批主体,不发起额外审批;它只校验 `approval_id`、审批主体、审批范围、TTL、`risk_level` 和策略是否匹配。 @@ -69,13 +69,13 @@ Manager 可以有本地 user cache,但 canonical user identity 应来自登录 ```text 长期密钥 -> 用户授权或绑定资源 --> Manager 写入 OpenBao +-> Manager 通过 VM Managed Identity 写入 Azure Key Vault -> Manager DB 只保存 secret_ref --> OpenBao 只允许 Manager/Agnet 受控网络访问,不对公网暴露 +-> Azure Key Vault 通过 Private Endpoint / 防火墙限制,只允许 Manager/Agnet 受控网络访问 高危操作 -> 客户端审批 --> Manager/Agnet 平台按 secret_ref 从 OpenBao 获取或派生短期凭证 +-> Manager/Agnet 平台按 secret_ref 从 Azure Key Vault 获取或派生短期凭证 -> 短期凭证可注入子 Agnet -> 子 Agnet 完成任务后凭证过期或撤销 ``` @@ -86,15 +86,15 @@ Manager 可以有本地 user cache,但 canonical user identity 应来自登录 - `heicode.xinghanlab.com` 是 Manager、NewAPI 与内部服务的统一公网域名入口。 - Nginx 可以为 Manager 和 NewAPI 制定路由,例如 Manager 主站与 NewAPI 受控 API 路由。 -- OpenBao 不应作为普通公网路由开放;如果 Manager 已经提供客户端验证、资源绑定、审批和 `secret_ref` 管理接口,客户端不需要直连 OpenBao。 -- OpenBao 访问应限制在容器网络、VM loopback、AKS 内网、Workload Identity 或其它受保护服务间通道。 +- Azure Key Vault 不应作为普通公网路由开放;如果 Manager 已经提供客户端验证、资源绑定、审批和 `secret_ref` 管理接口,客户端不需要直连 Azure Key Vault。 +- Azure Key Vault 访问应限制在 Private Endpoint、Azure 防火墙规则、VM Managed Identity、AKS Workload Identity 或其它受保护服务间通道。 -OpenBao 不暴露公网的检查口径: +Azure Key Vault 不暴露普通公网入口的检查口径: -- 公网 DNS、Nginx `server_name`、Ingress、LoadBalancer 和安全组规则不得直接指向 OpenBao 服务端口。 +- Key Vault 应优先关闭 Public Network Access,并通过 Private Endpoint 接入 `heicode-vnet`。 - 外部客户端只能通过 Manager 的认证、资源绑定、审批和 `secret_ref` 管理接口间接操作密钥引用。 -- Manager、Agnet 平台和子 Agnet 访问 OpenBao 时必须走内网地址、loopback、容器网络、AKS private endpoint 或 Workload Identity 绑定。 -- 健康检查和联调报告只能证明内网访问可用;不得把公网可访问的 OpenBao health endpoint 作为验收口径。 +- Manager、Agnet 平台和子 Agnet 访问 Azure Key Vault 时必须走 Private Endpoint / Workload Identity / Managed Identity 绑定。 +- 健康检查和联调报告只能证明受控网络访问可用;不得把公网可访问作为验收口径。 短期凭证注入必须满足: @@ -183,7 +183,7 @@ Manager 请求 Agnet 平台部署或执行任务时,应携带登录用户上 ] }, "secret_context": { - "secret_refs": ["vault://secret/resources/res_git_1"], + "secret_refs": ["azkv://heicode-kv.vault.azure.net/secrets/res_git_1"], "inject_short_lived_credentials": true, "approval_id": "approval_123" } @@ -192,7 +192,7 @@ Manager 请求 Agnet 平台部署或执行任务时,应携带登录用户上 这里的 `user_context` 用于确认身份,`billing_context` 用于 NewAPI 余额、用量和扣费映射,`agent_runtime` 用于 Agnet 平台独立选择子 Agnet 模型和实例数量,`work_context` 用于团队开发控制,`secret_context` 只传 `secret_ref` 和审批结果。 -Agnet 平台执行时,应把用户、角色、Git 绑定、deployment、Agnet model profile 和 NewAPI 映射写入 metadata 或审计日志。真实 NewAPI key 和 OpenBao 凭证由平台安全通道读取,不进入 Markdown。 +Agnet 平台执行时,应把用户、角色、Git 绑定、deployment、Agnet model profile 和 NewAPI 映射写入 metadata 或审计日志。真实 NewAPI key 和 Azure Key Vault 凭证由平台安全通道读取,不进入 Markdown。 ## 六、当前主线结论 @@ -202,5 +202,5 @@ Agnet 平台执行时,应把用户、角色、Git 绑定、deployment、Agnet - 团队开发控制优先由绑定 Git、允许路径、Agnet 角色、资源授权表达。 - 子 Agnet 的运行模型由 Agnet 平台独立配置,不和 NewAPI 扣费对象混在一起。 - NewAPI 负责模型网关、用户/Token/Group 额度、余额、日志和扣费。 -- OpenBao 负责长期密钥托管,子 Agnet 只拿短期、最小权限、可审计凭证。 +- Azure Key Vault 负责长期密钥托管,子 Agnet 只拿短期、最小权限、可审计凭证。 - 高危操作审批发生在客户端,审批结果随任务上下文传给 Manager/Agnet 平台。 diff --git a/docs/heicode.md b/docs/heicode.md index fa379a5..086376f 100644 --- a/docs/heicode.md +++ b/docs/heicode.md @@ -39,7 +39,7 @@ Manager 是用户操作入口;Agnet 是运行时执行层;NewAPI 是模型 7. Manager 负责资源绑定、权限分配和凭证托管能力;真实密钥放入 Secret Store。 8. 子 Agnet 不保存长期密钥,只接收角色、资源元数据、AGENT.md 和受控访问方式。 9. Agnet 平台在 AKS 上负责运行时身份、隔离、状态、事件和审计回传。 -10. 高危操作审批只在客户端完成;审批通过后可以把 OpenBao 派生的短期、最小权限凭证注入子 Agnet,但不能注入长期密钥。 +10. 高危操作审批只在客户端完成;审批通过后可以把密钥保管器派生的短期、最小权限凭证注入子 Agnet,但不能注入长期密钥。 11. 子 Agnet 的运行模型是 Agnet 平台的部署配置,独立于 NewAPI 的用户、Token、Group 扣费映射。 ## 四、Manager 的核心功能 @@ -130,7 +130,7 @@ P1 permission manifest 示例: "ref": "main", "paths": ["services/api/**"] }, - "secret_ref": "vault://secret/resources/repo_demo" + "secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/repo-demo" } ] } @@ -154,7 +154,7 @@ Secret Broker 负责: - 接收 OAuth、GitHub App、云授权回调后的凭证。 - 生成按用户、资源和角色隔离的 secret path。 -- 写入 Vault、Infisical 或 Azure Key Vault。 +- 写入 Azure Key Vault。 - 创建或更新 policy。 - 保存 `secret_ref` 到 Manager DB。 - 轮换、撤销、禁用凭证。 @@ -185,7 +185,7 @@ Agnet 平台底层是 AKS,因此运行时权限应和 Kubernetes 身份绑定 | 受控注入 | Git clone、开发/测试环境、低风险资源 | | 短期凭证注入 | 已经客户端审批的生产部署、数据库写入、高危云操作 | -普通开发资源可受控注入;生产云资源和高危操作也只在客户端审批通过后,注入 OpenBao 派生的短期、最小权限凭证。 +普通开发资源可受控注入;生产云资源和高危操作也只在客户端审批通过后,注入密钥保管器派生的短期、最小权限凭证。 ## 八、NewAPI 边界 diff --git a/docs/integration/agnet-platform-request-contract.md b/docs/integration/agnet-platform-request-contract.md index e13a7dc..8d6b000 100644 --- a/docs/integration/agnet-platform-request-contract.md +++ b/docs/integration/agnet-platform-request-contract.md @@ -48,7 +48,7 @@ AGNET_PLATFORM_BASE_URL=https://agnet-platform.example.com ```text AGNET_PLATFORM_BASE_URL=https://staging-agnet.example.com -MANAGER_SERVICE_TOKEN_SECRET_REF=vault://secret/users/manager-service/bindings/agnet-platform/service-token +MANAGER_SERVICE_TOKEN_SECRET_REF=azkv://heicode-kv.vault.azure.net/secrets/manager-service-agnet-platform-service-token ``` 完整路径示例: @@ -230,7 +230,7 @@ POST /api/agnet/deployments "repo_url": "https://example.com/org/repo.git" }, "status": "active", - "secret_ref": "vault://secret/users/user_123/bindings/repo_default/resources/res_git_repo_001", + "secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/user-123-repo-default-res-git-repo-001", "audit": { "created_by": "manager", "approval_id": "approval_001" @@ -419,7 +419,7 @@ POST /api/agnet/deployments "service_name": "new-api" }, "status": "active", - "secret_ref": "vault://secret/users/user_123/bindings/newapi-prod/resources/res_newapi_vm", + "secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/user-123-newapi-prod-res-newapi-vm", "audit": { "created_by": "manager", "approval_id": "approval_newapi_001" @@ -441,11 +441,11 @@ POST /api/agnet/deployments "plaintext_export_forbidden": "true" }, "metadata": { - "secret_provider": "openbao", + "secret_provider": "azure_key_vault", "scope": "newapi-runtime" }, "status": "active", - "secret_ref": "vault://secret/users/user_123/bindings/newapi-prod/resources/res_newapi_runtime_env", + "secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/user-123-newapi-prod-res-newapi-runtime-env", "audit": { "created_by": "manager", "approval_id": "approval_newapi_001" @@ -912,7 +912,7 @@ Agnet 平台不承担高危操作审批主体。审批只发生在客户端;Ag | 审批范围 | 审批范围必须覆盖 `binding_scope`、`permission_scope`、目标环境、资源 ID 和操作类型。 | | TTL / 时间窗口 | 审批记录必须未过期;若使用 `window` 或 TTL,当前执行时间必须落在允许范围内。 | | `risk_level` | 高危资源写入、生产部署、云资源修改、删除和扩容必须为 `high`。 | -| 策略 | 平台 policy、OpenBao policy、Kubernetes/Workload Identity、网络策略和最小权限约束均必须允许本次动作。 | +| 策略 | 平台 policy、Key Vault 访问策略、Kubernetes/Workload Identity、网络策略和最小权限约束均必须允许本次动作。 | 任一校验不通过时,Agnet 平台应返回 `POLICY_REJECTED` 或 `FORBIDDEN_SCOPE`,不得发起额外批准流程。 diff --git a/docs/plan.md b/docs/plan.md index d682b6e..4dbdb58 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -57,7 +57,7 @@ 任务: - 优先选型 HashiCorp Vault。 -- 保留 Infisical 和 Azure Key Vault 作为 Secret Provider 备选。 +- 当前生产方向使用 Azure Key Vault 作为 Secret Provider。 - 在 Manager 后端实现 Secret Broker。 - Secret Broker 负责写入、轮换、撤销、禁用和审计。 - Manager DB 只保存 `secret_ref`,不保存明文密钥。 @@ -76,10 +76,10 @@ - Agnet 平台支持 deployment / role 到 Kubernetes ServiceAccount 的映射。 - 支持 Vault Kubernetes Auth 或等价 Workload Identity。 -- 支持按 user / resource binding / role 生成 OpenBao policy。 +- 支持按 user / resource binding / role 生成密钥访问策略。 - 子 Agnet 运行时只能访问被授权的 secret。 - 普通开发资源支持受控注入。 -- 高危操作审批只在客户端完成;审批通过后允许向子 Agnet 注入 OpenBao 派生的短期、最小权限凭证。 +- 高危操作审批只在客户端完成;审批通过后允许向子 Agnet 注入密钥保管器派生的短期、最小权限凭证。 验收: diff --git a/docs/product-package/01-product-overview.md b/docs/product-package/01-product-overview.md index 1ffe5f5..d15ff3c 100644 --- a/docs/product-package/01-product-overview.md +++ b/docs/product-package/01-product-overview.md @@ -38,7 +38,7 @@ Heicode 是一款覆盖软件生命周期的智能开发 Code 工具。它面向 | Heicode Manager / 浏览器控制台 | 辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态总览、模型余额与用量、审计与日志查看 | | Agnet 平台 | 执行层,负责部署和运行子 Agnet,在任务推进过程中完成需求、开发、测试、修复、交付与部署,并回传日志、状态、事件和指标 | | CodeGW | 内部模型网关与计费服务,普通用户不直接进入后台 | -| 密钥保管器 | OpenBao 实现,保存长期凭证,按审批和权限提供短期凭证租约 | +| 密钥保管器 | Azure Key Vault 实现,保存长期凭证引用,按审批和权限提供短期访问能力 | 说明:Heicode 客户端是用户主体验,用户不在网页上编码。Heicode Manager 是浏览器里的辅助控制台,承担资源、部署、状态、余额、审计和下载等辅助操作。真正持续推进任务的是 Heicode 调度下的 Agnet 执行闭环,Agnet 在过程中还可以调用已授权的 SK 工具和外部能力,最终完成交付与部署并把结果回传到客户端。 diff --git a/docs/product-package/02-homepage-design-brief.md b/docs/product-package/02-homepage-design-brief.md index 3fd4526..681063c 100644 --- a/docs/product-package/02-homepage-design-brief.md +++ b/docs/product-package/02-homepage-design-brief.md @@ -136,7 +136,7 @@ | 客户端 | 主对话体验、任务推进和高危审批 | | Agnet 平台 | AKS 执行层 | | CodeGW | 模型网关与用量 | -| OpenBao | 密钥保管器 | +| Azure Key Vault | 密钥保管器 | 补充说明: @@ -170,4 +170,4 @@ - 信息密度适中,避免空泛大词。 - 主色与 Heicode 和客户端保持一致。 - 图片优先使用真实界面和流程,不使用无意义装饰图。 -- 首页文案避免出现 CodeGW 后台、OpenBao 控制台、模型提供方配置等内部概念。 +- 首页文案避免出现 CodeGW 后台、Azure Key Vault 控制台、模型提供方配置等内部概念。 diff --git a/docs/product-package/04-platform-usage-guide.md b/docs/product-package/04-platform-usage-guide.md index a912e9b..a7c9e74 100644 --- a/docs/product-package/04-platform-usage-guide.md +++ b/docs/product-package/04-platform-usage-guide.md @@ -12,7 +12,7 @@ https://code.xinghanlab.com ``` -用户只需要登录 Heicode。普通用户不需要登录 CodeGW 后台,也不需要登录 OpenBao。 +用户只需要登录 Heicode。普通用户不需要登录 CodeGW 后台,也不需要登录 Azure Key Vault。 ## 第一步:登录 Heicode diff --git a/docs/product-package/05-ppt-copy.md b/docs/product-package/05-ppt-copy.md index 20b3c12..9513b43 100644 --- a/docs/product-package/05-ppt-copy.md +++ b/docs/product-package/05-ppt-copy.md @@ -65,7 +65,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法, 讲述: -用户不需要直接操作 CodeGW 后台,也不需要直接管理 OpenBao。所有操作都在 Heicode 和客户端内完成。 +用户不需要直接操作 CodeGW 后台,也不需要直接管理 Azure Key Vault。所有操作都在 Heicode 和客户端内完成。 ## Slide 5:核心能力 @@ -91,13 +91,13 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法, -> Heicode Manager -> Agnet 平台 -> CodeGW --> OpenBao +-> Azure Key Vault -> Git / SK / 云资源 ``` 讲述: -客户端是主体验,Manager 是辅助控制台。Agnet 平台是执行层,执行过程中可调用 SK。CodeGW 是模型网关和计费服务。OpenBao 是密钥保管器。 +客户端是主体验,Manager 是辅助控制台。Agnet 平台是执行层,执行过程中可调用 SK。CodeGW 是模型网关和计费服务。Azure Key Vault 是密钥保管器。 ## Slide 7:安全模型 @@ -108,7 +108,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法, 要点: - Heicode 服务端数据库只保存 `secret_ref`。 -- 长期密钥进入 OpenBao。 +- 长期密钥进入 Azure Key Vault。 - 子 Agnet 只获得短期、最小权限凭证。 - 高危操作必须在客户端审批。 - 日志、Markdown、前端响应不包含明文密钥。 @@ -141,7 +141,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法, > 当前 MVP 先闭环三件事 -1. Heicode、CodeGW、OpenBao 解耦部署。 +1. Heicode、CodeGW、Azure Key Vault 解耦部署。 2. 资源绑定、Resource Grant、manifest 和密钥保管。 3. 客户端只登录 Heicode,只使用 Heicode 模型。 diff --git a/docs/product-package/06-security-and-credential-management.md b/docs/product-package/06-security-and-credential-management.md index a39b3d7..13f0b54 100644 --- a/docs/product-package/06-security-and-credential-management.md +++ b/docs/product-package/06-security-and-credential-management.md @@ -26,17 +26,17 @@ Heicode 是 SaaS 产品,不能把凭证管理转嫁给用户,也不能让密 用户可见名称:密钥保管器。 -技术实现:OpenBao,兼容 Vault API。 +技术实现:Azure Key Vault。生产环境中 Manager 通过 VM Managed Identity 访问 Key Vault,不在业务数据库保存明文密钥。 -用户不需要直接登录 OpenBao,也不需要看到 OpenBao 控制台。 +用户不需要直接登录 Azure Key Vault,也不需要看到 Key Vault 控制台。 ## Secret Broker 流程 ```text 用户绑定资源 -> Heicode 接收授权结果 --> Heicode Secret Broker 写入 OpenBao --> OpenBao 返回或形成 secret_ref +-> Heicode Secret Broker 写入 Azure Key Vault +-> Azure Key Vault 返回或形成 azkv://... 形式的 secret_ref -> Heicode DB 保存 secret_ref -> 前端只展示脱敏引用和状态 ``` @@ -87,7 +87,7 @@ Heicode 生成 Resource Grant 3. 禁止前端返回明文密钥。 4. 禁止在日志中打印密钥。 5. 禁止把长期云密钥注入子 Agnet。 -6. 禁止把 OpenBao 暴露为普通公网入口。 +6. 禁止把 Azure Key Vault 管理入口作为普通用户入口暴露。 7. 禁止把 CodeGW key 原文交给客户端长期保存。 ## 用户侧解释 @@ -105,4 +105,4 @@ Heicode 生成 Resource Grant | 日志 | 不包含 token、password、private key、access key | | 子 Agnet | 不保存长期凭证 | | 高危操作 | 有客户端审批记录 | -| OpenBao | 不对普通公网暴露 | +| Azure Key Vault | 普通用户不可直接进入;Manager 仅通过受控后端身份访问 | diff --git a/docs/product-package/07-integration-boundaries.md b/docs/product-package/07-integration-boundaries.md index 71aea20..af7b70f 100644 --- a/docs/product-package/07-integration-boundaries.md +++ b/docs/product-package/07-integration-boundaries.md @@ -1,8 +1,8 @@ -# 07. CodeGW、OpenBao、Agnet 平台边界说明 +# 07. CodeGW、Azure Key Vault、Agnet 平台边界说明 ## 目标 -本文用于讲清楚 Heicode 与 CodeGW、OpenBao、Agnet 平台之间的产品和技术边界,避免再次变成缝合式系统。 +本文用于讲清楚 Heicode 与 CodeGW、Azure Key Vault、Agnet 平台之间的产品和技术边界,避免再次变成缝合式系统。 ## 总体边界 @@ -10,7 +10,7 @@ |------|----------|----------------------| | Heicode | 产品总入口,包含客户端主体验与 Manager 辅助控制台 | 是 | | CodeGW | 模型网关、模型调用、余额、额度、用量日志 | 否 | -| OpenBao | 长期凭证保管、短期凭证租约 | 否 | +| Azure Key Vault | 长期凭证托管、密钥引用、后端受控读取 | 否 | | Agnet 平台 | AKS 执行层、状态、日志、事件、指标 | 否 | ## Heicode 与 CodeGW @@ -34,20 +34,20 @@ Heicode 不做: > CodeGW 是 Heicode 的模型网关和计费底座,用户在 Heicode 中查看模型和用量,不直接使用 CodeGW 后台。 -## Heicode 与 OpenBao +## Heicode 与 Azure Key Vault -Heicode 使用 OpenBao 的方式: +Heicode 使用 Azure Key Vault 的方式: - 写入用户授权后的长期凭证。 - 保存和管理 `secret_ref`。 -- 执行密钥 health、metadata、轮换、禁用、撤销。 +- 执行密钥健康检查、metadata、轮换、禁用、撤销。 - 为高危操作申请短期凭证。 Heicode 不做: - 在数据库保存明文密钥。 -- 把 OpenBao token 交给前端。 -- 把 OpenBao 作为普通公网服务暴露。 +- 把 Azure Key Vault access token 交给前端。 +- 把 Azure Key Vault 管理入口作为普通用户入口暴露。 产品口径: @@ -86,7 +86,7 @@ Heicode 不传: - 长期明文密钥。 - CodeGW key 原文。 -- OpenBao root token。 +- Azure Key Vault 管理凭据。 - 数据库密码。 产品口径: @@ -117,7 +117,7 @@ Manager 主做: - 选择模型提供方。 - 配置 CodeGW 渠道。 -- 直接访问 OpenBao。 +- 直接访问 Azure Key Vault。 - 直接保存长期云密钥。 Manager 不做: @@ -144,5 +144,5 @@ Heicode 到 Agnet 平台真实部署 API 的完整联调仍可延期。 1. 普通用户只进入 Heicode。 2. 客户端只登录 Heicode。 3. CodeGW 独立服务运行。 -4. OpenBao 只通过受控内网或后端访问。 +4. Azure Key Vault 只通过 Manager 后端受控身份访问,普通用户不直接进入。 5. Agnet 平台接口只收结构化上下文和 `secret_ref`,不收长期明文密钥。 diff --git a/docs/product-package/08-client-guide.md b/docs/product-package/08-client-guide.md index 6ed894a..60e767a 100644 --- a/docs/product-package/08-client-guide.md +++ b/docs/product-package/08-client-guide.md @@ -104,7 +104,7 @@ macOS 注意: 2. 旧登录入口。 3. 第三方路由登录入口。 4. CodeGW 后台入口。 -5. OpenBao 地址或 token。 +5. Azure Key Vault 地址、access token 或密钥保管器后台信息。 6. 长期云密钥输入框。 7. 无法解释的个人 API key 自动复制。 diff --git a/docs/product-package/09-demo-script.md b/docs/product-package/09-demo-script.md index 52d8b61..25e58a1 100644 --- a/docs/product-package/09-demo-script.md +++ b/docs/product-package/09-demo-script.md @@ -166,7 +166,7 @@ 1. 不展示真实密钥。 2. 不进入 CodeGW 管理后台。 -3. 不展示 OpenBao token。 +3. 不展示 Azure Key Vault access token。 4. 不强调底层配置复杂度,强调用户流程。 5. 如果 Agnet 真实部署尚未联调,明确说当前展示的是 Heicode 侧 payload、manifest 和占位控制面。 @@ -174,4 +174,4 @@ 话术: -> Heicode 是从想法到上线的智能开发工具。用户主要在客户端输入想法和继续推进任务,在 Manager 绑定 Git、文档、SK 和云资源并部署 Agnet。Heicode 生成子 Agnet 团队、角色和权限,密钥进入密钥保管器,子 Agnet 只拿短期凭证。Agnet 在执行中可以调用 SK 工具,最后完成交付与部署;CodeGW 提供模型、余额和用量,Heicode 展示日志、状态和审计。用户不需要进入 CodeGW 后台,也不需要管理 OpenBao,只在 Heicode 和客户端完成整个开发流程。 +> Heicode 是从想法到上线的智能开发工具。用户主要在客户端输入想法和继续推进任务,在 Manager 绑定 Git、文档、SK 和云资源并部署 Agnet。Heicode 生成子 Agnet 团队、角色和权限,密钥进入密钥保管器,子 Agnet 只拿短期凭证。Agnet 在执行中可以调用 SK 工具,最后完成交付与部署;CodeGW 提供模型、余额和用量,Heicode 展示日志、状态和审计。用户不需要进入 CodeGW 后台,也不需要管理 Azure Key Vault,只在 Heicode 和客户端完成整个开发流程。 diff --git a/docs/product-package/10-frontend-detail-spec.md b/docs/product-package/10-frontend-detail-spec.md index 9e42eac..4e62f9a 100644 --- a/docs/product-package/10-frontend-detail-spec.md +++ b/docs/product-package/10-frontend-detail-spec.md @@ -38,7 +38,7 @@ Heicode 不以后台菜单作为主体验。建议使用顶部轻量入口和主 - CodeGW 后台。 - 渠道管理。 - 模型供应商配置。 -- OpenBao 控制台。 +- Azure Key Vault 控制台。 - 模型提供方选择。 - 旧服务入口。 @@ -322,7 +322,7 @@ Heicode 只追问影响执行路径的少量问题: ## 文案规则 - 用“任务上下文”或“资源上下文”,不要把主流程叫成资源管理。 -- 用“密钥保管器”,不用“OpenBao”作为普通用户标题。 +- 用“密钥保管器”,不要用具体实现名作为普通用户标题。 - 用“客户端”,不用“本地模型提供方”。 - 用“模型与余额”,不用“CodeGW 后台”。 - 用“执行计划”,不用“控制面草案”。 diff --git a/docs/product-package/12-narrated-user-operation-flow.md b/docs/product-package/12-narrated-user-operation-flow.md index d6d166b..2f05267 100644 --- a/docs/product-package/12-narrated-user-operation-flow.md +++ b/docs/product-package/12-narrated-user-operation-flow.md @@ -334,7 +334,7 @@ Heicode 会复用之前的项目上下文、资源绑定和 Agnet 角色,生 ## 口述时不要说 1. 让用户去 CodeGW 后台配置模型。 -2. 让用户去 OpenBao 控制台管理密钥。 +2. 让用户去 Azure Key Vault 控制台管理密钥。 3. 让客户端选择模型提供方。 4. 让用户手写复杂 manifest。 5. 把 tenant/project 当作当前产品主轴。 diff --git a/docs/product-package/13-platform-description.md b/docs/product-package/13-platform-description.md index 843faab..3db6faf 100644 --- a/docs/product-package/13-platform-description.md +++ b/docs/product-package/13-platform-description.md @@ -256,7 +256,9 @@ Heicode 通过服务端调用 CodeGW,并把普通用户需要的信息展示 ### 5.5 密钥保管器 -密钥保管器用于保存长期凭证。当前技术实现是 OpenBao。 +密钥保管器用于保存长期凭证。当前技术实现是 Azure Key Vault。 + +生产环境中 Manager 使用 VM Managed Identity 访问 Azure Key Vault。用户绑定资源后,Manager 将敏感凭证写入 Key Vault,并在 Heicode 数据库中只保存 `azkv:///secrets/` 形式的 `secret_ref`。 它负责: @@ -334,7 +336,7 @@ Heicode 判断当前任务需要哪些上下文 用户不需要直接使用: - CodeGW 后台。 -- OpenBao 控制台。 +- Azure Key Vault 控制台。 - Agnet 平台后台。 用户不应该看到: @@ -342,7 +344,7 @@ Heicode 判断当前任务需要哪些上下文 - 模型提供方选择。 - 旧服务入口。 - 明文密钥。 -- OpenBao token。 +- Azure Key Vault access token。 - CodeGW 管理员配置。 ## 十一、常见问题 diff --git a/docs/product-package/README.md b/docs/product-package/README.md index 2e3536c..5176245 100644 --- a/docs/product-package/README.md +++ b/docs/product-package/README.md @@ -14,7 +14,7 @@ | [`04-platform-usage-guide.md`](./04-platform-usage-guide.md) | 登录平台和使用平台指南,面向真实用户上手 | | [`05-ppt-copy.md`](./05-ppt-copy.md) | 产品 PPT 文案,面向路演、客户介绍和团队同步 | | [`06-security-and-credential-management.md`](./06-security-and-credential-management.md) | 安全与凭证管理说明,解释密钥保管器、`secret_ref`、高危审批 | -| [`07-integration-boundaries.md`](./07-integration-boundaries.md) | CodeGW、OpenBao、Agnet 平台边界说明 | +| [`07-integration-boundaries.md`](./07-integration-boundaries.md) | CodeGW、Azure Key Vault、Agnet 平台边界说明 | | [`08-client-guide.md`](./08-client-guide.md) | Heicode 客户端说明,强调只登录 Heicode、只使用 Heicode 模型 | | [`09-demo-script.md`](./09-demo-script.md) | 5 分钟演示脚本,用于产品演示和录屏 | | [`10-frontend-detail-spec.md`](./10-frontend-detail-spec.md) | 前端细节说明,覆盖 Heicode 和客户端页面、组件、状态、文案规则 | @@ -37,7 +37,7 @@ Heicode 是一款面向全流程智能开发的 SaaS Code 工具。用户注册 - `Heicode Manager` 是浏览器里的辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态、余额与审计。 - 继续开发时,Heicode 会持续调用 Agnet 推进需求、开发、测试、修复和部署等子环节。 - Agnet 在执行过程中可以调用已授权的 SK 工具,最终完成交付与部署并把结果回传给客户端和 Manager。 -- 用户侧叫 `密钥保管器`,技术实现可以说明是 OpenBao。 +- 用户侧叫 `密钥保管器`,技术实现可以说明是 Azure Key Vault;Manager 通过 VM Managed Identity 访问,不要求普通用户进入 Key Vault 后台。 - 用户侧叫 `资源绑定`,不要叫 Git 来源。 - 客户端不出现 `模型提供方`、`旧服务入口`、`第三方路由入口`。 - CodeGW 是内部模型网关和计费服务,不作为普通用户产品入口。 diff --git a/heicode/.gitignore b/heicode/.gitignore index d49f238..84f648f 100644 --- a/heicode/.gitignore +++ b/heicode/.gitignore @@ -9,7 +9,13 @@ build *.db-journal logs web/default/dist +!web/default/dist/ +web/default/dist/* +!web/default/dist/index.html web/classic/dist +!web/classic/dist/ +web/classic/dist/* +!web/classic/dist/index.html web/node_modules web/dist .env diff --git a/heicode/VERSION b/heicode/VERSION index 9df886c..428b770 100644 --- a/heicode/VERSION +++ b/heicode/VERSION @@ -1 +1 @@ -1.4.2 +1.4.3 diff --git a/heicode/controller/agnet_control_plane_test.go b/heicode/controller/agnet_control_plane_test.go index 5047e73..8e2f3ff 100644 --- a/heicode/controller/agnet_control_plane_test.go +++ b/heicode/controller/agnet_control_plane_test.go @@ -91,7 +91,7 @@ func baseAgnetResourceGrantPlan() agnetOrchestrationPlan { "repo_url": "https://example.invalid/acme/project.git", }, Status: agnetGrantStatusActive, - SecretRef: "vault://secret/users/user-p1/bindings/project-main/resources/res-git-main", + SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main", Audit: map[string]string{ "created_by": "manager-test", }, @@ -162,7 +162,7 @@ func TestAgnetCreateDeploymentAcceptsP1ResourceGrantModel(t *testing.T) { require.Equal(t, "https://example.invalid/acme/project.git#main", grants[0].BindingScope) require.Equal(t, "builder", grants[0].TargetRole) require.Equal(t, "agent-builder-1", grants[0].TargetAgentRef) - require.Equal(t, "vault://secret/users/user-p1/bindings/project-main/resources/res-git-main", grants[0].SecretRef) + require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main", grants[0].SecretRef) require.Empty(t, grants[1].SecretRef, "project document grants should not require credential material") require.Equal(t, "newapi", stored.Plan.BillingContext.Provider) require.Equal(t, "channel-p1", stored.Plan.UserContext.ChannelID) @@ -176,7 +176,7 @@ func TestAgnetCreateDeploymentAcceptsP1ResourceGrantModel(t *testing.T) { require.Equal(t, "user-p1", stored.ResourceGrantManifest.UserID) require.Equal(t, "builder", stored.ResourceGrantManifest.AgentRole) require.Equal(t, "agent-builder-1", stored.ResourceGrantManifest.TargetAgentRef) - require.Equal(t, "vault://secret/users/user-p1/bindings/project-main/resources/res-git-main", stored.ResourceGrantManifest.ResourceGrants[0].SecretRef) + require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main", stored.ResourceGrantManifest.ResourceGrants[0].SecretRef) } func TestAgnetPermissionManifestExcludesInactiveResourceGrants(t *testing.T) { diff --git a/heicode/controller/azure_resource_discovery.go b/heicode/controller/azure_resource_discovery.go new file mode 100644 index 0000000..a758724 --- /dev/null +++ b/heicode/controller/azure_resource_discovery.go @@ -0,0 +1,311 @@ +package controller + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/heicode/manager/common" + "github.com/heicode/manager/model" + "gorm.io/gorm" +) + +type azureResourceDiscoveryClient struct { + authorityHost string + armBaseURL string + client *http.Client +} + +type azureARMResource struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Location string `json:"location"` + Tags map[string]any `json:"tags"` +} + +type azureARMResourceListResponse struct { + Value []azureARMResource `json:"value"` + NextLink string `json:"nextLink"` +} + +func newAzureResourceDiscoveryClientFromEnv() azureResourceDiscoveryClient { + authorityHost := strings.TrimRight(strings.TrimSpace(os.Getenv("AZURE_AUTHORITY_HOST")), "/") + if authorityHost == "" { + authorityHost = "https://login.microsoftonline.com" + } + armBaseURL := strings.TrimRight(strings.TrimSpace(os.Getenv("AZURE_ARM_BASE_URL")), "/") + if armBaseURL == "" { + armBaseURL = "https://management.azure.com" + } + return azureResourceDiscoveryClient{ + authorityHost: authorityHost, + armBaseURL: armBaseURL, + client: &http.Client{Timeout: 20 * time.Second}, + } +} + +func DiscoverAzureResources(c *gin.Context) { + userId := c.GetInt("id") + var account model.ResourceBinding + if err := model.DB.Where("id = ? AND user_id = ?", c.Param("id"), userId).First(&account).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + common.ApiErrorMsg(c, "resource not found") + return + } + common.ApiError(c, err) + return + } + if account.ResourceType != "cloud_account" || strings.ToLower(account.Provider) != "azure" { + common.ApiErrorMsg(c, "resource must be an active Azure cloud_account") + return + } + if account.Status != "active" { + common.ApiErrorMsg(c, "resource must be an active Azure cloud_account") + return + } + + secretClient, err := newSecretStoreClientFromEnv() + if err != nil { + common.ApiError(c, err) + return + } + secretData, err := secretClient.getJSONSecret(account.SecretRef) + if err != nil { + common.ApiError(c, err) + return + } + metadata := unmarshalResourceJSON(account.Metadata) + credentials := azureDiscoveryCredentials{ + SubscriptionID: firstString(metadata, secretData, account.ExternalId, "subscription_id"), + TenantID: firstString(metadata, secretData, account.TenantId, "tenant_id"), + ClientID: firstString(metadata, secretData, "", "client_id"), + ClientSecret: firstString(nil, secretData, "", "client_secret"), + } + if err := credentials.validate(); err != nil { + common.ApiError(c, err) + return + } + + azureClient := newAzureResourceDiscoveryClientFromEnv() + token, err := azureClient.clientCredentialsToken(credentials) + if err != nil { + common.ApiError(c, err) + return + } + discovered, err := azureClient.listSubscriptionResources(credentials.SubscriptionID, token) + if err != nil { + common.ApiError(c, err) + return + } + items, err := upsertDiscoveredAzureResources(account, credentials, discovered) + if err != nil { + common.ApiError(c, err) + return + } + common.ApiSuccess(c, gin.H{ + "account": resourceToResponse(account), + "items": items, + "discovered": len(items), + }) +} + +type azureDiscoveryCredentials struct { + SubscriptionID string + TenantID string + ClientID string + ClientSecret string +} + +func (c azureDiscoveryCredentials) validate() error { + if strings.TrimSpace(c.SubscriptionID) == "" { + return errors.New("Azure subscription_id required") + } + if strings.TrimSpace(c.TenantID) == "" { + return errors.New("Azure tenant_id required") + } + if strings.TrimSpace(c.ClientID) == "" { + return errors.New("Azure client_id required") + } + if strings.TrimSpace(c.ClientSecret) == "" { + return errors.New("Azure client_secret required") + } + return nil +} + +func firstString(primary map[string]any, secondary map[string]any, fallback string, key string) string { + if value := mapString(primary, key); value != "" { + return value + } + if value := mapString(secondary, key); value != "" { + return value + } + return strings.TrimSpace(fallback) +} + +func mapString(values map[string]any, key string) string { + if values == nil { + return "" + } + switch value := values[key].(type) { + case string: + return strings.TrimSpace(value) + case fmt.Stringer: + return strings.TrimSpace(value.String()) + default: + return "" + } +} + +func (a azureResourceDiscoveryClient) clientCredentialsToken(credentials azureDiscoveryCredentials) (string, error) { + endpoint := fmt.Sprintf("%s/%s/oauth2/v2.0/token", a.authorityHost, url.PathEscape(credentials.TenantID)) + form := url.Values{} + form.Set("grant_type", "client_credentials") + form.Set("client_id", credentials.ClientID) + form.Set("client_secret", credentials.ClientSecret) + form.Set("scope", "https://management.azure.com/.default") + req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(form.Encode())) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := a.client.Do(req) + if err != nil { + return "", fmt.Errorf("Azure client credential token request failed: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + message := readSecretStoreError(resp.Body) + if message == "" { + message = resp.Status + } + return "", fmt.Errorf("Azure client credential token request failed: %s", message) + } + var payload struct { + AccessToken string `json:"access_token"` + } + if err := common.DecodeJson(resp.Body, &payload); err != nil { + return "", err + } + if strings.TrimSpace(payload.AccessToken) == "" { + return "", errors.New("Azure client credential token response missing access_token") + } + return payload.AccessToken, nil +} + +func (a azureResourceDiscoveryClient) listSubscriptionResources(subscriptionID string, token string) ([]azureARMResource, error) { + endpoint := fmt.Sprintf("%s/subscriptions/%s/resources?api-version=2021-04-01", a.armBaseURL, url.PathEscape(subscriptionID)) + var out []azureARMResource + for page := 0; page < 20 && endpoint != ""; page++ { + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + resp, err := a.client.Do(req) + if err != nil { + return nil, fmt.Errorf("Azure ARM resources request failed: %w", err) + } + var payload azureARMResourceListResponse + if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { + err = common.DecodeJson(resp.Body, &payload) + } else { + message := readSecretStoreError(resp.Body) + if message == "" { + message = resp.Status + } + err = fmt.Errorf("Azure ARM resources request failed: %s", message) + } + resp.Body.Close() + if err != nil { + return nil, err + } + out = append(out, payload.Value...) + endpoint = strings.TrimSpace(payload.NextLink) + } + return out, nil +} + +func upsertDiscoveredAzureResources(account model.ResourceBinding, credentials azureDiscoveryCredentials, discovered []azureARMResource) ([]resourceResponse, error) { + items := make([]resourceResponse, 0, len(discovered)) + for _, armResource := range discovered { + if strings.TrimSpace(armResource.ID) == "" { + continue + } + metadata := map[string]any{ + "subscription_id": credentials.SubscriptionID, + "tenant_id": credentials.TenantID, + "source_account_id": account.Id, + "type": armResource.Type, + "location": armResource.Location, + "resource_group": azureResourceGroupFromID(armResource.ID), + "tags": armResource.Tags, + } + metadataJSON, err := marshalResourceJSON(metadata) + if err != nil { + return nil, err + } + permissionScopeJSON, err := marshalResourceJSON(map[string]any{"actions": []string{"azure:read"}}) + if err != nil { + return nil, err + } + constraints := unmarshalResourceJSON(account.Constraints) + constraintsJSON, err := marshalResourceJSON(constraints) + if err != nil { + return nil, err + } + name := strings.TrimSpace(armResource.Name) + if name == "" { + name = armResource.ID + } + bindingScope := fmt.Sprintf("azure:%s:%s", credentials.SubscriptionID, armResource.ID) + var resource model.ResourceBinding + err = model.DB.Where( + "user_id = ? AND resource_type = ? AND provider = ? AND external_id = ?", + account.UserId, + "cloud_resource", + "azure", + armResource.ID, + ).First(&resource).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + resource.UserId = account.UserId + resource.TenantId = account.TenantId + resource.ProjectId = account.ProjectId + resource.BindingScope = bindingScope + resource.Name = name + resource.ResourceType = "cloud_resource" + resource.Provider = "azure" + resource.ExternalId = armResource.ID + resource.SecretRef = account.SecretRef + resource.Metadata = metadataJSON + resource.PermissionScope = permissionScopeJSON + resource.Constraints = constraintsJSON + resource.Status = "active" + if resource.Id == 0 { + if err := model.DB.Create(&resource).Error; err != nil { + return nil, err + } + } else if err := model.DB.Save(&resource).Error; err != nil { + return nil, err + } + items = append(items, resourceToResponse(resource)) + } + return items, nil +} + +func azureResourceGroupFromID(id string) string { + parts := strings.Split(strings.Trim(id, "/"), "/") + for i := 0; i+1 < len(parts); i++ { + if strings.EqualFold(parts[i], "resourceGroups") { + return parts[i+1] + } + } + return "" +} diff --git a/heicode/controller/resource.go b/heicode/controller/resource.go index aeac6c1..cd4c731 100644 --- a/heicode/controller/resource.go +++ b/heicode/controller/resource.go @@ -1,6 +1,8 @@ package controller import ( + "crypto/sha256" + "encoding/hex" "errors" "fmt" "net/http" @@ -45,6 +47,9 @@ var secretLikeKeys = map[string]bool{ "token": true, } +const azureKeyVaultSecretNameMaxLen = 127 +const azureKeyVaultSecretNameHashLen = 16 + type resourcePayload struct { TenantId string `json:"tenant_id"` ProjectId string `json:"project_id"` @@ -532,12 +537,13 @@ func UpsertResourceSecret(c *gin.Context) { common.ApiError(c, err) return } - secretPath := resourceSecretPath(resource) - if err := client.putKV2(secretPath, payload.Data); err != nil { + secretName := resourceSecretName(resource) + secretRef, err := client.putSecret(secretName, payload.Data) + if err != nil { common.ApiError(c, err) return } - resource.SecretRef = fmt.Sprintf("vault://%s/%s", client.mount, secretPath) + resource.SecretRef = secretRef if err := model.DB.Save(&resource).Error; err != nil { common.ApiError(c, err) return @@ -548,7 +554,7 @@ func UpsertResourceSecret(c *gin.Context) { }) } -func resourceSecretPath(resource model.ResourceBinding) string { +func resourceSecretName(resource model.ResourceBinding) string { scope := resource.BindingScope if strings.TrimSpace(scope) == "" { scope = resource.ExternalId @@ -556,20 +562,17 @@ func resourceSecretPath(resource model.ResourceBinding) string { if strings.TrimSpace(scope) == "" { scope = resource.Name } - return strings.Join([]string{ - "users", - fmt.Sprintf("%d", resource.UserId), - "bindings", + return compactAzureSecretName( + fmt.Sprintf("users-%d-bindings-", resource.UserId), sanitizeSecretPathSegment(scope), - "resources", - fmt.Sprintf("%d", resource.Id), - }, "/") + fmt.Sprintf("-resources-%d", resource.Id), + ) } func sanitizeSecretPathSegment(value string) string { value = strings.TrimSpace(value) if value == "" { - return "_" + return "resource" } var b strings.Builder for _, r := range value { @@ -580,7 +583,7 @@ func sanitizeSecretPathSegment(value string) string { b.WriteRune(r) case r >= '0' && r <= '9': b.WriteRune(r) - case r == '_' || r == '-' || r == '.': + case r == '-': b.WriteRune(r) default: b.WriteRune('-') @@ -589,6 +592,30 @@ func sanitizeSecretPathSegment(value string) string { return b.String() } +func compactAzureSecretName(prefix string, segment string, suffix string) string { + name := prefix + segment + suffix + if len(name) <= azureKeyVaultSecretNameMaxLen { + return name + } + hash := sha256.Sum256([]byte(segment)) + hashSuffix := hex.EncodeToString(hash[:])[:azureKeyVaultSecretNameHashLen] + maxSegmentLen := azureKeyVaultSecretNameMaxLen - len(prefix) - len(suffix) - 1 - azureKeyVaultSecretNameHashLen + if maxSegmentLen < 1 { + maxSegmentLen = 1 + } + if len(segment) > maxSegmentLen { + segment = segment[:maxSegmentLen] + } + segment = strings.Trim(segment, "-") + if segment == "" { + segment = "resource" + if len(segment) > maxSegmentLen { + segment = segment[:maxSegmentLen] + } + } + return prefix + segment + "-" + hashSuffix + suffix +} + func marshalResourcePayloadJSON(payload resourcePayload) (string, string, string, error) { metadata, err := marshalResourceJSON(payload.Metadata) if err != nil { diff --git a/heicode/controller/resource_test.go b/heicode/controller/resource_test.go index 21b9b61..20afb7f 100644 --- a/heicode/controller/resource_test.go +++ b/heicode/controller/resource_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "net/url" "strings" "testing" @@ -65,7 +66,7 @@ func TestCreateResourceStoresMetadataAndSecretRefOnly(t *testing.T) { "resource_type":"git", "provider":"github", "external_id":"https://example.com/org/repo", - "secret_ref":"vault://secret/resources/repo", + "secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo", "metadata":{"repo_url":"https://example.com/org/repo","ref":"main","allowed_paths":["."]}, "permission_scope":{"actions":["read","write"]}, "constraints":{"environment":"dev"} @@ -75,13 +76,13 @@ func TestCreateResourceStoresMetadataAndSecretRefOnly(t *testing.T) { require.Equal(t, http.StatusOK, w.Code) require.Contains(t, w.Body.String(), `"success":true`) require.Contains(t, w.Body.String(), `"binding_scope":"https://example.com/org/repo#main"`) - require.Contains(t, w.Body.String(), `"secret_ref":"vault://secret/resources/repo"`) + require.Contains(t, w.Body.String(), `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo"`) var resource model.ResourceBinding require.NoError(t, db.First(&resource).Error) require.Equal(t, "git", resource.ResourceType) require.Equal(t, "https://example.com/org/repo#main", resource.BindingScope) - require.Equal(t, "vault://secret/resources/repo", resource.SecretRef) + require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/repo", resource.SecretRef) require.NotContains(t, resource.Metadata, "token") require.NotContains(t, resource.PermissionScope, "token") require.NotContains(t, resource.Constraints, "token") @@ -93,7 +94,7 @@ func TestCreateResourceRejectsPlaintextSecretKeys(t *testing.T) { "name":"Cloud account", "resource_type":"cloud_account", "metadata":{"account_id":"sub-1","access_key":"do-not-store"}, - "secret_ref":"vault://secret/cloud/sub-1" + "secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/cloud-sub-1" }` w := performResourceRequest(CreateResource, 7, http.MethodPost, "/", body) @@ -110,7 +111,7 @@ func TestCreateResourceGrantAssignsBoundResourceToRoleAgnet(t *testing.T) { Name: "SK repo", ResourceType: "sk", Provider: "git", - SecretRef: "vault://secret/resources/sk-repo", + SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/sk-repo", Metadata: `{"repo_url":"https://example.com/sk.git"}`, Status: "active", } @@ -173,7 +174,7 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) { ResourceType: "git", Provider: "github", ExternalId: "https://example.com/org/repo.git", - SecretRef: "vault://secret/resources/repo-main", + SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/repo-main", Metadata: `{"repo_url":"https://example.com/org/repo.git"}`, PermissionScope: `{"actions":["read","write"]}`, Status: "active", @@ -215,11 +216,11 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) { require.Contains(t, w.Body.String(), `"target_agent_ref":"agnet-backend-1"`) require.Contains(t, w.Body.String(), `"resource_type":"git"`) require.Contains(t, w.Body.String(), `"allowed_actions":["read"]`) - require.Contains(t, w.Body.String(), `"secret_ref":"vault://secret/resources/repo-main"`) + require.Contains(t, w.Body.String(), `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo-main"`) require.NotContains(t, w.Body.String(), `"allowed_actions":["write"]`) } -func TestUpsertResourceSecretWritesOpenBaoAndStoresOnlySecretRef(t *testing.T) { +func TestUpsertResourceSecretWritesAzureKeyVaultAndStoresOnlySecretRef(t *testing.T) { db := setupResourceControllerTestDB(t) resource := model.ResourceBinding{ UserId: 7, @@ -231,38 +232,50 @@ func TestUpsertResourceSecretWritesOpenBaoAndStoresOnlySecretRef(t *testing.T) { } require.NoError(t, db.Create(&resource).Error) + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + require.Equal(t, "true", r.Header.Get("Metadata")) + require.Equal(t, "2018-02-01", r.URL.Query().Get("api-version")) + require.Equal(t, "https://vault.azure.net", r.URL.Query().Get("resource")) + _, _ = w.Write([]byte(`{"access_token":"manager-token","expires_in":"3600"}`)) + })) + defer tokenServer.Close() + var writtenPath string - var writtenBody map[string]map[string]any - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - require.Equal(t, "/v1/secret/data/users/7/bindings/github-org-repo-main/resources/1", r.URL.Path) - require.Equal(t, "manager-token", r.Header.Get("X-Vault-Token")) + var writtenBody map[string]any + vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPut, r.Method) + require.Equal(t, "Bearer manager-token", r.Header.Get("Authorization")) + require.Equal(t, "/secrets/users-7-bindings-github-org-repo-main-resources-1", r.URL.Path) + require.Equal(t, "7.4", r.URL.Query().Get("api-version")) require.NoError(t, common.DecodeJson(r.Body, &writtenBody)) writtenPath = r.URL.Path w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"data":{"version":1}}`)) + _, _ = w.Write([]byte(`{"id":"https://heicode-kv.vault.azure.net/secrets/users-7-bindings-github-org-repo-main-resources-1/version"}`)) })) - defer server.Close() - t.Setenv("OPENBAO_ADDR", server.URL) - t.Setenv("OPENBAO_TOKEN", "manager-token") - t.Setenv("OPENBAO_KV_MOUNT", "secret") + defer vaultServer.Close() + t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL) + t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL) body := `{"data":{"access_token":"do-not-echo","username":"octo"}}` w := performResourceRequestWithRoute(UpsertResourceSecret, 7, http.MethodPost, "/:id/secret", "/1/secret", body) require.Equal(t, http.StatusOK, w.Code) require.Contains(t, w.Body.String(), `"success":true`) - require.Contains(t, w.Body.String(), `"secret_ref":"vault://secret/users/7/bindings/github-org-repo-main/resources/1"`) + require.Contains(t, w.Body.String(), `"secret_ref":"azkv://127.0.0.1`) + require.Contains(t, w.Body.String(), `/secrets/users-7-bindings-github-org-repo-main-resources-1"`) require.NotContains(t, w.Body.String(), "do-not-echo") - require.Equal(t, "/v1/secret/data/users/7/bindings/github-org-repo-main/resources/1", writtenPath) - require.Equal(t, "do-not-echo", writtenBody["data"]["access_token"]) + require.Equal(t, "/secrets/users-7-bindings-github-org-repo-main-resources-1", writtenPath) + require.Equal(t, "application/json", writtenBody["contentType"]) + require.JSONEq(t, `{"access_token":"do-not-echo","username":"octo"}`, writtenBody["value"].(string)) var stored model.ResourceBinding require.NoError(t, db.First(&stored, resource.Id).Error) - require.Equal(t, "vault://secret/users/7/bindings/github-org-repo-main/resources/1", stored.SecretRef) + require.Contains(t, stored.SecretRef, "azkv://127.0.0.1") + require.Contains(t, stored.SecretRef, "/secrets/users-7-bindings-github-org-repo-main-resources-1") require.NotContains(t, stored.Metadata, "do-not-echo") } -func TestUpsertResourceSecretRequiresSecretStoreToken(t *testing.T) { +func TestUpsertResourceSecretRequiresAzureKeyVaultURL(t *testing.T) { db := setupResourceControllerTestDB(t) resource := model.ResourceBinding{ UserId: 7, @@ -272,14 +285,151 @@ func TestUpsertResourceSecretRequiresSecretStoreToken(t *testing.T) { Status: "active", } require.NoError(t, db.Create(&resource).Error) - t.Setenv("OPENBAO_ADDR", "http://127.0.0.1:8200") - t.Setenv("OPENBAO_TOKEN", "") - t.Setenv("VAULT_TOKEN", "") - t.Setenv("OPENBAO_TOKEN_FILE", "") - t.Setenv("VAULT_TOKEN_FILE", "") + t.Setenv("AZURE_KEY_VAULT_URL", "") w := performResourceRequestWithRoute(UpsertResourceSecret, 7, http.MethodPost, "/:id/secret", "/1/secret", `{"data":{"token":"x"}}`) require.Equal(t, http.StatusOK, w.Code) require.Contains(t, w.Body.String(), `"success":false`) - require.Contains(t, w.Body.String(), "secret store token is not configured") + require.Contains(t, w.Body.String(), "AZURE_KEY_VAULT_URL is not configured") +} + +func TestDiscoverAzureResourcesReadsKeyVaultSecretAndStoresCloudResources(t *testing.T) { + db := setupResourceControllerTestDB(t) + + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + require.Equal(t, "https://vault.azure.net", r.URL.Query().Get("resource")) + _, _ = w.Write([]byte(`{"access_token":"kv-token"}`)) + })) + defer tokenServer.Close() + + vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + require.Equal(t, "Bearer kv-token", r.Header.Get("Authorization")) + require.Equal(t, "/secrets/azure-sub-1", r.URL.Path) + require.Equal(t, "7.4", r.URL.Query().Get("api-version")) + _, _ = w.Write([]byte(`{"value":"{\"client_secret\":\"sp-secret\"}"}`)) + })) + defer vaultServer.Close() + vaultURL, err := url.Parse(vaultServer.URL) + require.NoError(t, err) + + authorityServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "/tenant-a/oauth2/v2.0/token", r.URL.Path) + require.NoError(t, r.ParseForm()) + require.Equal(t, "client_credentials", r.Form.Get("grant_type")) + require.Equal(t, "client-a", r.Form.Get("client_id")) + require.Equal(t, "sp-secret", r.Form.Get("client_secret")) + require.Equal(t, "https://management.azure.com/.default", r.Form.Get("scope")) + _, _ = w.Write([]byte(`{"access_token":"arm-token"}`)) + })) + defer authorityServer.Close() + + armServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + require.Equal(t, "Bearer arm-token", r.Header.Get("Authorization")) + require.Equal(t, "/subscriptions/sub-a/resources", r.URL.Path) + require.Equal(t, "2021-04-01", r.URL.Query().Get("api-version")) + _, _ = w.Write([]byte(`{ + "value": [ + { + "id": "/subscriptions/sub-a/resourceGroups/rg-app/providers/Microsoft.Compute/virtualMachines/vm-api-01", + "name": "vm-api-01", + "type": "Microsoft.Compute/virtualMachines", + "location": "eastus", + "tags": {"env": "test"} + } + ] + }`)) + })) + defer armServer.Close() + + t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL) + t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL) + t.Setenv("AZURE_AUTHORITY_HOST", authorityServer.URL) + t.Setenv("AZURE_ARM_BASE_URL", armServer.URL) + + account := model.ResourceBinding{ + UserId: 7, + BindingScope: "azure:sub-a", + Name: "Test Azure", + ResourceType: "cloud_account", + Provider: "azure", + ExternalId: "sub-a", + SecretRef: fmt.Sprintf("azkv://%s/secrets/azure-sub-1", vaultURL.Host), + Metadata: `{"subscription_id":"sub-a","tenant_id":"tenant-a","client_id":"client-a"}`, + Status: "active", + } + require.NoError(t, db.Create(&account).Error) + + w := performResourceRequestWithRoute( + DiscoverAzureResources, + 7, + http.MethodPost, + "/:id/azure/discover", + "/1/azure/discover", + "", + ) + require.Equal(t, http.StatusOK, w.Code) + require.Contains(t, w.Body.String(), `"success":true`) + require.Contains(t, w.Body.String(), `"resource_group":"rg-app"`) + require.Contains(t, w.Body.String(), `"type":"Microsoft.Compute/virtualMachines"`) + require.NotContains(t, w.Body.String(), "sp-secret") + + var resources []model.ResourceBinding + require.NoError(t, db.Where("user_id = ? AND resource_type = ?", 7, "cloud_resource").Find(&resources).Error) + require.Len(t, resources, 1) + require.Equal(t, "vm-api-01", resources[0].Name) + require.Equal(t, "azure", resources[0].Provider) + require.Equal(t, "active", resources[0].Status) + require.Equal(t, account.SecretRef, resources[0].SecretRef) + require.Contains(t, resources[0].ExternalId, "Microsoft.Compute/virtualMachines/vm-api-01") + require.Contains(t, resources[0].Metadata, `"resource_group":"rg-app"`) + require.NotContains(t, resources[0].Metadata, "sp-secret") +} + +func TestResourceSecretNameFitsAzureKeyVaultRules(t *testing.T) { + name := resourceSecretName(model.ResourceBinding{ + Id: 99, + UserId: 7, + BindingScope: strings.Repeat("repo/with spaces/", 20), + Name: "fallback", + }) + + require.LessOrEqual(t, len(name), azureKeyVaultSecretNameMaxLen) + require.Regexp(t, `^[A-Za-z0-9-]+$`, name) + require.Contains(t, name, "users-7-bindings-") + require.Contains(t, name, "-resources-99") +} + +func TestGetSecretStoreStatusReportsAzureKeyVaultReachable(t *testing.T) { + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "true", r.Header.Get("Metadata")) + _, _ = w.Write([]byte(`{"access_token":"manager-token"}`)) + })) + defer tokenServer.Close() + + vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + require.Equal(t, "/secrets", r.URL.Path) + require.Equal(t, "Bearer manager-token", r.Header.Get("Authorization")) + _, _ = w.Write([]byte(`{"value":[]}`)) + })) + defer vaultServer.Close() + + t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL) + t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL) + + r := gin.New() + r.GET("/status", GetSecretStoreStatus) + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/status", nil)) + + require.Equal(t, http.StatusOK, w.Code) + require.Contains(t, w.Body.String(), `"success":true`) + require.Contains(t, w.Body.String(), `"configured":true`) + require.Contains(t, w.Body.String(), `"reachable":true`) + require.Contains(t, w.Body.String(), `"provider":"azure_key_vault"`) + require.Contains(t, w.Body.String(), "Azure Key Vault is reachable") } diff --git a/heicode/controller/secret_store.go b/heicode/controller/secret_store.go index 726ff9b..d479ce3 100644 --- a/heicode/controller/secret_store.go +++ b/heicode/controller/secret_store.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "strings" "time" @@ -16,73 +17,67 @@ import ( ) type secretStoreClient struct { - address string - mount string - token string - client *http.Client + vaultURL string + vaultHost string + tokenEndpoint string + clientID string + client *http.Client } func newSecretStoreClientFromEnv() (secretStoreClient, error) { - address := strings.TrimRight(strings.TrimSpace(os.Getenv("OPENBAO_ADDR")), "/") - if address == "" { - address = strings.TrimRight(strings.TrimSpace(os.Getenv("VAULT_ADDR")), "/") + vaultURL := strings.TrimRight(strings.TrimSpace(os.Getenv("AZURE_KEY_VAULT_URL")), "/") + if vaultURL == "" { + return secretStoreClient{}, errors.New("AZURE_KEY_VAULT_URL is not configured") } - if address == "" { - address = "http://127.0.0.1:8200" + parsed, err := url.Parse(vaultURL) + if err != nil || parsed.Scheme == "" || parsed.Host == "" { + return secretStoreClient{}, errors.New("AZURE_KEY_VAULT_URL must be an absolute URL") } - token := strings.TrimSpace(os.Getenv("OPENBAO_TOKEN")) - if token == "" { - token = strings.TrimSpace(os.Getenv("VAULT_TOKEN")) + if parsed.Scheme != "https" && !strings.HasPrefix(parsed.Host, "127.0.0.1") && !strings.HasPrefix(parsed.Host, "localhost") { + return secretStoreClient{}, errors.New("AZURE_KEY_VAULT_URL must use https") } - if token == "" { - tokenFile := strings.TrimSpace(os.Getenv("OPENBAO_TOKEN_FILE")) - if tokenFile == "" { - tokenFile = strings.TrimSpace(os.Getenv("VAULT_TOKEN_FILE")) - } - if tokenFile != "" { - data, err := os.ReadFile(tokenFile) - if err != nil { - return secretStoreClient{}, fmt.Errorf("failed to read secret store token file: %w", err) - } - token = strings.TrimSpace(string(data)) - } - } - if token == "" { - return secretStoreClient{}, errors.New("secret store token is not configured") - } - mount := strings.Trim(strings.TrimSpace(os.Getenv("OPENBAO_KV_MOUNT")), "/") - if mount == "" { - mount = strings.Trim(strings.TrimSpace(os.Getenv("VAULT_KV_MOUNT")), "/") - } - if mount == "" { - mount = "secret" + tokenEndpoint := strings.TrimSpace(os.Getenv("AZURE_MANAGED_IDENTITY_TOKEN_URL")) + if tokenEndpoint == "" { + tokenEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token" } return secretStoreClient{ - address: address, - mount: mount, - token: token, - client: &http.Client{Timeout: 10 * time.Second}, + vaultURL: vaultURL, + vaultHost: parsed.Host, + tokenEndpoint: tokenEndpoint, + clientID: strings.TrimSpace(os.Getenv("AZURE_CLIENT_ID")), + client: &http.Client{Timeout: 10 * time.Second}, }, nil } -func (s secretStoreClient) putKV2(path string, data map[string]any) error { +func (s secretStoreClient) putSecret(name string, data map[string]any) (string, error) { if len(data) == 0 { - return errors.New("secret data required") + return "", errors.New("secret data required") } - body, err := common.Marshal(map[string]any{"data": data}) + value, err := common.Marshal(data) if err != nil { - return err + return "", err } - url := fmt.Sprintf("%s/v1/%s/data/%s", s.address, s.mount, strings.Trim(path, "/")) - req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + body, err := common.Marshal(map[string]any{ + "value": string(value), + "contentType": "application/json", + }) if err != nil { - return err + return "", err + } + token, err := s.accessToken() + if err != nil { + return "", err + } + endpoint := fmt.Sprintf("%s/secrets/%s?api-version=7.4", s.vaultURL, url.PathEscape(name)) + req, err := http.NewRequest(http.MethodPut, endpoint, bytes.NewReader(body)) + if err != nil { + return "", err } req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Vault-Token", s.token) + req.Header.Set("Authorization", "Bearer "+token) resp, err := s.client.Do(req) if err != nil { - return err + return "", err } defer resp.Body.Close() if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { @@ -90,121 +85,206 @@ func (s secretStoreClient) putKV2(path string, data map[string]any) error { if message == "" { message = resp.Status } - return fmt.Errorf("secret store write failed: %s", message) + return "", fmt.Errorf("Azure Key Vault secret write failed: %s", message) } - return nil + return s.secretRef(name), nil +} + +func (s secretStoreClient) secretRef(name string) string { + return fmt.Sprintf("azkv://%s/secrets/%s", s.vaultHost, name) +} + +func (s secretStoreClient) getJSONSecret(secretRef string) (map[string]any, error) { + name, err := s.secretNameFromRef(secretRef) + if err != nil { + return nil, err + } + token, err := s.accessToken() + if err != nil { + return nil, err + } + endpoint := fmt.Sprintf("%s/secrets/%s?api-version=7.4", s.vaultURL, url.PathEscape(name)) + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + resp, err := s.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + message := readSecretStoreError(resp.Body) + if message == "" { + message = resp.Status + } + return nil, fmt.Errorf("Azure Key Vault secret read failed: %s", message) + } + var payload struct { + Value string `json:"value"` + } + if err := common.DecodeJson(resp.Body, &payload); err != nil { + return nil, err + } + if strings.TrimSpace(payload.Value) == "" { + return nil, errors.New("Azure Key Vault secret value is empty") + } + var data map[string]any + if err := common.UnmarshalJsonStr(payload.Value, &data); err != nil { + return nil, errors.New("Azure Key Vault secret value must be JSON") + } + if len(data) == 0 { + return nil, errors.New("Azure Key Vault secret value is empty") + } + return data, nil +} + +func (s secretStoreClient) secretNameFromRef(secretRef string) (string, error) { + parsed, err := url.Parse(strings.TrimSpace(secretRef)) + if err != nil || parsed.Scheme != "azkv" || parsed.Host == "" { + return "", errors.New("secret_ref must use azkv:///secrets/") + } + if !strings.EqualFold(parsed.Host, s.vaultHost) { + return "", errors.New("secret_ref vault does not match configured Azure Key Vault") + } + parts := strings.Split(strings.Trim(parsed.Path, "/"), "/") + if len(parts) < 2 || parts[0] != "secrets" || strings.TrimSpace(parts[1]) == "" { + return "", errors.New("secret_ref must use azkv:///secrets/") + } + return parts[1], nil +} + +func (s secretStoreClient) accessToken() (string, error) { + u, err := url.Parse(s.tokenEndpoint) + if err != nil { + return "", fmt.Errorf("invalid Azure managed identity token endpoint: %w", err) + } + q := u.Query() + if q.Get("api-version") == "" { + q.Set("api-version", "2018-02-01") + } + if q.Get("resource") == "" { + q.Set("resource", "https://vault.azure.net") + } + if s.clientID != "" && q.Get("client_id") == "" { + q.Set("client_id", s.clientID) + } + u.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return "", err + } + req.Header.Set("Metadata", "true") + resp, err := s.client.Do(req) + if err != nil { + return "", fmt.Errorf("Azure managed identity token request failed: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + message := readSecretStoreError(resp.Body) + if message == "" { + message = resp.Status + } + return "", fmt.Errorf("Azure managed identity token request failed: %s", message) + } + var payload struct { + AccessToken string `json:"access_token"` + } + if err := common.DecodeJson(resp.Body, &payload); err != nil { + return "", err + } + if strings.TrimSpace(payload.AccessToken) == "" { + return "", errors.New("Azure managed identity token response missing access_token") + } + return payload.AccessToken, nil } func readSecretStoreError(body io.Reader) string { var payload struct { Errors []string `json:"errors"` + Error struct { + Message string `json:"message"` + Code string `json:"code"` + } `json:"error"` } if err := common.DecodeJson(body, &payload); err != nil { return "" } + if payload.Error.Message != "" { + if payload.Error.Code != "" { + return payload.Error.Code + ": " + payload.Error.Message + } + return payload.Error.Message + } return strings.Join(payload.Errors, "; ") } -// secretStoreHealth is what `GET /v1/sys/health` returns on the -// OpenBao/Vault side. We only pull the public-safe fields — version -// string, initialized/sealed/standby flags, server time. No cluster -// IDs, no auth lease ttls, nothing that could leak operator detail. -type secretStoreHealth struct { - Initialized bool `json:"initialized"` - Sealed bool `json:"sealed"` - Standby bool `json:"standby"` - Version string `json:"version"` - ServerTime int64 `json:"server_time_utc"` -} - -// fetchHealth pings the OpenBao /sys/health endpoint. That endpoint -// is intentionally token-less in upstream Vault — it's the standard -// liveness probe — so we don't include X-Vault-Token here. Vault -// returns non-2xx HTTP codes for sealed/uninitialized states by -// design (200=initialized+unsealed, 429=standby, 472=DR secondary, -// 473=performance standby, 501=not initialized, 503=sealed). All of -// those still ship a JSON body with the same fields, so we always -// decode and let the caller interpret the status field separately. -func (s secretStoreClient) fetchHealth() (secretStoreHealth, int, error) { - url := fmt.Sprintf("%s/v1/sys/health", s.address) - req, err := http.NewRequest(http.MethodGet, url, nil) +func (s secretStoreClient) probe() (int, error) { + token, err := s.accessToken() if err != nil { - return secretStoreHealth{}, 0, err + return 0, err } + endpoint := s.vaultURL + "/secrets?api-version=7.4&maxresults=1" + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return 0, err + } + req.Header.Set("Authorization", "Bearer "+token) resp, err := s.client.Do(req) if err != nil { - return secretStoreHealth{}, 0, err + return 0, err } defer resp.Body.Close() - var h secretStoreHealth - if err := common.DecodeJson(resp.Body, &h); err != nil { - return secretStoreHealth{}, resp.StatusCode, err - } - return h, resp.StatusCode, nil + _, _ = io.Copy(io.Discard, resp.Body) + return resp.StatusCode, nil } // secretStoreStatusResponse is the public-safe envelope rendered by -// the admin status page. Crucially does NOT include the configured -// address (could leak internal network topology), the token, the -// mount path, or any secret names. +// the admin status page. It never includes secret names or values. type secretStoreStatusResponse struct { - Configured bool `json:"configured"` // env vars present? - Reachable bool `json:"reachable"` // /sys/health responded? - Initialized bool `json:"initialized,omitempty"` - Sealed bool `json:"sealed,omitempty"` - Standby bool `json:"standby,omitempty"` - Version string `json:"version,omitempty"` - Message string `json:"message,omitempty"` // human-readable status - CheckedAt int64 `json:"checked_at"` // unix ms (server clock) + Configured bool `json:"configured"` // env vars present? + Reachable bool `json:"reachable"` // Key Vault data plane responded? + Provider string `json:"provider,omitempty"` + AuthMethod string `json:"auth_method,omitempty"` + Message string `json:"message,omitempty"` // human-readable status + CheckedAt int64 `json:"checked_at"` // unix ms (server clock) } -// GetSecretStoreStatus returns a sanitized vault health snapshot for -// the admin "密钥保管器" status panel. Admin-only (route is mounted -// under the rootRoute group in api-router.go so non-admins can't even -// reach it). The handler is deliberately tolerant: missing env vars, -// network errors, and sealed-vault responses ALL render usable JSON -// the UI can present — we don't bubble HTTP 500 for any of them, the -// "missing config" / "network error" / "sealed" states are valid -// operating modes the UI must visualise distinctly. +// GetSecretStoreStatus returns a sanitized Azure Key Vault status snapshot. func GetSecretStoreStatus(c *gin.Context) { out := secretStoreStatusResponse{ - CheckedAt: time.Now().UnixMilli(), + CheckedAt: time.Now().UnixMilli(), + Provider: "azure_key_vault", + AuthMethod: "managed_identity", } client, err := newSecretStoreClientFromEnv() if err != nil { - // Most common case in current production: OPENBAO_TOKEN not - // set. Render as "not configured" instead of a server error. out.Configured = false - out.Message = "secret store env vars not set (OPENBAO_ADDR / OPENBAO_TOKEN). The vault container may still be running but Manager is not wired up to it yet." + out.Message = err.Error() c.JSON(http.StatusOK, gin.H{"success": true, "data": out}) return } out.Configured = true - health, status, err := client.fetchHealth() + status, err := client.probe() if err != nil { out.Reachable = false - out.Message = "network error contacting secret store: " + err.Error() + out.Message = "Azure Key Vault probe failed: " + err.Error() c.JSON(http.StatusOK, gin.H{"success": true, "data": out}) return } out.Reachable = true - out.Initialized = health.Initialized - out.Sealed = health.Sealed - out.Standby = health.Standby - out.Version = health.Version - // Vault uses non-2xx HTTP for sealed/uninit states — surface a - // short reason so the UI can show a coloured pill without parsing - // the boolean matrix itself. switch { - case !health.Initialized: - out.Message = fmt.Sprintf("secret store reachable but not initialized (HTTP %d)", status) - case health.Sealed: - out.Message = fmt.Sprintf("secret store reachable but SEALED — operator must unseal (HTTP %d)", status) - case health.Standby: - out.Message = fmt.Sprintf("secret store in standby mode (HTTP %d)", status) + case status == http.StatusForbidden: + out.Message = "Azure Key Vault reachable, but managed identity lacks list permission; secret writes can still work if set permission is granted" + case status == http.StatusUnauthorized: + out.Message = "Azure Key Vault reachable, but managed identity was not authorized" + case status >= http.StatusOK && status < http.StatusMultipleChoices: + out.Message = "Azure Key Vault is reachable" default: - out.Message = "secret store is healthy" + out.Message = fmt.Sprintf("Azure Key Vault returned HTTP %d", status) } c.JSON(http.StatusOK, gin.H{"success": true, "data": out}) } diff --git a/heicode/docker-compose.azure-vm.yml b/heicode/docker-compose.azure-vm.yml index a4ca882..dea2bf9 100644 --- a/heicode/docker-compose.azure-vm.yml +++ b/heicode/docker-compose.azure-vm.yml @@ -11,6 +11,7 @@ # SQL_DSN=postgresql://...@heicode.postgres.database.azure.com:5432/heicode?sslmode=require # REDIS_CONN_STRING=rediss://:...@heicode.redis.cache.windows.net:6380 # SESSION_SECRET=... +# AZURE_KEY_VAULT_URL=https://.vault.azure.net # (以及项目要求的其它密钥,见官方 heicode 文档) # # 3. 构建 Heicode 定制镜像时与本地 override 一致: @@ -47,6 +48,10 @@ services: # Agnet 登录后 JIT 同步:邮箱命中以下白名单则自动提权 - HEICODE_ROOT_EMAILS=${HEICODE_ROOT_EMAILS:-} - HEICODE_ADMIN_EMAILS=${HEICODE_ADMIN_EMAILS:-} + # Long-lived resource credentials are written to Azure Key Vault via + # the VM managed identity. No static secret-store token is required. + - AZURE_KEY_VAULT_URL=${AZURE_KEY_VAULT_URL:-} + - AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-} networks: - heicode-network healthcheck: diff --git a/heicode/relay/helper/stream_scanner.go b/heicode/relay/helper/stream_scanner.go index 0daf11e..7b7c4ac 100644 --- a/heicode/relay/helper/stream_scanner.go +++ b/heicode/relay/helper/stream_scanner.go @@ -23,6 +23,7 @@ import ( const ( InitialScannerBufferSize = 64 << 10 // 64KB (64*1024) DefaultMaxScannerBufferSize = 64 << 20 // 64MB (64*1024*1024) default SSE buffer size + DefaultStreamingTimeout = 30 * time.Second DefaultPingInterval = 15 * time.Second ) @@ -51,6 +52,9 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon }() streamingTimeout := time.Duration(constant.StreamingTimeout) * time.Second + if streamingTimeout <= 0 { + streamingTimeout = DefaultStreamingTimeout + } var ( stopChan = make(chan bool, 3) // 增加缓冲区避免阻塞 diff --git a/heicode/router/api-router.go b/heicode/router/api-router.go index 00132a9..d3c7fe0 100644 --- a/heicode/router/api-router.go +++ b/heicode/router/api-router.go @@ -29,10 +29,11 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/uptime/status", controller.GetUptimeKumaStatus) apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels) apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus) - // M7 — secret-store (OpenBao / Vault) status snapshot for the - // admin "密钥保管器" panel. Admin-only because the response - // reveals seal state + version which we don't want public. - apiRouter.GET("/secret-store/status", middleware.AdminAuth(), controller.GetSecretStoreStatus) + // Secret-store status snapshot. The handler returns only + // sanitized Azure Key Vault config/reachability flags and never + // exposes addresses, tokens, secret names or secret values, so + // resource binding can read it as an authenticated user. + apiRouter.GET("/secret-store/status", middleware.UserAuth(), controller.GetSecretStoreStatus) apiRouter.GET("/notice", controller.GetNotice) apiRouter.GET("/user-agreement", controller.GetUserAgreement) apiRouter.GET("/privacy-policy", controller.GetPrivacyPolicy) @@ -215,6 +216,7 @@ func SetApiRouter(router *gin.Engine) { resourceRoute.GET("/", controller.ListResources) resourceRoute.POST("/", controller.CreateResource) resourceRoute.POST("/:id/secret", controller.UpsertResourceSecret) + resourceRoute.POST("/:id/azure/discover", controller.DiscoverAzureResources) resourceRoute.PUT("/:id", controller.UpdateResource) resourceRoute.DELETE("/:id", controller.DeleteResource) } diff --git a/heicode/web/classic/dist/index.html b/heicode/web/classic/dist/index.html new file mode 100644 index 0000000..ae76d08 --- /dev/null +++ b/heicode/web/classic/dist/index.html @@ -0,0 +1,10 @@ + + + + + Heicode Manager classic placeholder + + +
+ + diff --git a/heicode/web/default/.gitignore b/heicode/web/default/.gitignore index a613398..2ecb96d 100644 --- a/heicode/web/default/.gitignore +++ b/heicode/web/default/.gitignore @@ -9,6 +9,9 @@ lerna-debug.log* node_modules dist +!dist/ +dist/* +!dist/index.html dist-ssr *.local @@ -24,4 +27,4 @@ dist-ssr *.njsproj *.sln *.sw? -.tanstack/* \ No newline at end of file +.tanstack/* diff --git a/heicode/web/default/bun.lock b/heicode/web/default/bun.lock index 56e0a3f..56334fa 100644 --- a/heicode/web/default/bun.lock +++ b/heicode/web/default/bun.lock @@ -8,6 +8,7 @@ "@fontsource-variable/public-sans": "^5.2.7", "@hookform/resolvers": "^5.2.2", "@lobehub/icons": "^4.0.3", + "@lobehub/ui": "^5.14.0", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.11", @@ -30,6 +31,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-use-controllable-state": "^1.2.2", + "@rspack/binding-win32-x64-msvc": "^2.0.0", "@tailwindcss/postcss": "^4.2.2", "@tanstack/react-query": "^5.95.2", "@tanstack/react-router": "^1.168.23", @@ -38,6 +40,7 @@ "@visactor/react-vchart": "^2.0.13", "@visactor/vchart": "^2.0.13", "ai": "^6.0.27", + "antd": "^6.4.3", "auto-skeleton-react": "^1.0.5", "axios": "^1.13.6", "class-variance-authority": "^0.7.1", @@ -83,6 +86,7 @@ "@tanstack/react-router-devtools": "^1.166.13", "@tanstack/router-plugin": "^1.167.23", "@trivago/prettier-plugin-sort-imports": "^6.0.2", + "@types/hast": "^3.0.4", "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -118,7 +122,7 @@ "@ant-design/fast-color": ["@ant-design/fast-color@3.0.1", "", {}, "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw=="], - "@ant-design/icons": ["@ant-design/icons@6.1.1", "", { "dependencies": { "@ant-design/colors": "^8.0.0", "@ant-design/icons-svg": "^4.4.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q=="], + "@ant-design/icons": ["@ant-design/icons@6.2.3", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.4.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg=="], "@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="], @@ -382,7 +386,7 @@ "@lobehub/icons": ["@lobehub/icons@4.12.0", "", { "dependencies": { "antd-style": "^4.1.0", "lucide-react": "^0.469.0", "polished": "^4.3.1" }, "peerDependencies": { "@lobehub/ui": "^4.3.3", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-DVH7pVzM6wEvua2LXH+Iv10/cLeBbueggMFBHa8IlfQel5u3I6JzuaNXXxj2qJu5QYjUCNL5LTpSWuh4TnuLGw=="], - "@lobehub/ui": ["@lobehub/ui@4.38.4", "", { "dependencies": { "@ant-design/cssinjs": "^2.0.3", "@base-ui/react": "1.0.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@emotion/is-prop-valid": "^1.4.0", "@floating-ui/react": "^0.27.17", "@giscus/react": "^3.1.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@pierre/diffs": "^1.0.10", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^3.22.0", "@shikijs/transformers": "^3.22.0", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.6", "antd-style": "^4.1.0", "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.19", "emoji-mart": "^5.6.0", "es-toolkit": "^1.44.0", "fast-deep-equal": "^3.1.3", "immer": "^11.1.3", "katex": "^0.16.28", "leva": "^0.10.1", "lucide-react": "^0.563.0", "marked": "^17.0.1", "mermaid": "^11.12.2", "motion": "^12.30.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^14.0.0", "react-error-boundary": "^6.1.0", "react-hotkeys-hook": "^5.2.4", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.2", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.2.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^1.2.3", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "remend": "^1.2.0", "shiki": "^3.22.0", "shiki-stream": "^0.1.4", "swr": "^2.4.0", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0", "virtua": "^0.48.5" }, "peerDependencies": { "@lobehub/fluent-emoji": "^4.0.0", "@lobehub/icons": "^4.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-FYQeWkR0CoZCaPqEX9AUGrhaIfkYeuacW2KtV+1GS7eGVjREFNNOAgY5PLk20ZMYV/cRFsn9fNG0rqn9PxChxw=="], + "@lobehub/ui": ["@lobehub/ui@5.15.0", "", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@base-ui/react": "1.0.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@emotion/is-prop-valid": "^1.4.0", "@floating-ui/react": "^0.27.19", "@giscus/react": "^3.1.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@pierre/diffs": "^1.1.19", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^4.0.2", "@shikijs/transformers": "^4.0.2", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.7", "antd-style": "^4.1.0", "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.20", "emoji-mart": "^5.6.0", "es-toolkit": "^1.46.0", "fast-deep-equal": "^3.1.3", "immer": "^11.1.4", "katex": "^0.16.45", "leva": "^0.10.1", "lucide-react": "^1.11.0", "marked": "^17.0.6", "mermaid": "^11.14.0", "motion": "^12.38.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^15.1.0", "react-error-boundary": "^6.1.1", "react-hotkeys-hook": "^5.2.4", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.3", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.2.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^2.0.1", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "remend": "^1.3.0", "shiki": "^4.0.2", "shiki-stream": "^0.1.4", "swr": "^2.4.1", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0", "virtua": "^0.49.1" }, "peerDependencies": { "@lobehub/fluent-emoji": "^4.0.0", "@lobehub/icons": "^5.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-zh+xWZiOX/yEuV5Vxp7234bXCu7X5lXzLtrh95DMPkaU22lkLOWAjBT24OV5uroc0YjpmmJNfwDB0VQsAXnrAg=="], "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], @@ -498,9 +502,9 @@ "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.19.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw=="], - "@pierre/diffs": ["@pierre/diffs@1.1.13", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-lnX9Fy5eC+07b8g+D8krC3txOY6LRN5VNR1qr9bph9XEyLxbwwfGN7SFRu4HGozpkDdA76JARgxgWHN+uAihmg=="], + "@pierre/diffs": ["@pierre/diffs@1.2.3", "", { "dependencies": { "@pierre/theme": "1.0.3", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-ul83DHH1yqgGxJAw2tqQm2gDO+oQsaF82ZVocwJYfXAm2FhZyyKPTdtv6jswR4A5eF/ILPjiQxyfScMhQcofbA=="], - "@pierre/theme": ["@pierre/theme@0.0.28", "", {}, "sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw=="], + "@pierre/theme": ["@pierre/theme@1.0.3", "", {}, "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA=="], "@primer/octicons": ["@primer/octicons@19.23.1", "", { "dependencies": { "object-assign": "^4.1.1" } }, "sha512-CzjGmxkmNhyst6EekrS3SJPdtzgIkUMP/LSJch65y99/kmiFXbO1a+q7zoYe3hnI9NaOM0IN+ydDIbOmd8YqcA=="], @@ -602,7 +606,7 @@ "@rc-component/async-validator": ["@rc-component/async-validator@5.1.0", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA=="], - "@rc-component/cascader": ["@rc-component/cascader@1.14.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ=="], + "@rc-component/cascader": ["@rc-component/cascader@1.15.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.3.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw=="], "@rc-component/checkbox": ["@rc-component/checkbox@2.0.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ=="], @@ -612,23 +616,23 @@ "@rc-component/context": ["@rc-component/context@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw=="], - "@rc-component/dialog": ["@rc-component/dialog@1.8.4", "", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw=="], + "@rc-component/dialog": ["@rc-component/dialog@1.9.0", "", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A=="], "@rc-component/drawer": ["@rc-component/drawer@1.4.2", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.1.3", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q=="], "@rc-component/dropdown": ["@rc-component/dropdown@1.0.2", "", { "dependencies": { "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg=="], - "@rc-component/form": ["@rc-component/form@1.8.0", "", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.6.2", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-eUD5KKYnIZWmJwRA0vnyO/ovYUfHGU1svydY1OrqU5fw8Oz9Tdqvxvrlh0wl6xI/EW69dT7II49xpgOWzK3T5A=="], + "@rc-component/form": ["@rc-component/form@1.8.1", "", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.6.2", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-8O7TB55Fi2mWIGvSnwZjk8jFqVNYyKDAswglwGShcbndxqzKz4cHwNtNaLjZlAeRge9wcB0LL8IWsC/Bl18raQ=="], - "@rc-component/image": ["@rc-component/image@1.8.1", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-JfPCijmMl+EaMvbftsEs/4VHmTyJKsZBh5ujFowSA45i9NTVYS1vuHtgpVV/QrGa27kXwbVOZriffCe/PNKuMw=="], + "@rc-component/image": ["@rc-component/image@1.9.0", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ=="], - "@rc-component/input": ["@rc-component/input@1.1.2", "", { "dependencies": { "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg=="], + "@rc-component/input": ["@rc-component/input@1.3.1", "", { "dependencies": { "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig=="], "@rc-component/input-number": ["@rc-component/input-number@1.6.2", "", { "dependencies": { "@rc-component/mini-decimal": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w=="], - "@rc-component/mentions": ["@rc-component/mentions@1.6.0", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/menu": "~1.2.0", "@rc-component/textarea": "~1.1.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ=="], + "@rc-component/mentions": ["@rc-component/mentions@1.9.0", "", { "dependencies": { "@rc-component/input": "~1.3.0", "@rc-component/menu": "~1.3.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g=="], - "@rc-component/menu": ["@rc-component/menu@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg=="], + "@rc-component/menu": ["@rc-component/menu@1.3.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-u3NfiwpiEgT177qa5Yxm5QsI8i/93EBGpWj8HYZQDnh2pCZ2xtQCe/+w3pSR2NlwKOZDTCKzEhEyD09mGphssA=="], "@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw=="], @@ -636,13 +640,13 @@ "@rc-component/mutate-observer": ["@rc-component/mutate-observer@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w=="], - "@rc-component/notification": ["@rc-component/notification@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA=="], + "@rc-component/notification": ["@rc-component/notification@2.0.7", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ=="], "@rc-component/overflow": ["@rc-component/overflow@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw=="], "@rc-component/pagination": ["@rc-component/pagination@1.2.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw=="], - "@rc-component/picker": ["@rc-component/picker@1.9.1", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g=="], + "@rc-component/picker": ["@rc-component/picker@1.10.0", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w=="], "@rc-component/portal": ["@rc-component/portal@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg=="], @@ -664,25 +668,23 @@ "@rc-component/switch": ["@rc-component/switch@1.0.3", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw=="], - "@rc-component/table": ["@rc-component/table@1.9.1", "", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.1.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg=="], + "@rc-component/table": ["@rc-component/table@1.10.0", "", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.1.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-SjtpcCf+rL7dDc62GKT3rXTdERjVuJvRiqjpU7g0Jc/ewCifXynHc7Nm3Em1XsD+WhGrgQtxNDScI/0+Lpfr0w=="], - "@rc-component/tabs": ["@rc-component/tabs@1.7.0", "", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w=="], - - "@rc-component/textarea": ["@rc-component/textarea@1.1.2", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A=="], + "@rc-component/tabs": ["@rc-component/tabs@1.9.0", "", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.3.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-tn1slmbbaTyt8mgwyWJcT8jo/qNiYUs6u1H7OgGQt9faYO06BJIkU5cTmMqORzIrNmSEeeUY6pD5i+JlqSHYhg=="], "@rc-component/tooltip": ["@rc-component/tooltip@1.4.0", "", { "dependencies": { "@rc-component/trigger": "^3.7.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg=="], - "@rc-component/tour": ["@rc-component/tour@2.3.0", "", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow=="], + "@rc-component/tour": ["@rc-component/tour@2.4.0", "", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg=="], - "@rc-component/tree": ["@rc-component/tree@1.2.4", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.8.1", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w=="], + "@rc-component/tree": ["@rc-component/tree@1.3.2", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.11.1", "@rc-component/virtual-list": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-bJFj46wEkpBPnWyTm18XmgAgNQ/4YvprxMOPPY2a6rmhGJYxLuNKEFiL5Qej4Qctu9wHJm8WW+v2SYskafE0kA=="], - "@rc-component/tree-select": ["@rc-component/tree-select@1.8.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ=="], + "@rc-component/tree-select": ["@rc-component/tree-select@1.9.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.3.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w=="], "@rc-component/trigger": ["@rc-component/trigger@3.9.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.2.0", "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg=="], "@rc-component/upload": ["@rc-component/upload@1.1.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw=="], - "@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + "@rc-component/util": ["@rc-component/util@1.11.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-awVlI3ub2vqfqkYxOBc/uQ0efm3jw0wcrhtO/YWLyZfxiKXczKwNbVuhlnyxytDt7H9pbbVQiqr+O6MLATtRYg=="], "@rc-component/virtual-list": ["@rc-component/virtual-list@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ=="], @@ -756,7 +758,7 @@ "@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="], - "@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], + "@shikijs/transformers": ["@shikijs/transformers@4.1.0", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/types": "4.1.0" } }, "sha512-YbuOcAA3kwqKDU9YSt00dtFLrY5lBXjKU3dWaMATyEyPSqBm9Jqblk/uVICxz7lcjwAHzYaEvIiMWX3mTpogkA=="], "@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], @@ -1058,7 +1060,7 @@ "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], - "antd": ["antd@6.3.5", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.1.1", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.28.4", "@rc-component/cascader": "~1.14.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.8.4", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.8.0", "@rc-component/image": "~1.8.0", "@rc-component/input": "~1.1.2", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.6.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~1.2.0", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.9.1", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.9.1", "@rc-component/tabs": "~1.7.0", "@rc-component/textarea": "~1.1.2", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.3.0", "@rc-component/tree": "~1.2.4", "@rc-component/tree-select": "~1.8.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.10.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-8BPz9lpZWQm42PTx7yL4KxWAotVuqINiKcoYRcLtdd5BFmAcAZicVyFTnBJyRDlzGZFZeRW3foGu6jXYFnej6Q=="], + "antd": ["antd@6.4.3", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.2.3", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.29.2", "@rc-component/cascader": "~1.15.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.9.0", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.8.1", "@rc-component/image": "~1.9.0", "@rc-component/input": "~1.3.0", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.9.0", "@rc-component/menu": "~1.3.0", "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~2.0.7", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.10.0", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.10.0", "@rc-component/tabs": "~1.9.0", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.4.0", "@rc-component/tree": "~1.3.1", "@rc-component/tree-select": "~1.9.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-6H2avkxCGfxcF67r3J2mwm9Ck50el1pks/73vfM1wDsPL/tPtj5vHuauMgJFnrqmq7CH3g8aoZ0VBQbt+jpAsw=="], "antd-style": ["antd-style@4.1.0", "", { "dependencies": { "@ant-design/cssinjs": "^2.0.0", "@babel/runtime": "^7.24.1", "@emotion/cache": "^11.11.0", "@emotion/css": "^11.11.2", "@emotion/react": "^11.11.4", "@emotion/serialize": "^1.1.3", "@emotion/utils": "^1.2.1", "use-merge-value": "^1.2.0" }, "peerDependencies": { "antd": ">=6.0.0", "react": ">=18" } }, "sha512-vnPBGg0OVlSz90KRYZhxd89aZiOImTiesF+9MQqN8jsLGZUQTjbP04X9jTdEfsztKUuMbBWg/RmB/wHTakbtMQ=="], @@ -1212,7 +1214,7 @@ "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], - "d3-array": ["d3-array@1.2.4", "", {}, "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], @@ -1360,7 +1362,7 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="], + "es-toolkit": ["es-toolkit@1.47.0", "", {}, "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw=="], "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], @@ -1856,9 +1858,9 @@ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], - "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gRzVLUdjXBLX6zNPSnHGDoo+ZTp5zy+MZm0g3sv+3chPXY7l9gW+DnrcHcZh/jiPR6MjPKO4AEJNp4Aw6V9z5Q=="], + "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@2.0.1", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw=="], - "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@2.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-egs6+12JU2yutskHY55FyR48ZiEcFOJFyk9rsiyIhcJ6IvWB6ABBqVrBw8IobqJTDZ/wdSr9eoXDPb5S2nW1bg=="], + "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@3.0.1", "", { "dependencies": { "get-east-asian-width": "^1.4.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w=="], "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], @@ -2132,7 +2134,7 @@ "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], - "react-avatar-editor": ["react-avatar-editor@14.0.0", "", { "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NaQM3oo4u0a1/Njjutc2FjwKX35vQV+t6S8hovsbAlMpBN1ntIwP/g+Yr9eDIIfaNtRXL0AqboTnPmRxhD/i8A=="], + "react-avatar-editor": ["react-avatar-editor@15.1.0", "", { "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Zto7u9l6Wd5LPPtjeFJ+7uwoT4bs01OSgkN2kxD18lWl8IiZ0GY3nWCbKPx4qIU7Au1vENsMJm19rfVWHHayaQ=="], "react-colorful": ["react-colorful@5.6.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw=="], @@ -2210,7 +2212,7 @@ "remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="], - "remark-cjk-friendly": ["remark-cjk-friendly@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g=="], + "remark-cjk-friendly": ["remark-cjk-friendly@2.0.1", "", { "dependencies": { "micromark-extension-cjk-friendly": "2.0.1" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-6WwkoQyZf/4j5k53zdFYrR8Ca+UVn992jXdLUSBDZR4eBpFhKyVxmA4gUHra/5fesjGIxrDhHesNr/sVoiiysA=="], "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], @@ -2512,7 +2514,7 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "virtua": ["virtua@0.48.8", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-jpsxOw5V4B6hg44JePRLo9DL0TV7N1lBEVtPjKpAJebXyhI2s9lfiXJESaLapNtr3vtiSk/pWHiLf7B2a6UcgQ=="], + "virtua": ["virtua@0.49.1", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-6f79msqg3jzNFdqJiS0FSzhRN1EHlDhR7EvW7emp6z5qQ22VdsReiDHflkpMEMhoAyUuYr69nwT0aagiM7NrUg=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], @@ -2574,6 +2576,10 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@ant-design/cssinjs/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@ant-design/cssinjs-utils/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], @@ -2598,20 +2604,20 @@ "@eslint/config-array/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@lobehub/fluent-emoji/es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="], + "@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], "@lobehub/icons/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="], - "@lobehub/ui/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - - "@lobehub/ui/lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="], - - "@lobehub/ui/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + "@lobehub/ui/lucide-react": ["lucide-react@1.16.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ=="], "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], + "@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "@pierre/diffs/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], @@ -2644,21 +2650,69 @@ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "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" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@rc-component/checkbox/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/collapse/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/color-picker/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/context/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + "@rc-component/dialog/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], "@rc-component/drawer/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], + "@rc-component/drawer/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/dropdown/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + "@rc-component/image/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], + "@rc-component/input-number/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/motion/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/mutate-observer/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/overflow/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/pagination/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/progress/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/rate/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/resize-observer/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/segmented/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/select/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/slider/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/steps/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/switch/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/tooltip/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + "@rc-component/tour/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], + "@rc-component/tree/@rc-component/virtual-list": ["@rc-component/virtual-list@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-iavRm1Jo4GDbASQwdGa7jFyk93RvSOo9xHyBT4QL1pgFJj/Fdf1G+3RErH7/7BmAMvx2AkF62mjGYxDbXsK9TQ=="], + "@rc-component/trigger/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], + "@rc-component/trigger/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/upload/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/virtual-list/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + "@rspack/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + "@shikijs/transformers/@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="], - "@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@shikijs/transformers/@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], @@ -2706,26 +2760,22 @@ "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], - "d3/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - "d3/d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], "d3/d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], - "d3-contour/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - "d3-dsv/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "d3-dsv/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "d3-fetch/d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + "d3-geo/d3-array": ["d3-array@1.2.4", "", {}, "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="], + + "d3-sankey/d3-array": ["d3-array@1.2.4", "", {}, "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="], + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "d3-scale/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - - "d3-time/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - "eslint/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], @@ -2822,20 +2872,12 @@ "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], - "@lobehub/ui/@shikijs/core/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - - "@lobehub/ui/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], - - "@lobehub/ui/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], - - "@lobehub/ui/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], - - "@lobehub/ui/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], - - "@lobehub/ui/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@pierre/diffs/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], "@pierre/diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], @@ -2848,6 +2890,14 @@ "@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@rc-component/dialog/@rc-component/portal/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/image/@rc-component/portal/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@rc-component/tour/@rc-component/portal/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + + "@shikijs/transformers/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="], + "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], diff --git a/heicode/web/default/dist/index.html b/heicode/web/default/dist/index.html new file mode 100644 index 0000000..8108841 --- /dev/null +++ b/heicode/web/default/dist/index.html @@ -0,0 +1,10 @@ + + + + + Heicode Manager placeholder + + +
+ + diff --git a/heicode/web/default/src/components/query-state.tsx b/heicode/web/default/src/components/query-state.tsx index 812e0c7..f636dd8 100644 --- a/heicode/web/default/src/components/query-state.tsx +++ b/heicode/web/default/src/components/query-state.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import { ShieldOff, WifiOff } from 'lucide-react' +import { ShieldOff, WifiOff, type LucideIcon } from 'lucide-react' import { useTranslation } from 'react-i18next' import { EmptyState } from './empty-state' import { ErrorState } from './error-state' @@ -41,7 +41,7 @@ interface QueryStateProps { loadingFallback?: ReactNode emptyTitle?: string emptyDescription?: string - emptyIcon?: React.ComponentType<{ className?: string }> + emptyIcon?: LucideIcon emptyAction?: ReactNode } diff --git a/heicode/web/default/src/features/agnet-console/azure-cloud-binding-sheet.tsx b/heicode/web/default/src/features/agnet-console/azure-cloud-binding-sheet.tsx index 0587a5b..4a47141 100644 --- a/heicode/web/default/src/features/agnet-console/azure-cloud-binding-sheet.tsx +++ b/heicode/web/default/src/features/agnet-console/azure-cloud-binding-sheet.tsx @@ -1,7 +1,14 @@ import { useState } from 'react' -import { useTranslation } from 'react-i18next' import { useMutation, useQueryClient } from '@tanstack/react-query' import { AlertTriangle, Cloud, ShieldCheck } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' +import { + createManagerResource, + discoverManagerAzureResources, + putManagerResourceSecret, + updateManagerResource, +} from '@/lib/manager-resources' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' @@ -12,28 +19,21 @@ import { SheetHeader, SheetTitle, } from '@/components/ui/sheet' -import { toast } from 'sonner' -import { createResource } from '@/lib/heicode-mcp' -// M2 phase 1 — Azure cloud account binding via manual Service Principal -// credentials. Phase 2 (real OAuth code flow) and phase 3 (ARM resource -// auto-discovery) require Azure App Registration + OpenBao to be online -// first; until then, operators paste SP creds manually here. The sheet -// is deliberately explicit about the security caveat: when OpenBao -// isn't configured, the client_secret would be stored alongside the -// resource binding, NOT in a vault. Caller sees a yellow warning until -// vault is wired up. +// Azure cloud account binding via manual Service Principal credentials. +// The Manager creates a local ResourceBinding first, writes the +// client_secret to Azure Key Vault via /api/resources/:id/secret, stores +// only azkv://... on the resource row, then asks Manager to enumerate ARM +// resources and persist them as cloud_resource bindings. // -// Field map → mcp-server ResourceBinding: -// type = 'cloud_account' +// Field map → Manager ResourceBinding: +// resource_type = 'cloud_account' // name = display_name -// external_ref = subscription_id -// metadata = { provider, tenant_id, client_id } -// secret_ref = vault:// (intentionally empty during phase 1; -// server should refuse to materialise creds until -// OpenBao is wired up) -// constraints = {} (resource group filters land in phase 3) -// permission_scope= ['azure:read'] (read-only ARM access scope) +// external_id = subscription_id +// metadata = { subscription_id, tenant_id, client_id } +// secret_ref = azkv:///secrets/ +// constraints = {} (resource group filters can be added later) +// permission_scope= { actions: ['azure:read'] } interface Props { open: boolean @@ -41,7 +41,11 @@ interface Props { vaultConfigured?: boolean } -export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }: Props) { +export function AzureCloudBindingSheet({ + open, + onOpenChange, + vaultConfigured, +}: Props) { const { t } = useTranslation() const qc = useQueryClient() @@ -54,28 +58,68 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }: }) const mutation = useMutation({ - mutationFn: () => - createResource({ - type: 'cloud_account', + mutationFn: async () => { + if (!vaultConfigured) { + throw new Error(t('Azure Key Vault is not ready')) + } + const pending = await createManagerResource({ + resource_type: 'cloud_account', name: form.name.trim(), - external_ref: form.subscription_id.trim(), + provider: 'azure', + external_id: form.subscription_id.trim(), + binding_scope: `azure:${form.subscription_id.trim()}`, metadata: { - provider: 'azure', + subscription_id: form.subscription_id.trim(), tenant_id: form.tenant_id.trim(), client_id: form.client_id.trim(), - // NOTE: client_secret is NOT included here. We rely on the - // server side to mint a secret_ref via Secret Broker when - // vault is online; until then the binding is created with - // empty secret_ref and a "needs vault" status. Phase 2 - // wires up the actual cred-write step. }, - permission_scope: ['azure:read'], + permission_scope: { actions: ['azure:read'] }, constraints: {}, - status: vaultConfigured ? 'active' : 'pending', - }), - onSuccess: () => { + status: 'pending', + }) + const secret = await putManagerResourceSecret(pending.id, { + subscription_id: form.subscription_id.trim(), + tenant_id: form.tenant_id.trim(), + client_id: form.client_id.trim(), + client_secret: form.client_secret, + }) + const account = await updateManagerResource(pending.id, { + resource_type: 'cloud_account', + name: pending.name, + provider: pending.provider, + external_id: pending.external_id, + binding_scope: pending.binding_scope, + secret_ref: secret.secret_ref, + metadata: pending.metadata, + permission_scope: pending.permission_scope, + constraints: pending.constraints, + status: 'active', + }) + try { + const discovery = await discoverManagerAzureResources(account.id) + return { account, discovery, discoveryError: '' } + } catch (err) { + return { + account, + discovery: null, + discoveryError: + err instanceof Error ? err.message : t('Azure discovery failed'), + } + } + }, + onSuccess: (result) => { void qc.invalidateQueries({ queryKey: ['heicode', 'resources'] }) + void qc.invalidateQueries({ queryKey: ['manager', 'cloud-resources'] }) toast.success(t('Azure subscription bound')) + if (result.discoveryError) { + toast.warning(result.discoveryError) + } else if (result.discovery) { + toast.success( + t('{{n}} Azure resources discovered', { + n: result.discovery.discovered, + }) + ) + } onOpenChange(false) setForm({ name: '', @@ -86,7 +130,9 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }: }) }, onError: (err) => { - toast.error(err instanceof Error ? err.message : t('Failed to bind Azure account')) + toast.error( + err instanceof Error ? err.message : t('Failed to bind Azure account') + ) }, }) @@ -95,19 +141,20 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }: form.subscription_id.trim() !== '' && form.tenant_id.trim() !== '' && form.client_id.trim() !== '' && - form.client_secret.trim() !== '' + form.client_secret.trim() !== '' && + Boolean(vaultConfigured) return ( - + - + {t('Connect Azure subscription')} {t( - 'Paste a Service Principal that has at least Reader role on the subscription. Heicode uses it to enumerate cloud resources (VMs, databases, AKS, storage) once auto-discovery lands. AWS and GCP are coming soon.' + 'Paste a Service Principal that has at least Reader role on the subscription. Heicode uses it to enumerate cloud resources (VMs, databases, AKS, storage). AWS and GCP are coming soon.' )} @@ -118,9 +165,9 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }: {t('Secret vault not yet configured')}

-

+

{t( - 'OpenBao is not wired up to the Manager yet (see System settings → Secret vault status). The binding will be created in pending state — the client_secret will NOT be persisted server-side until vault is configured. Re-enter or rotate the secret once vault is online.' + 'Azure Key Vault is not ready yet (see System settings → Secret vault status). The subscription cannot be bound until Manager can write the client_secret to Key Vault.' )}

@@ -165,7 +212,9 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
- +
-
-

- +

+

+ {t('What Heicode will and will not do')}

  • · {t('Read-only ARM access (Reader role recommended)')}
  • -
  • · {t('Auto-discover VM / database / AKS / storage — coming soon')}
  • -
  • · {t('Never modify resources without explicit approval from the desktop client')}
  • -
  • · {t('Never write the client_secret into Manager logs or audit payloads')}
  • +
  • + ·{' '} + {t('Auto-discover VM / database / AKS / storage')} +
  • +
  • + ·{' '} + {t( + 'Never modify resources without explicit approval from the desktop client' + )} +
  • +
  • + ·{' '} + {t( + 'Never write the client_secret into Manager logs or audit payloads' + )} +
diff --git a/heicode/web/default/src/features/agnet-console/pages.tsx b/heicode/web/default/src/features/agnet-console/pages.tsx index 1255469..5cf4c20 100644 --- a/heicode/web/default/src/features/agnet-console/pages.tsx +++ b/heicode/web/default/src/features/agnet-console/pages.tsx @@ -5,17 +5,13 @@ import { Activity, AlertOctagon, ArrowUpRight, - Bot, Building2, - Calendar, CheckCircle2, CircleDashed, Coins, FileSearch, Filter, - GitBranch, GitCommit, - Hash, PlayCircle, Plus, Rocket, @@ -23,31 +19,11 @@ import { ShieldCheck, Tag, Trash2, - User2, XCircle, } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { cn } from '@/lib/utils' -import { Input } from '@/components/ui/input' -import { Button } from '@/components/ui/button' -import { Skeleton } from '@/components/ui/skeleton' -import { QueryState } from '@/components/query-state' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { - getAgnetDeploymentEvents, - getAgnetSnapshots, - listAgnetDeployments, - listAgnetDeploymentsQuiet, - type AgnetDeployment, - type AgnetRuntimeExecution, - type AgnetSKAccessPolicy, -} from './api' +import { toast } from 'sonner' +import { api } from '@/lib/api' // /audit pulls from mcp-server §5.10 stub now, not the Heicode-local // controller — the contract doc names that endpoint as the canonical // source. The shape of McpAuditEntry is wider than the legacy local one @@ -62,8 +38,17 @@ import { type ResourceBinding, type ResourceType, } from '@/lib/heicode-mcp' -import { AzureCloudBindingSheet } from './azure-cloud-binding-sheet' -import { CreateAgnetDeploymentSheet } from './create-agnet-deployment-sheet' +import { listManagerResources } from '@/lib/manager-resources' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' import { Sheet, SheetContent, @@ -71,10 +56,25 @@ import { SheetHeader, SheetTitle, } from '@/components/ui/sheet' -import { toast } from 'sonner' +import { Skeleton } from '@/components/ui/skeleton' +import { QueryState } from '@/components/query-state' +import { + getAgnetDeploymentEvents, + listAgnetDeployments, + type AgnetDeployment, + type AgnetRuntimeExecution, + type AgnetSKAccessPolicy, +} from './api' +import { AzureCloudBindingSheet } from './azure-cloud-binding-sheet' +import { CreateAgnetDeploymentSheet } from './create-agnet-deployment-sheet' type StatusKey = 'running' | 'success' | 'failed' | 'pending' +type SecretStoreStatus = { + configured: boolean + reachable: boolean +} + const STATUS_TO_KEY: Record = { running: 'running', active: 'running', @@ -118,7 +118,7 @@ function StatusBadge({ phase }: { phase: string }) { return ( @@ -138,12 +138,12 @@ function PageSurface(props: {
-

+

Heicode Manager

{props.title}

{props.subtitle && ( -

+

{props.subtitle}

)} @@ -162,10 +162,10 @@ function PageSurface(props: { function EmptySurface(props: { title: string; hint?: string }) { return (
- +

{props.title}

{props.hint && ( -

{props.hint}

+

{props.hint}

)}
) @@ -197,10 +197,10 @@ function MetaPill({ value: string }) { return ( - - + + {label} - + {value} @@ -253,7 +253,9 @@ function describeScope(dep: AgnetDeployment): string { ) } -function collectResourceGrants(dep: AgnetDeployment): Record[] { +function collectResourceGrants( + dep: AgnetDeployment +): Record[] { return ( dep.orchestration_plan?.agents ?.flatMap((agent) => agent.resource_grants || []) @@ -290,7 +292,7 @@ function formatRelativeTime(value: string | undefined): string { // maskSecretRef shows enough of a secret_ref to identify which vault // entry it points at, but NEVER the full value. Product docs §10 // "高级展开" + §13.9 forbid leaking plaintext credentials anywhere in -// the UI. Accepted shapes are vault://path/to/secret or opaque IDs. +// the UI. Accepted shapes are azkv://... Key Vault refs or opaque IDs. // We keep the scheme and the first 6 chars of the leaf, then ellipsis. function maskSecretRef(ref: string | undefined): string { if (!ref) return '—' @@ -338,13 +340,13 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
-

+

{t('Run detail')}

-

+

{dep.deployment_id}

-

+

{dep.orchestration_plan?.objective || dep.orchestration_plan?.template_hint || t('No objective')} @@ -356,7 +358,11 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {

- +
-
+
-

- +

+ {t('Permission manifest')}

-

+

{t('Run manifest hint')}

-

+

{grants.length} {t('resource grants')}

@@ -405,18 +411,14 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) { - + - + @@ -429,8 +431,13 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) { : [] const constraints = (g.constraints as Record) || {} - const constraintEntries = Object.entries(constraints).slice(0, 6) - const secret = maskSecretRef(g.secret_ref as string | undefined) + const constraintEntries = Object.entries(constraints).slice( + 0, + 6 + ) + const secret = maskSecretRef( + g.secret_ref as string | undefined + ) const status = String(g.status || 'active') return ( @@ -455,7 +462,7 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) { {actions.map((a) => ( {a} @@ -471,10 +478,11 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) { {constraintEntries.map(([k, v]) => ( - {k}={String(v).length > 24 + {k}= + {String(v).length > 24 ? String(v).slice(0, 24) + '…' : v} @@ -482,7 +490,7 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) { )} -
- {t('Resource')} - {t('Resource')} {t('Allowed actions')} {t('Constraints')} - {t('secret_ref')} - {t('secret_ref')} {t('Status')}
- + {resType} -

+

{resId}

+ {secret} @@ -500,7 +508,7 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) { })}
-

+

{t( 'Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.' )} @@ -509,21 +517,21 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) { )}

-
-

- +

+

+ {t('Events usage')}

-

+

{t('Run events hint')}

-
-

- +

+

+ {t('Audit usage')}

-

+

{t('Run audit hint')}

@@ -598,7 +606,7 @@ export function AgnetDeploymentsPage() { {t('New run')}
- + setKeyword(e.target.value)} @@ -632,7 +640,9 @@ export function AgnetDeploymentsPage() { retry={() => void refetchDeployments()} loadingFallback={} emptyTitle={t('No runs match the current filter')} - emptyDescription={t('Adjust filters or trigger a new orchestration plan.')} + emptyDescription={t( + 'Adjust filters or trigger a new orchestration plan.' + )} >
{filtered.map((dep) => { @@ -656,7 +666,7 @@ export function AgnetDeploymentsPage() { } }} className={cn( - 'group flex cursor-pointer flex-col gap-3 rounded-2xl border bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 transition hover:border-primary/45 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', + 'group hover:border-primary/45 focus-visible:ring-ring flex cursor-pointer flex-col gap-3 rounded-2xl border bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 transition focus-visible:ring-2 focus-visible:outline-none', selected ? 'border-primary/55' : 'border-[color-mix(in_oklch,var(--primary)_18%,var(--border))]' @@ -670,14 +680,14 @@ export function AgnetDeploymentsPage() {