feat: add azure key vault resource discovery
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -143,5 +143,5 @@ Heicode 今日工作总结与后续计划
|
||||
六、安全说明
|
||||
|
||||
- 本次文档和代码未写入任何真实密码、Token、Redis 密钥、PostgreSQL 密码、SSH 密码或 Git 密码。
|
||||
- 文档示例全部使用占位符或 vault:// secret_ref。
|
||||
- 文档示例使用占位符或 Azure Key Vault `azkv://...` secret_ref。
|
||||
- 后续部署与推送仍需继续遵守:密钥只走环境变量、Secret Store、交互式凭据或平台安全注入,不进入 Git。
|
||||
|
||||
+1
-1
@@ -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 生产部署前的安全守卫、环境变量注入和验证计划 |
|
||||
|
||||
@@ -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=<azure-redis-connection-string>
|
||||
SESSION_SECRET=<generated-session-secret>
|
||||
HEICODE_ROOT_EMAILS=<comma-separated-root-emails-if-needed>
|
||||
HEICODE_ADMIN_EMAILS=<comma-separated-admin-emails-if-needed>
|
||||
AZURE_KEY_VAULT_URL=<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`。 |
|
||||
|
||||
@@ -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 平台。
|
||||
|
||||
+4
-4
@@ -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 边界
|
||||
|
||||
|
||||
@@ -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`,不得发起额外批准流程。
|
||||
|
||||
|
||||
+3
-3
@@ -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 注入密钥保管器派生的短期、最小权限凭证。
|
||||
|
||||
验收:
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ Heicode 是一款覆盖软件生命周期的智能开发 Code 工具。它面向
|
||||
| Heicode Manager / 浏览器控制台 | 辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态总览、模型余额与用量、审计与日志查看 |
|
||||
| Agnet 平台 | 执行层,负责部署和运行子 Agnet,在任务推进过程中完成需求、开发、测试、修复、交付与部署,并回传日志、状态、事件和指标 |
|
||||
| CodeGW | 内部模型网关与计费服务,普通用户不直接进入后台 |
|
||||
| 密钥保管器 | OpenBao 实现,保存长期凭证,按审批和权限提供短期凭证租约 |
|
||||
| 密钥保管器 | Azure Key Vault 实现,保存长期凭证引用,按审批和权限提供短期访问能力 |
|
||||
|
||||
说明:Heicode 客户端是用户主体验,用户不在网页上编码。Heicode Manager 是浏览器里的辅助控制台,承担资源、部署、状态、余额、审计和下载等辅助操作。真正持续推进任务的是 Heicode 调度下的 Agnet 执行闭环,Agnet 在过程中还可以调用已授权的 SK 工具和外部能力,最终完成交付与部署并把结果回传到客户端。
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
| 客户端 | 主对话体验、任务推进和高危审批 |
|
||||
| Agnet 平台 | AKS 执行层 |
|
||||
| CodeGW | 模型网关与用量 |
|
||||
| OpenBao | 密钥保管器 |
|
||||
| Azure Key Vault | 密钥保管器 |
|
||||
|
||||
补充说明:
|
||||
|
||||
@@ -170,4 +170,4 @@
|
||||
- 信息密度适中,避免空泛大词。
|
||||
- 主色与 Heicode 和客户端保持一致。
|
||||
- 图片优先使用真实界面和流程,不使用无意义装饰图。
|
||||
- 首页文案避免出现 CodeGW 后台、OpenBao 控制台、模型提供方配置等内部概念。
|
||||
- 首页文案避免出现 CodeGW 后台、Azure Key Vault 控制台、模型提供方配置等内部概念。
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
https://code.xinghanlab.com
|
||||
```
|
||||
|
||||
用户只需要登录 Heicode。普通用户不需要登录 CodeGW 后台,也不需要登录 OpenBao。
|
||||
用户只需要登录 Heicode。普通用户不需要登录 CodeGW 后台,也不需要登录 Azure Key Vault。
|
||||
|
||||
## 第一步:登录 Heicode
|
||||
|
||||
|
||||
@@ -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 模型。
|
||||
|
||||
|
||||
@@ -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 仅通过受控后端身份访问 |
|
||||
|
||||
@@ -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`,不收长期明文密钥。
|
||||
|
||||
@@ -104,7 +104,7 @@ macOS 注意:
|
||||
2. 旧登录入口。
|
||||
3. 第三方路由登录入口。
|
||||
4. CodeGW 后台入口。
|
||||
5. OpenBao 地址或 token。
|
||||
5. Azure Key Vault 地址、access token 或密钥保管器后台信息。
|
||||
6. 长期云密钥输入框。
|
||||
7. 无法解释的个人 API key 自动复制。
|
||||
|
||||
|
||||
@@ -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 和客户端完成整个开发流程。
|
||||
|
||||
@@ -38,7 +38,7 @@ Heicode 不以后台菜单作为主体验。建议使用顶部轻量入口和主
|
||||
- CodeGW 后台。
|
||||
- 渠道管理。
|
||||
- 模型供应商配置。
|
||||
- OpenBao 控制台。
|
||||
- Azure Key Vault 控制台。
|
||||
- 模型提供方选择。
|
||||
- 旧服务入口。
|
||||
|
||||
@@ -322,7 +322,7 @@ Heicode 只追问影响执行路径的少量问题:
|
||||
## 文案规则
|
||||
|
||||
- 用“任务上下文”或“资源上下文”,不要把主流程叫成资源管理。
|
||||
- 用“密钥保管器”,不用“OpenBao”作为普通用户标题。
|
||||
- 用“密钥保管器”,不要用具体实现名作为普通用户标题。
|
||||
- 用“客户端”,不用“本地模型提供方”。
|
||||
- 用“模型与余额”,不用“CodeGW 后台”。
|
||||
- 用“执行计划”,不用“控制面草案”。
|
||||
|
||||
@@ -334,7 +334,7 @@ Heicode 会复用之前的项目上下文、资源绑定和 Agnet 角色,生
|
||||
## 口述时不要说
|
||||
|
||||
1. 让用户去 CodeGW 后台配置模型。
|
||||
2. 让用户去 OpenBao 控制台管理密钥。
|
||||
2. 让用户去 Azure Key Vault 控制台管理密钥。
|
||||
3. 让客户端选择模型提供方。
|
||||
4. 让用户手写复杂 manifest。
|
||||
5. 把 tenant/project 当作当前产品主轴。
|
||||
|
||||
@@ -256,7 +256,9 @@ Heicode 通过服务端调用 CodeGW,并把普通用户需要的信息展示
|
||||
|
||||
### 5.5 密钥保管器
|
||||
|
||||
密钥保管器用于保存长期凭证。当前技术实现是 OpenBao。
|
||||
密钥保管器用于保存长期凭证。当前技术实现是 Azure Key Vault。
|
||||
|
||||
生产环境中 Manager 使用 VM Managed Identity 访问 Azure Key Vault。用户绑定资源后,Manager 将敏感凭证写入 Key Vault,并在 Heicode 数据库中只保存 `azkv://<vault>/secrets/<name>` 形式的 `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 管理员配置。
|
||||
|
||||
## 十一、常见问题
|
||||
|
||||
@@ -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 是内部模型网关和计费服务,不作为普通用户产品入口。
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.4.2
|
||||
1.4.3
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
+192
-112
@@ -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
|
||||
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,
|
||||
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://<vault>/secrets/<name>")
|
||||
}
|
||||
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://<vault>/secrets/<name>")
|
||||
}
|
||||
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"`
|
||||
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(),
|
||||
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})
|
||||
}
|
||||
|
||||
@@ -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-name>.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:
|
||||
|
||||
@@ -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) // 增加缓冲区避免阻塞
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Heicode Manager classic placeholder</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+3
@@ -9,6 +9,9 @@ lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
!dist/
|
||||
dist/*
|
||||
!dist/index.html
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
|
||||
Vendored
+107
-57
@@ -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=="],
|
||||
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Heicode Manager placeholder</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+111
-49
@@ -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://<TBD> (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://<vault>/secrets/<name>
|
||||
// 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(),
|
||||
metadata: {
|
||||
provider: 'azure',
|
||||
external_id: form.subscription_id.trim(),
|
||||
binding_scope: `azure:${form.subscription_id.trim()}`,
|
||||
metadata: {
|
||||
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 (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent className='w-[min(560px,96vw)] sm:max-w-none overflow-y-auto'>
|
||||
<SheetContent className='w-[min(560px,96vw)] overflow-y-auto sm:max-w-none'>
|
||||
<SheetHeader>
|
||||
<SheetTitle className='flex items-center gap-2'>
|
||||
<Cloud className='h-5 w-5 text-primary' />
|
||||
<Cloud className='text-primary h-5 w-5' />
|
||||
{t('Connect Azure subscription')}
|
||||
</SheetTitle>
|
||||
<SheetDescription>
|
||||
{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.'
|
||||
)}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
@@ -118,9 +165,9 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
|
||||
<AlertTriangle className='h-3.5 w-3.5' />
|
||||
{t('Secret vault not yet configured')}
|
||||
</p>
|
||||
<p className='mt-1.5 text-muted-foreground'>
|
||||
<p className='text-muted-foreground mt-1.5'>
|
||||
{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.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -165,7 +212,9 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
|
||||
</div>
|
||||
<div className='grid gap-3 sm:grid-cols-2'>
|
||||
<div>
|
||||
<Label htmlFor='az-client'>{t('Client ID (Application ID)')}</Label>
|
||||
<Label htmlFor='az-client'>
|
||||
{t('Client ID (Application ID)')}
|
||||
</Label>
|
||||
<Input
|
||||
id='az-client'
|
||||
value={form.client_id}
|
||||
@@ -191,16 +240,29 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-2 rounded-lg border border-dashed bg-card/40 p-3 text-xs text-muted-foreground'>
|
||||
<p className='flex items-center gap-1.5 font-medium text-foreground'>
|
||||
<ShieldCheck className='h-3.5 w-3.5 text-primary' />
|
||||
<div className='bg-card/40 text-muted-foreground mt-2 rounded-lg border border-dashed p-3 text-xs'>
|
||||
<p className='text-foreground flex items-center gap-1.5 font-medium'>
|
||||
<ShieldCheck className='text-primary h-3.5 w-3.5' />
|
||||
{t('What Heicode will and will not do')}
|
||||
</p>
|
||||
<ul className='mt-2 space-y-1'>
|
||||
<li>· {t('Read-only ARM access (Reader role recommended)')}</li>
|
||||
<li>· {t('Auto-discover VM / database / AKS / storage — coming soon')}</li>
|
||||
<li>· {t('Never modify resources without explicit approval from the desktop client')}</li>
|
||||
<li>· {t('Never write the client_secret into Manager logs or audit payloads')}</li>
|
||||
<li>
|
||||
·{' '}
|
||||
{t('Auto-discover VM / database / AKS / storage')}
|
||||
</li>
|
||||
<li>
|
||||
·{' '}
|
||||
{t(
|
||||
'Never modify resources without explicit approval from the desktop client'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
·{' '}
|
||||
{t(
|
||||
'Never write the client_secret into Manager logs or audit payloads'
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+268
-187
File diff suppressed because it is too large
Load Diff
@@ -259,6 +259,11 @@ export function SignUpForm({
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<p className='text-muted-foreground mt-1.5 text-xs leading-relaxed'>
|
||||
{t(
|
||||
'We recommend using a mainland China email address so verification emails arrive more reliably.'
|
||||
)}
|
||||
</p>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -18,9 +18,8 @@
|
||||
* 还没接,等接通后这个 stash 流程换成 POST /api/task。
|
||||
*/
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { Link, useNavigate } from '@tanstack/react-router'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, useNavigate } from '@tanstack/react-router'
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
@@ -36,15 +35,16 @@ import {
|
||||
UserCog,
|
||||
XCircle,
|
||||
} from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast as sonnerToast } from 'sonner'
|
||||
import {
|
||||
createTaskFromIntent,
|
||||
listHeicodeTasks,
|
||||
type HeicodeTask,
|
||||
} from '@/lib/heicode-mcp'
|
||||
import { toast as sonnerToast } from 'sonner'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
const DRAFT_STORAGE_KEY = 'heicode_idea_draft'
|
||||
|
||||
@@ -93,7 +93,7 @@ function StatusBadge({ phase }: { phase: string }) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.12em] ring-1 ring-inset',
|
||||
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-[0.12em] uppercase ring-1 ring-inset',
|
||||
p.cls
|
||||
)}
|
||||
>
|
||||
@@ -144,10 +144,14 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const task = await createTaskFromIntent(trimmed)
|
||||
sonnerToast.success(t('Task drafted. Answer the follow-ups to build the recommendation.'))
|
||||
sonnerToast.success(
|
||||
t('Task drafted. Answer the follow-ups to build the recommendation.')
|
||||
)
|
||||
void navigate({ to: '/tasks/$id', params: { id: task.id } })
|
||||
} catch (err) {
|
||||
sonnerToast.error(err instanceof Error ? err.message : t('Failed to create task'))
|
||||
sonnerToast.error(
|
||||
err instanceof Error ? err.message : t('Failed to create task')
|
||||
)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@@ -183,7 +187,7 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
|
||||
>
|
||||
{t('What idea would you like to turn into shippable software?')}
|
||||
</h2>
|
||||
<p className='mt-2 text-sm text-muted-foreground'>
|
||||
<p className='text-muted-foreground mt-2 text-sm'>
|
||||
{t(
|
||||
'Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agnet team.'
|
||||
)}
|
||||
@@ -200,7 +204,7 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
|
||||
placeholder={t(
|
||||
'e.g. A task-management SaaS for small teams: login, projects, tasks, comments, notifications, deploy to Azure.'
|
||||
)}
|
||||
className='w-full resize-none rounded-2xl border px-4 py-3 text-sm leading-relaxed shadow-[inset_0_1px_0_rgba(123,107,227,0.10)] outline-none transition focus:ring-2 focus:ring-[color-mix(in_oklch,var(--primary)_40%,transparent)]'
|
||||
className='w-full resize-none rounded-2xl border px-4 py-3 text-sm leading-relaxed shadow-[inset_0_1px_0_rgba(123,107,227,0.10)] transition outline-none focus:ring-2 focus:ring-[color-mix(in_oklch,var(--primary)_40%,transparent)]'
|
||||
style={{
|
||||
borderColor: 'rgba(123,107,227,0.22)',
|
||||
backgroundColor: 'rgba(255,255,255,0.04)',
|
||||
@@ -214,7 +218,7 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
|
||||
/>
|
||||
|
||||
<div className='flex flex-wrap items-center justify-between gap-3'>
|
||||
<p className='text-xs text-muted-foreground'>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t(
|
||||
'Heicode Manager only captures the idea. The main task conversation happens in the desktop client.'
|
||||
)}
|
||||
@@ -222,6 +226,7 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
|
||||
<Button
|
||||
type='button'
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
className='h-10 gap-2 rounded-xl px-5 text-sm font-semibold text-white shadow-[0_18px_48px_-18px_rgba(123,107,227,0.65)] transition-transform hover:translate-y-[-1px] active:translate-y-0 active:scale-[0.99]'
|
||||
style={{
|
||||
backgroundImage: 'var(--gradient-brand-btn)',
|
||||
@@ -229,7 +234,7 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
|
||||
}}
|
||||
>
|
||||
<Sparkles className='h-4 w-4' />
|
||||
{t('Save idea')}
|
||||
{submitting ? t('Saving') : t('Save idea')}
|
||||
<span className='ms-1 hidden text-[10px] font-medium opacity-80 sm:inline'>
|
||||
{t('⌘/Ctrl + Enter')}
|
||||
</span>
|
||||
@@ -250,7 +255,9 @@ function ContinueTasks({
|
||||
t: ReturnType<typeof useTranslation>['t']
|
||||
}) {
|
||||
const recent = useMemo(() => {
|
||||
const sorted = [...tasks].sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0))
|
||||
const sorted = [...tasks].sort(
|
||||
(a, b) => (b.updated_at || 0) - (a.updated_at || 0)
|
||||
)
|
||||
return sorted.slice(0, 4)
|
||||
}, [tasks])
|
||||
|
||||
@@ -258,7 +265,7 @@ function ContinueTasks({
|
||||
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_60%,transparent)] p-5'>
|
||||
<header className='mb-4 flex items-center justify-between'>
|
||||
<div>
|
||||
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
||||
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.16em] uppercase'>
|
||||
{t('Continue working')}
|
||||
</p>
|
||||
<h3 className='mt-0.5 text-base font-semibold'>
|
||||
@@ -269,7 +276,7 @@ function ContinueTasks({
|
||||
asChild
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='gap-1 text-primary'
|
||||
className='text-primary gap-1'
|
||||
>
|
||||
<Link to='/deployments'>
|
||||
{t('All tasks')}
|
||||
@@ -284,7 +291,7 @@ function ContinueTasks({
|
||||
))}
|
||||
</div>
|
||||
) : recent.length === 0 ? (
|
||||
<p className='rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-background/40 p-4 text-center text-xs text-muted-foreground'>
|
||||
<p className='bg-background/40 text-muted-foreground rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-4 text-center text-xs'>
|
||||
{t(
|
||||
'No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agnet.'
|
||||
)}
|
||||
@@ -296,17 +303,17 @@ function ContinueTasks({
|
||||
<Link
|
||||
to='/tasks/$id'
|
||||
params={{ id: task.id }}
|
||||
className='flex items-start justify-between gap-3 rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-3 transition hover:-translate-y-px hover:border-primary/40 hover:shadow-[0_18px_48px_-32px_rgba(123,107,227,0.4)]'
|
||||
className='bg-background/40 hover:border-primary/40 flex items-start justify-between gap-3 rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] p-3 transition hover:-translate-y-px hover:shadow-[0_18px_48px_-32px_rgba(123,107,227,0.4)]'
|
||||
>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<p className='line-clamp-1 text-sm font-medium'>
|
||||
{task.name || task.intent || t('Untitled task')}
|
||||
</p>
|
||||
<p className='mt-1 text-[11px] text-muted-foreground'>
|
||||
<p className='text-muted-foreground mt-1 text-[11px]'>
|
||||
{task.status_caption || task.status}
|
||||
{' · '}
|
||||
{t('Updated')}{' '}
|
||||
{formatRelativeTimeMs(task.updated_at)} {t('ago')}
|
||||
{t('Updated')} {formatRelativeTimeMs(task.updated_at)}{' '}
|
||||
{t('ago')}
|
||||
</p>
|
||||
</div>
|
||||
<StatusBadge phase={task.status} />
|
||||
@@ -329,14 +336,18 @@ function TodayFocus({
|
||||
t: ReturnType<typeof useTranslation>['t']
|
||||
}) {
|
||||
const buckets = useMemo(() => {
|
||||
const failed = tasks.filter((task) => classifyStatus(task.status) === 'failed')
|
||||
const failed = tasks.filter(
|
||||
(task) => classifyStatus(task.status) === 'failed'
|
||||
)
|
||||
// "Pending confirmation" maps to HeicodeTask.status = 'configuring'
|
||||
// (followups not all answered yet) AND tasks awaiting_approval.
|
||||
const pending = tasks.filter(
|
||||
(task) =>
|
||||
task.status === 'configuring' || task.status === 'awaiting_approval'
|
||||
)
|
||||
const running = tasks.filter((task) => classifyStatus(task.status) === 'running')
|
||||
const running = tasks.filter(
|
||||
(task) => classifyStatus(task.status) === 'running'
|
||||
)
|
||||
return { failed, pending, running }
|
||||
}, [tasks])
|
||||
|
||||
@@ -373,14 +384,15 @@ function TodayFocus({
|
||||
const toneCls: Record<'failed' | 'pending' | 'running' | 'idle', string> = {
|
||||
failed: 'text-rose-400 bg-rose-500/10 ring-rose-500/25',
|
||||
pending: 'text-amber-400 bg-amber-500/10 ring-amber-500/25',
|
||||
running: 'text-primary bg-[color-mix(in_oklch,var(--primary)_18%,transparent)] ring-primary/30',
|
||||
running:
|
||||
'text-primary bg-[color-mix(in_oklch,var(--primary)_18%,transparent)] ring-primary/30',
|
||||
idle: 'text-muted-foreground bg-muted/40 ring-border',
|
||||
}
|
||||
|
||||
return (
|
||||
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_60%,transparent)] p-5'>
|
||||
<header className='mb-4'>
|
||||
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
||||
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.16em] uppercase'>
|
||||
{t("Today's focus")}
|
||||
</p>
|
||||
<h3 className='mt-0.5 text-base font-semibold'>
|
||||
@@ -398,7 +410,7 @@ function TodayFocus({
|
||||
{focusItems.map((it) => (
|
||||
<div
|
||||
key={it.title}
|
||||
className='rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-3'
|
||||
className='bg-background/40 rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] p-3'
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span
|
||||
@@ -409,12 +421,12 @@ function TodayFocus({
|
||||
>
|
||||
<it.Icon className='h-3.5 w-3.5' />
|
||||
</span>
|
||||
<p className='text-xs font-medium text-muted-foreground'>
|
||||
<p className='text-muted-foreground text-xs font-medium'>
|
||||
{it.title}
|
||||
</p>
|
||||
</div>
|
||||
<p className='mt-2 text-2xl font-semibold'>{it.count}</p>
|
||||
<p className='mt-0.5 text-[11px] text-muted-foreground'>
|
||||
<p className='text-muted-foreground mt-0.5 text-[11px]'>
|
||||
{it.hint}
|
||||
</p>
|
||||
</div>
|
||||
@@ -425,11 +437,7 @@ function TodayFocus({
|
||||
)
|
||||
}
|
||||
|
||||
function HelperEntries({
|
||||
t,
|
||||
}: {
|
||||
t: ReturnType<typeof useTranslation>['t']
|
||||
}) {
|
||||
function HelperEntries({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
|
||||
const entries: Array<{
|
||||
Icon: typeof GitBranch
|
||||
title: string
|
||||
@@ -477,7 +485,7 @@ function HelperEntries({
|
||||
return (
|
||||
<section>
|
||||
<header className='mb-3'>
|
||||
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
||||
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.16em] uppercase'>
|
||||
{t('Helpers')}
|
||||
</p>
|
||||
<h3 className='mt-0.5 text-base font-semibold'>
|
||||
@@ -489,10 +497,10 @@ function HelperEntries({
|
||||
<Link
|
||||
key={e.to}
|
||||
to={e.to}
|
||||
className='group flex items-start gap-3 rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 transition hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-[0_18px_48px_-32px_rgba(123,107,227,0.55)]'
|
||||
className='group hover:border-primary/40 flex items-start gap-3 rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 transition hover:-translate-y-0.5 hover:shadow-[0_18px_48px_-32px_rgba(123,107,227,0.55)]'
|
||||
>
|
||||
<span
|
||||
className='inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-primary'
|
||||
className='text-primary inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl'
|
||||
style={{
|
||||
backgroundColor: 'rgba(123,107,227,0.12)',
|
||||
border: '1px solid rgba(123,107,227,0.22)',
|
||||
@@ -505,7 +513,7 @@ function HelperEntries({
|
||||
{e.title}
|
||||
<ArrowRight className='h-3.5 w-3.5 -translate-x-0.5 opacity-0 transition group-hover:translate-x-0 group-hover:opacity-70' />
|
||||
</p>
|
||||
<p className='mt-0.5 text-xs text-muted-foreground'>{e.desc}</p>
|
||||
<p className='text-muted-foreground mt-0.5 text-xs'>{e.desc}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
+22
-50
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CheckCircle2, AlertTriangle, RefreshCw, ShieldOff } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { api } from '@/lib/api'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { SettingsSection } from '../components/settings-section'
|
||||
|
||||
// M7 — admin-only secret-store (OpenBao / Vault) status snapshot.
|
||||
// Secret-store (Azure Key Vault) status snapshot.
|
||||
// Pure read; deliberately exposes only:
|
||||
// - configuration state (env vars set?)
|
||||
// - reachability
|
||||
@@ -21,10 +21,8 @@ import { SettingsSection } from '../components/settings-section'
|
||||
type StoreStatus = {
|
||||
configured: boolean
|
||||
reachable: boolean
|
||||
initialized?: boolean
|
||||
sealed?: boolean
|
||||
standby?: boolean
|
||||
version?: string
|
||||
provider?: string
|
||||
auth_method?: string
|
||||
message?: string
|
||||
checked_at: number
|
||||
}
|
||||
@@ -73,9 +71,9 @@ export function SecretStoreSection() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// Pick the colour-coded summary pill based on the three operational
|
||||
// states the backend can return. Order matters: not-configured beats
|
||||
// reachability, and sealed beats reachable-but-otherwise-fine.
|
||||
// Pick the colour-coded summary pill based on the operational states
|
||||
// the backend can return. Order matters: not-configured beats
|
||||
// reachability.
|
||||
let toneClass = 'bg-muted/40 text-muted-foreground ring-border/60'
|
||||
let toneIcon = ShieldOff
|
||||
let toneLabel = t('Unknown')
|
||||
@@ -88,14 +86,6 @@ export function SecretStoreSection() {
|
||||
toneClass = 'bg-rose-500/15 text-rose-400 ring-rose-500/30'
|
||||
toneIcon = AlertTriangle
|
||||
toneLabel = t('Unreachable')
|
||||
} else if (status.sealed) {
|
||||
toneClass = 'bg-amber-500/15 text-amber-400 ring-amber-500/30'
|
||||
toneIcon = AlertTriangle
|
||||
toneLabel = t('Sealed')
|
||||
} else if (!status.initialized) {
|
||||
toneClass = 'bg-amber-500/15 text-amber-400 ring-amber-500/30'
|
||||
toneIcon = AlertTriangle
|
||||
toneLabel = t('Not initialized')
|
||||
} else {
|
||||
toneClass = 'bg-emerald-500/15 text-emerald-400 ring-emerald-500/30'
|
||||
toneIcon = CheckCircle2
|
||||
@@ -108,10 +98,10 @@ export function SecretStoreSection() {
|
||||
<SettingsSection
|
||||
title={t('Secret vault status')}
|
||||
description={t(
|
||||
'Heicode stores long-lived credentials in OpenBao / Vault. This panel surfaces health and seal state for the operator — secret names and values are NEVER displayed here.'
|
||||
'Heicode stores long-lived credentials in Azure Key Vault. This panel surfaces provider, managed identity and reachability status — secret names and values are NEVER displayed here.'
|
||||
)}
|
||||
>
|
||||
<div className='rounded-lg border bg-card/40 p-4'>
|
||||
<div className='bg-card/40 rounded-lg border p-4'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<span
|
||||
className={cn(
|
||||
@@ -130,7 +120,7 @@ export function SecretStoreSection() {
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn('h-3.5 w-3.5 me-1', loading && 'animate-spin')}
|
||||
className={cn('me-1 h-3.5 w-3.5', loading && 'animate-spin')}
|
||||
/>
|
||||
{t('Refresh')}
|
||||
</Button>
|
||||
@@ -144,36 +134,18 @@ export function SecretStoreSection() {
|
||||
|
||||
{status && (
|
||||
<div className='mt-4 grid gap-3 sm:grid-cols-2'>
|
||||
<Field label={t('Configured')} value={status.configured ? 'Yes' : 'No'} />
|
||||
<Field
|
||||
label={t('Configured')}
|
||||
value={status.configured ? 'Yes' : 'No'}
|
||||
/>
|
||||
<Field label={t('Provider')} value={status.provider || '—'} />
|
||||
<Field label={t('Auth method')} value={status.auth_method || '—'} />
|
||||
<Field
|
||||
label={t('Reachable')}
|
||||
value={
|
||||
status.configured ? (status.reachable ? 'Yes' : 'No') : '—'
|
||||
}
|
||||
/>
|
||||
<Field
|
||||
label={t('Initialized')}
|
||||
value={
|
||||
status.reachable
|
||||
? status.initialized
|
||||
? 'Yes'
|
||||
: 'No'
|
||||
: '—'
|
||||
}
|
||||
/>
|
||||
<Field
|
||||
label={t('Sealed')}
|
||||
value={
|
||||
status.reachable ? (status.sealed ? 'Yes' : 'No') : '—'
|
||||
}
|
||||
/>
|
||||
<Field
|
||||
label={t('Standby')}
|
||||
value={
|
||||
status.reachable ? (status.standby ? 'Yes' : 'No') : '—'
|
||||
}
|
||||
/>
|
||||
<Field label={t('Version')} value={status.version || '—'} />
|
||||
<Field
|
||||
label={t('Last checked')}
|
||||
value={formatTs(status.checked_at)}
|
||||
@@ -182,15 +154,15 @@ export function SecretStoreSection() {
|
||||
)}
|
||||
|
||||
{status?.message && (
|
||||
<p className='mt-3 rounded-md border border-dashed border-border/60 bg-background/40 p-2 text-xs text-muted-foreground'>
|
||||
<p className='border-border/60 bg-background/40 text-muted-foreground mt-3 rounded-md border border-dashed p-2 text-xs'>
|
||||
{status.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{status && !status.configured && (
|
||||
<p className='mt-3 text-xs text-muted-foreground'>
|
||||
<p className='text-muted-foreground mt-3 text-xs'>
|
||||
{t(
|
||||
'To enable: set OPENBAO_ADDR + OPENBAO_TOKEN (or VAULT_*) env vars on the Manager container and restart. The status panel will pick up the connection on the next refresh.'
|
||||
'To enable: set AZURE_KEY_VAULT_URL on the Manager container, enable the VM managed identity, grant it Key Vault secret permissions, and restart. The status panel will pick up the connection on the next refresh.'
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
@@ -202,10 +174,10 @@ export function SecretStoreSection() {
|
||||
function Field({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div>
|
||||
<p className='text-[11px] uppercase tracking-[0.1em] text-muted-foreground'>
|
||||
<p className='text-muted-foreground text-[11px] tracking-[0.1em] uppercase'>
|
||||
{label}
|
||||
</p>
|
||||
<p className='mt-0.5 font-mono text-sm text-foreground'>{value}</p>
|
||||
<p className='text-foreground mt-0.5 font-mono text-sm'>{value}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -81,12 +81,12 @@ const MAINTENANCE_SECTIONS = [
|
||||
},
|
||||
},
|
||||
{
|
||||
// M7 — secret-store (OpenBao / Vault) status panel. Read-only,
|
||||
// Secret-store (Azure Key Vault) status panel. Read-only,
|
||||
// shows seal state + reachability; never exposes secret names
|
||||
// or plaintext values.
|
||||
id: 'secret-store',
|
||||
titleKey: 'Secret vault status',
|
||||
descriptionKey: 'OpenBao / Vault health and seal state',
|
||||
descriptionKey: 'Azure Key Vault reachability and managed identity state',
|
||||
build: (_settings: MaintenanceSettings) => <SecretStoreSection />,
|
||||
},
|
||||
{
|
||||
|
||||
+6
-6
@@ -1,19 +1,19 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getSelf } from '@/lib/api'
|
||||
import { getHeicodeBalance } from '@/lib/heicode-mcp'
|
||||
import { useStatus } from '@/hooks/use-status'
|
||||
import { useSystemConfig } from '@/hooks/use-system-config'
|
||||
import { getHeicodeBalance } from '@/lib/heicode-mcp'
|
||||
import { SectionPageLayout } from '@/components/layout'
|
||||
import { AffiliateRewardsCard } from './components/affiliate-rewards-card'
|
||||
import { BillingHistoryDialog } from './components/dialogs/billing-history-dialog'
|
||||
import { CreemConfirmDialog } from './components/dialogs/creem-confirm-dialog'
|
||||
import { PaymentConfirmDialog } from './components/dialogs/payment-confirm-dialog'
|
||||
import { TransferDialog } from './components/dialogs/transfer-dialog'
|
||||
import { HeicodeUsageCard } from './components/heicode-usage-card'
|
||||
import { RechargeFormCard } from './components/recharge-form-card'
|
||||
import { SubscriptionPlansCard } from './components/subscription-plans-card'
|
||||
import { WalletStatsCard } from './components/wallet-stats-card'
|
||||
import { HeicodeUsageCard } from './components/heicode-usage-card'
|
||||
import { DEFAULT_DISCOUNT_RATE } from './constants'
|
||||
import {
|
||||
useTopupInfo,
|
||||
@@ -101,14 +101,14 @@ export function Wallet(props: WalletProps) {
|
||||
setUser({
|
||||
id: heicodeBalance.heicodeUserId,
|
||||
username: heicodeBalance.username,
|
||||
email: heicodeBalance.email,
|
||||
display_name: heicodeBalance.displayName,
|
||||
group: heicodeBalance.group,
|
||||
status: heicodeBalance.status,
|
||||
quota: heicodeBalance.quota,
|
||||
used_quota: heicodeBalance.usedQuota,
|
||||
request_count: heicodeBalance.requestCount,
|
||||
} as UserWalletData)
|
||||
aff_quota: 0,
|
||||
aff_history_quota: 0,
|
||||
aff_count: 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
const response = await getSelf()
|
||||
|
||||
+13
-8
@@ -3949,6 +3949,7 @@
|
||||
"WeChat login QR code": "WeChat login QR code",
|
||||
"WeChat QR code will be displayed here": "WeChat QR code will be displayed here",
|
||||
"WeChat sign in": "WeChat sign in",
|
||||
"We recommend using a mainland China email address so verification emails arrive more reliably.": "We recommend using a mainland China email address so verification emails arrive more reliably.",
|
||||
"Week": "Week",
|
||||
"Weekday": "Weekday",
|
||||
"Weekly": "Weekly",
|
||||
@@ -4120,8 +4121,8 @@
|
||||
"{{n}} SK source connected": "{{n}} SK source connected",
|
||||
"Pick a reusable skill / agent toolset repository, or skip.": "Pick a reusable skill / agent toolset repository, or skip.",
|
||||
"Secret vault status": "Secret vault status",
|
||||
"OpenBao / Vault health and seal state": "OpenBao / Vault health and seal state",
|
||||
"Heicode stores long-lived credentials in OpenBao / Vault. This panel surfaces health and seal state for the operator — secret names and values are NEVER displayed here.": "Heicode stores long-lived credentials in OpenBao / Vault. This panel surfaces health and seal state for the operator — secret names and values are NEVER displayed here.",
|
||||
"Azure Key Vault reachability and managed identity state": "Azure Key Vault reachability and managed identity state",
|
||||
"Heicode stores long-lived credentials in Azure Key Vault. This panel surfaces provider, managed identity and reachability status — secret names and values are NEVER displayed here.": "Heicode stores long-lived credentials in Azure Key Vault. This panel surfaces provider, managed identity and reachability status — secret names and values are NEVER displayed here.",
|
||||
"Not configured": "Not configured",
|
||||
"Unreachable": "Unreachable",
|
||||
"Sealed": "Sealed",
|
||||
@@ -4131,21 +4132,23 @@
|
||||
"Failed to read secret store status": "Failed to read secret store status",
|
||||
"Refresh": "Refresh",
|
||||
"Configured": "Configured",
|
||||
"Provider": "Provider",
|
||||
"Auth method": "Auth method",
|
||||
"Reachable": "Reachable",
|
||||
"Initialized": "Initialized",
|
||||
"Standby": "Standby",
|
||||
"Version": "Version",
|
||||
"Last checked": "Last checked",
|
||||
"To enable: set OPENBAO_ADDR + OPENBAO_TOKEN (or VAULT_*) env vars on the Manager container and restart. The status panel will pick up the connection on the next refresh.": "To enable: set OPENBAO_ADDR + OPENBAO_TOKEN (or VAULT_*) env vars on the Manager container and restart. The status panel will pick up the connection on the next refresh.",
|
||||
"To enable: set AZURE_KEY_VAULT_URL on the Manager container, enable the VM managed identity, grant it Key Vault secret permissions, and restart. The status panel will pick up the connection on the next refresh.": "To enable: set AZURE_KEY_VAULT_URL on the Manager container, enable the VM managed identity, grant it Key Vault secret permissions, and restart. The status panel will pick up the connection on the next refresh.",
|
||||
"You do not have permission to view this": "You do not have permission to view this",
|
||||
"Your account or token lacks the required permission. Contact an administrator to request access.": "Your account or token lacks the required permission. Contact an administrator to request access.",
|
||||
"Network unreachable": "Network unreachable",
|
||||
"Could not reach the server. Check your internet connection and the Heicode Manager status, then retry.": "Could not reach the server. Check your internet connection and the Heicode Manager status, then retry.",
|
||||
"Failed to load": "Failed to load",
|
||||
"Connect Azure subscription": "Connect Azure subscription",
|
||||
"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) 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.": "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.",
|
||||
"Secret vault not yet configured": "Secret vault not yet configured",
|
||||
"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.": "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.": "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.",
|
||||
"Display name": "Display name",
|
||||
"Subscription ID": "Subscription ID",
|
||||
"Tenant ID": "Tenant ID",
|
||||
@@ -4153,15 +4156,17 @@
|
||||
"Client secret": "Client secret",
|
||||
"What Heicode will and will not do": "What Heicode will and will not do",
|
||||
"Read-only ARM access (Reader role recommended)": "Read-only ARM access (Reader role recommended)",
|
||||
"Auto-discover VM / database / AKS / storage — coming soon": "Auto-discover VM / database / AKS / storage — coming soon",
|
||||
"Auto-discover VM / database / AKS / storage": "Auto-discover VM / database / AKS / storage",
|
||||
"Never modify resources without explicit approval from the desktop client": "Never modify resources without explicit approval from the desktop client",
|
||||
"Never write the client_secret into Manager logs or audit payloads": "Never write the client_secret into Manager logs or audit payloads",
|
||||
"Binding...": "Binding...",
|
||||
"Bind subscription": "Bind subscription",
|
||||
"Azure subscription bound": "Azure subscription bound",
|
||||
"Azure discovery failed": "Azure discovery failed",
|
||||
"{{n}} Azure resources discovered": "{{n}} Azure resources discovered",
|
||||
"Failed to bind Azure account": "Failed to bind Azure account",
|
||||
"Connect Azure": "Connect Azure",
|
||||
"Authorize Azure (AWS / GCP coming soon). Resource auto-discovery later.": "Authorize Azure (AWS / GCP coming soon). Resource auto-discovery later.",
|
||||
"Resource auto-discovery — coming soon": "Resource auto-discovery — coming soon"
|
||||
"Authorize Azure (AWS / GCP coming soon). Heicode auto-discovers cloud resources.": "Authorize Azure (AWS / GCP coming soon). Heicode auto-discovers cloud resources.",
|
||||
"{{n}} cloud account connected": "{{n}} cloud account connected"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-8
@@ -3949,6 +3949,7 @@
|
||||
"WeChat login QR code": "微信登录二维码",
|
||||
"WeChat QR code will be displayed here": "微信二维码将显示在此处",
|
||||
"WeChat sign in": "微信登录",
|
||||
"We recommend using a mainland China email address so verification emails arrive more reliably.": "建议使用国内邮箱注册,验证码邮件到达会更稳定。",
|
||||
"Week": "周",
|
||||
"Weekday": "星期",
|
||||
"Weekly": "每周",
|
||||
@@ -4120,8 +4121,8 @@
|
||||
"{{n}} SK source connected": "已绑定 {{n}} 个 SK 来源",
|
||||
"Pick a reusable skill / agent toolset repository, or skip.": "选一个可复用的技能包 / Agent 工具集仓库,也可以跳过。",
|
||||
"Secret vault status": "密钥保管器状态",
|
||||
"OpenBao / Vault health and seal state": "OpenBao / Vault 的健康状态和封存状态",
|
||||
"Heicode stores long-lived credentials in OpenBao / Vault. This panel surfaces health and seal state for the operator — secret names and values are NEVER displayed here.": "Heicode 把长期凭证存在 OpenBao / Vault 里。这个面板只给运维同事看健康状态、是否封存——**永远不会**在这里显示密钥名称或明文。",
|
||||
"Azure Key Vault reachability and managed identity state": "Azure Key Vault 可达性与托管身份状态",
|
||||
"Heicode stores long-lived credentials in Azure Key Vault. This panel surfaces provider, managed identity and reachability status — secret names and values are NEVER displayed here.": "Heicode 把长期凭证存在 Azure Key Vault 里。这个面板只展示提供方、托管身份和可达性状态——永远不会显示密钥名称或明文。",
|
||||
"Not configured": "未配置",
|
||||
"Unreachable": "不可达",
|
||||
"Sealed": "已封存",
|
||||
@@ -4131,21 +4132,23 @@
|
||||
"Failed to read secret store status": "读取密钥保管器状态失败",
|
||||
"Refresh": "刷新",
|
||||
"Configured": "已配置",
|
||||
"Provider": "提供方",
|
||||
"Auth method": "认证方式",
|
||||
"Reachable": "可达",
|
||||
"Initialized": "已初始化",
|
||||
"Standby": "备用模式",
|
||||
"Version": "版本",
|
||||
"Last checked": "最近检查",
|
||||
"To enable: set OPENBAO_ADDR + OPENBAO_TOKEN (or VAULT_*) env vars on the Manager container and restart. The status panel will pick up the connection on the next refresh.": "启用方法:在 Manager 容器上配置 OPENBAO_ADDR + OPENBAO_TOKEN 环境变量(或 VAULT_*),然后重启容器。状态面板会在下次刷新时自动识别。",
|
||||
"To enable: set AZURE_KEY_VAULT_URL on the Manager container, enable the VM managed identity, grant it Key Vault secret permissions, and restart. The status panel will pick up the connection on the next refresh.": "启用方法:在 Manager 容器上配置 AZURE_KEY_VAULT_URL,启用 VM 托管身份,授予 Key Vault 密钥权限,然后重启容器。状态面板会在下次刷新时自动识别。",
|
||||
"You do not have permission to view this": "你没有查看此内容的权限",
|
||||
"Your account or token lacks the required permission. Contact an administrator to request access.": "你的账户或令牌缺少所需的权限。请联系管理员申请。",
|
||||
"Network unreachable": "网络无法连接",
|
||||
"Could not reach the server. Check your internet connection and the Heicode Manager status, then retry.": "无法连接服务器。请检查你的网络连接和 Heicode Manager 是否正常,然后重试。",
|
||||
"Failed to load": "加载失败",
|
||||
"Connect Azure subscription": "绑定 Azure 订阅",
|
||||
"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.": "粘贴一个对该订阅至少拥有 Reader 角色的 Service Principal。等资源自动发现功能上线后,Heicode 会用它枚举云资源(虚拟机、数据库、AKS、存储)。AWS 和 GCP 即将支持。",
|
||||
"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.": "粘贴一个对该订阅至少拥有 Reader 角色的 Service Principal。Heicode 会用它枚举云资源(虚拟机、数据库、AKS、存储)。AWS 和 GCP 即将支持。",
|
||||
"Secret vault not yet configured": "密钥保管器尚未配置",
|
||||
"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.": "Manager 还没接上 OpenBao(详见 系统设置 → 密钥保管器状态)。绑定会以 pending 状态创建——在密钥保管器接通之前,client_secret 不会持久化到服务端。等保管器上线后请重新输入或轮换密钥。",
|
||||
"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.": "Azure Key Vault 尚未就绪(详见 系统设置 → 密钥保管器状态)。Manager 能把 client_secret 写入 Key Vault 之前,不能绑定订阅。",
|
||||
"Display name": "显示名称",
|
||||
"Subscription ID": "订阅 ID",
|
||||
"Tenant ID": "租户 ID",
|
||||
@@ -4153,15 +4156,17 @@
|
||||
"Client secret": "Client secret",
|
||||
"What Heicode will and will not do": "Heicode 会做什么 / 不会做什么",
|
||||
"Read-only ARM access (Reader role recommended)": "只读 ARM 访问(推荐 Reader 角色)",
|
||||
"Auto-discover VM / database / AKS / storage — coming soon": "自动发现 VM / 数据库 / AKS / 存储 — 即将上线",
|
||||
"Auto-discover VM / database / AKS / storage": "自动发现 VM / 数据库 / AKS / 存储",
|
||||
"Never modify resources without explicit approval from the desktop client": "未经桌面客户端明确审批,不会修改任何资源",
|
||||
"Never write the client_secret into Manager logs or audit payloads": "不会把 client_secret 写入 Manager 日志或审计记录",
|
||||
"Binding...": "绑定中...",
|
||||
"Bind subscription": "绑定订阅",
|
||||
"Azure subscription bound": "Azure 订阅已绑定",
|
||||
"Azure discovery failed": "Azure 资源发现失败",
|
||||
"{{n}} Azure resources discovered": "已发现 {{n}} 个 Azure 资源",
|
||||
"Failed to bind Azure account": "Azure 账户绑定失败",
|
||||
"Connect Azure": "绑定 Azure",
|
||||
"Authorize Azure (AWS / GCP coming soon). Resource auto-discovery later.": "授权 Azure(AWS / GCP 即将支持)。资源自动发现稍后上线。",
|
||||
"Resource auto-discovery — coming soon": "资源自动发现 — 即将上线"
|
||||
"Authorize Azure (AWS / GCP coming soon). Heicode auto-discovers cloud resources.": "授权 Azure(AWS / GCP 即将支持)。Heicode 会自动发现云资源。",
|
||||
"{{n}} cloud account connected": "已连接 {{n}} 个云账号"
|
||||
}
|
||||
}
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
export type ManagerResourceType =
|
||||
| 'git'
|
||||
| 'sk'
|
||||
| 'project_document'
|
||||
| 'cloud_account'
|
||||
| 'cloud_resource'
|
||||
|
||||
export type ManagerResourceStatus =
|
||||
| 'pending'
|
||||
| 'active'
|
||||
| 'disabled'
|
||||
| 'revoked'
|
||||
|
||||
export type ManagerResource = {
|
||||
id: number
|
||||
user_id: number
|
||||
tenant_id: string
|
||||
project_id: string
|
||||
binding_scope: string
|
||||
name: string
|
||||
resource_type: ManagerResourceType
|
||||
provider: string
|
||||
external_id: string
|
||||
secret_ref: string
|
||||
metadata: Record<string, unknown>
|
||||
permission_scope: Record<string, unknown>
|
||||
constraints: Record<string, unknown>
|
||||
status: ManagerResourceStatus
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export type ManagerResourcePayload = {
|
||||
tenant_id?: string
|
||||
project_id?: string
|
||||
binding_scope?: string
|
||||
name: string
|
||||
resource_type: ManagerResourceType
|
||||
provider?: string
|
||||
external_id?: string
|
||||
secret_ref?: string
|
||||
metadata?: Record<string, unknown>
|
||||
permission_scope?: Record<string, unknown>
|
||||
constraints?: Record<string, unknown>
|
||||
status?: ManagerResourceStatus
|
||||
}
|
||||
|
||||
type Envelope<T> = {
|
||||
success: boolean
|
||||
message?: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
function unwrap<T>(env: Envelope<T>, fallback: string): T {
|
||||
if (!env.success || env.data == null) {
|
||||
throw new Error(env.message || fallback)
|
||||
}
|
||||
return env.data
|
||||
}
|
||||
|
||||
export async function listManagerResources(params?: {
|
||||
resource_type?: ManagerResourceType
|
||||
status?: ManagerResourceStatus
|
||||
}): Promise<ManagerResource[]> {
|
||||
const res = await api.get<Envelope<{ items: ManagerResource[] }>>(
|
||||
'/api/resources/',
|
||||
{ params }
|
||||
)
|
||||
return res.data?.data?.items ?? []
|
||||
}
|
||||
|
||||
export async function createManagerResource(
|
||||
body: ManagerResourcePayload
|
||||
): Promise<ManagerResource> {
|
||||
const res = await api.post<Envelope<ManagerResource>>('/api/resources/', body)
|
||||
return unwrap(res.data, 'createManagerResource failed')
|
||||
}
|
||||
|
||||
export async function updateManagerResource(
|
||||
id: number,
|
||||
body: ManagerResourcePayload
|
||||
): Promise<ManagerResource> {
|
||||
const res = await api.put<Envelope<ManagerResource>>(
|
||||
`/api/resources/${id}`,
|
||||
body
|
||||
)
|
||||
return unwrap(res.data, 'updateManagerResource failed')
|
||||
}
|
||||
|
||||
export async function putManagerResourceSecret(
|
||||
id: number,
|
||||
data: Record<string, unknown>
|
||||
): Promise<{ resource: ManagerResource; secret_ref: string }> {
|
||||
const res = await api.post<
|
||||
Envelope<{ resource: ManagerResource; secret_ref: string }>
|
||||
>(`/api/resources/${id}/secret`, { data })
|
||||
return unwrap(res.data, 'putManagerResourceSecret failed')
|
||||
}
|
||||
|
||||
export async function discoverManagerAzureResources(
|
||||
id: number
|
||||
): Promise<{
|
||||
account: ManagerResource
|
||||
items: ManagerResource[]
|
||||
discovered: number
|
||||
}> {
|
||||
const res = await api.post<
|
||||
Envelope<{
|
||||
account: ManagerResource
|
||||
items: ManagerResource[]
|
||||
discovered: number
|
||||
}>
|
||||
>(`/api/resources/${id}/azure/discover`)
|
||||
return unwrap(res.data, 'discoverManagerAzureResources failed')
|
||||
}
|
||||
+22
@@ -45,6 +45,7 @@ import { Route as AuthenticatedPlaygroundIndexRouteImport } from './routes/_auth
|
||||
import { Route as AuthenticatedModelsIndexRouteImport } from './routes/_authenticated/models/index'
|
||||
import { Route as AuthenticatedKeysIndexRouteImport } from './routes/_authenticated/keys/index'
|
||||
import { Route as AuthenticatedEventsIndexRouteImport } from './routes/_authenticated/events/index'
|
||||
import { Route as AuthenticatedDevicesIndexRouteImport } from './routes/_authenticated/devices/index'
|
||||
import { Route as AuthenticatedDesktopClientIndexRouteImport } from './routes/_authenticated/desktop-client/index'
|
||||
import { Route as AuthenticatedDeploymentsIndexRouteImport } from './routes/_authenticated/deployments/index'
|
||||
import { Route as AuthenticatedDashboardIndexRouteImport } from './routes/_authenticated/dashboard/index'
|
||||
@@ -264,6 +265,12 @@ const AuthenticatedEventsIndexRoute =
|
||||
path: '/events/',
|
||||
getParentRoute: () => AuthenticatedRouteRoute,
|
||||
} as any)
|
||||
const AuthenticatedDevicesIndexRoute =
|
||||
AuthenticatedDevicesIndexRouteImport.update({
|
||||
id: '/devices/',
|
||||
path: '/devices/',
|
||||
getParentRoute: () => AuthenticatedRouteRoute,
|
||||
} as any)
|
||||
const AuthenticatedDesktopClientIndexRoute =
|
||||
AuthenticatedDesktopClientIndexRouteImport.update({
|
||||
id: '/desktop-client/',
|
||||
@@ -464,6 +471,7 @@ export interface FileRoutesByFullPath {
|
||||
'/dashboard/': typeof AuthenticatedDashboardIndexRoute
|
||||
'/deployments/': typeof AuthenticatedDeploymentsIndexRoute
|
||||
'/desktop-client/': typeof AuthenticatedDesktopClientIndexRoute
|
||||
'/devices/': typeof AuthenticatedDevicesIndexRoute
|
||||
'/events/': typeof AuthenticatedEventsIndexRoute
|
||||
'/keys/': typeof AuthenticatedKeysIndexRoute
|
||||
'/models/': typeof AuthenticatedModelsIndexRoute
|
||||
@@ -527,6 +535,7 @@ export interface FileRoutesByTo {
|
||||
'/dashboard': typeof AuthenticatedDashboardIndexRoute
|
||||
'/deployments': typeof AuthenticatedDeploymentsIndexRoute
|
||||
'/desktop-client': typeof AuthenticatedDesktopClientIndexRoute
|
||||
'/devices': typeof AuthenticatedDevicesIndexRoute
|
||||
'/events': typeof AuthenticatedEventsIndexRoute
|
||||
'/keys': typeof AuthenticatedKeysIndexRoute
|
||||
'/models': typeof AuthenticatedModelsIndexRoute
|
||||
@@ -594,6 +603,7 @@ export interface FileRoutesById {
|
||||
'/_authenticated/dashboard/': typeof AuthenticatedDashboardIndexRoute
|
||||
'/_authenticated/deployments/': typeof AuthenticatedDeploymentsIndexRoute
|
||||
'/_authenticated/desktop-client/': typeof AuthenticatedDesktopClientIndexRoute
|
||||
'/_authenticated/devices/': typeof AuthenticatedDevicesIndexRoute
|
||||
'/_authenticated/events/': typeof AuthenticatedEventsIndexRoute
|
||||
'/_authenticated/keys/': typeof AuthenticatedKeysIndexRoute
|
||||
'/_authenticated/models/': typeof AuthenticatedModelsIndexRoute
|
||||
@@ -660,6 +670,7 @@ export interface FileRouteTypes {
|
||||
| '/dashboard/'
|
||||
| '/deployments/'
|
||||
| '/desktop-client/'
|
||||
| '/devices/'
|
||||
| '/events/'
|
||||
| '/keys/'
|
||||
| '/models/'
|
||||
@@ -723,6 +734,7 @@ export interface FileRouteTypes {
|
||||
| '/dashboard'
|
||||
| '/deployments'
|
||||
| '/desktop-client'
|
||||
| '/devices'
|
||||
| '/events'
|
||||
| '/keys'
|
||||
| '/models'
|
||||
@@ -789,6 +801,7 @@ export interface FileRouteTypes {
|
||||
| '/_authenticated/dashboard/'
|
||||
| '/_authenticated/deployments/'
|
||||
| '/_authenticated/desktop-client/'
|
||||
| '/_authenticated/devices/'
|
||||
| '/_authenticated/events/'
|
||||
| '/_authenticated/keys/'
|
||||
| '/_authenticated/models/'
|
||||
@@ -1091,6 +1104,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedEventsIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRouteRoute
|
||||
}
|
||||
'/_authenticated/devices/': {
|
||||
id: '/_authenticated/devices/'
|
||||
path: '/devices'
|
||||
fullPath: '/devices/'
|
||||
preLoaderRoute: typeof AuthenticatedDevicesIndexRouteImport
|
||||
parentRoute: typeof AuthenticatedRouteRoute
|
||||
}
|
||||
'/_authenticated/desktop-client/': {
|
||||
id: '/_authenticated/desktop-client/'
|
||||
path: '/desktop-client'
|
||||
@@ -1387,6 +1407,7 @@ interface AuthenticatedRouteRouteChildren {
|
||||
AuthenticatedDashboardIndexRoute: typeof AuthenticatedDashboardIndexRoute
|
||||
AuthenticatedDeploymentsIndexRoute: typeof AuthenticatedDeploymentsIndexRoute
|
||||
AuthenticatedDesktopClientIndexRoute: typeof AuthenticatedDesktopClientIndexRoute
|
||||
AuthenticatedDevicesIndexRoute: typeof AuthenticatedDevicesIndexRoute
|
||||
AuthenticatedEventsIndexRoute: typeof AuthenticatedEventsIndexRoute
|
||||
AuthenticatedKeysIndexRoute: typeof AuthenticatedKeysIndexRoute
|
||||
AuthenticatedModelsIndexRoute: typeof AuthenticatedModelsIndexRoute
|
||||
@@ -1419,6 +1440,7 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
|
||||
AuthenticatedDashboardIndexRoute: AuthenticatedDashboardIndexRoute,
|
||||
AuthenticatedDeploymentsIndexRoute: AuthenticatedDeploymentsIndexRoute,
|
||||
AuthenticatedDesktopClientIndexRoute: AuthenticatedDesktopClientIndexRoute,
|
||||
AuthenticatedDevicesIndexRoute: AuthenticatedDevicesIndexRoute,
|
||||
AuthenticatedEventsIndexRoute: AuthenticatedEventsIndexRoute,
|
||||
AuthenticatedKeysIndexRoute: AuthenticatedKeysIndexRoute,
|
||||
AuthenticatedModelsIndexRoute: AuthenticatedModelsIndexRoute,
|
||||
|
||||
@@ -33,11 +33,15 @@ if not PASS:
|
||||
print("MGR_PASS not set", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
DB_PASS = "Myadmin@123456."
|
||||
DB_PASS = os.environ.get("MGR_DB_PASS", "")
|
||||
DB_HOST = "heicode.postgres.database.azure.com"
|
||||
DB_USER = "heicode"
|
||||
DB_NAME = "heicode"
|
||||
|
||||
if not DB_PASS:
|
||||
print("MGR_DB_PASS not set", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# new-api token keys are 48-char alphanum (no padding). Match that
|
||||
# exactly so admin tools / dashboards parse it.
|
||||
ALPHABET = string.ascii_letters + string.digits
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Look up which user account to attach the swarm-program token to."""
|
||||
import os, sys, paramiko
|
||||
import os, shlex, sys, paramiko
|
||||
|
||||
db_pass = os.environ.get("MGR_DB_PASS", "")
|
||||
if not db_pass:
|
||||
print("MGR_DB_PASS not set", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
c = paramiko.SSHClient()
|
||||
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
@@ -19,9 +24,11 @@ LIMIT 10;
|
||||
# Wrap via psql exec inside the heicode container.
|
||||
cmd = (
|
||||
'sudo docker exec heicode sh -c '
|
||||
+ '"PGPASSWORD=\\"Myadmin@123456.\\" psql '
|
||||
+ shlex.quote(
|
||||
"PGPASSWORD=" + shlex.quote(db_pass) + " psql "
|
||||
+ '-h heicode.postgres.database.azure.com -U heicode heicode '
|
||||
+ "-c \\\"" + sql.replace("\n", " ").replace('"', '\\"\\"') + "\\\"\""
|
||||
+ "-c " + shlex.quote(sql.replace("\n", " "))
|
||||
)
|
||||
)
|
||||
_, o, e = c.exec_command(cmd, timeout=60, get_pty=True)
|
||||
for line in iter(o.readline, ""):
|
||||
|
||||
Reference in New Issue
Block a user