Compare commits

..

42 Commits

Author SHA1 Message Date
xiaojunnuo 9f7d766cb3 v1.39.13 2026-05-11 00:26:11 +08:00
xiaojunnuo 6c0d0b00c9 build: prepare to build 2026-05-11 00:21:28 +08:00
xiaojunnuo a82a38421d chore: sql 2026-05-11 00:20:25 +08:00
xiaojunnuo c4b01da384 fix(plugin-aliyun): 过滤非CAS证书并优化日志信息
过滤证书列表仅保留CAS类型证书,并更新相关日志信息以更准确反映操作情况
2026-05-11 00:07:48 +08:00
xiaojunnuo 3e1473dba5 chore: 1 2026-05-11 00:03:16 +08:00
xiaojunnuo d383706554 chore(用户资料): 添加头像和昵称的编辑按钮
- 在头像和昵称旁边添加编辑按钮,提升用户操作便捷性
- 移除原有的更新资料按钮,优化界面布局
- 调整角色信息显示位置至用户名旁边
2026-05-10 23:58:23 +08:00
xiaojunnuo e0eb0e21f6 perf(用户资料): 新增手机号邮箱绑定功能
实现用户邮箱和手机号的绑定与修改功能,包括:
1. 添加联系方式绑定API接口
2. 实现身份验证流程
3. 添加前端绑定对话框组件
4. 完善用户资料页面的联系方式展示和编辑入口
5. 添加联系方式冲突检测逻辑
6. 实现验证码校验功能
2026-05-10 23:51:45 +08:00
xiaojunnuo 7266af1749 fix: cnameProvider域名支持设置子域名托管 2026-05-10 22:41:07 +08:00
xiaojunnuo f93bc09438 chore(plugin-cert): 更新EAB选项标签和生成按钮文本
- 移除EAB选项中的免费说明文字
- 将"刷新"按钮文本改为"生成"
- 更新相关提示信息以保持一致性
2026-05-10 18:28:03 +08:00
xiaojunnuo fe3bb7c1b4 chore(plugin-cert): 修改刷新按钮文本为更简洁的表述 2026-05-10 17:15:47 +08:00
xiaojunnuo 923676c7d5 build: 添加 cross-env 依赖并更新单元测试脚本
在 package.json 和 pnpm-lock.yaml 中添加 cross-env 依赖,用于在单元测试时设置 NODE_ENV=unittest 环境变量。同时更新 AGENTS.md 文档说明单元测试环境要求。
2026-05-10 17:06:00 +08:00
xiaojunnuo 4755216505 perf: 重构自动加载模块并优化EAB授权处理
refactor(ui): 将分散的auto-*模块整合为统一命名的auto-register模块
perf(plugin-cert): 增强EAB授权功能,支持账号私钥刷新和类型选择
test: 添加EAB授权服务和ACME账号配置的单元测试
docs: 更新AGENTS.md补充ACME/EAB使用注意事项
chore: 统一各package.json中的测试脚本配置
2026-05-10 16:57:12 +08:00
xiaojunnuo 37d03c10f9 chore: 1 2026-05-08 22:28:10 +08:00
xiaojunnuo 490b724808 chore: 更新腾讯云证书(Id不变)插件下架 2026-05-08 22:19:31 +08:00
xiaojunnuo b8a64a6b5b perf(plugin-volcengine): 支持火山引擎VKE部署插件
- 改进Kubernetes错误处理,添加RBAC权限不足的友好提示
- 添加集群ID验证和kubeconfig解码验证
- 优化Ingress不存在时的错误提示,显示可用选项
- 移除未使用的TargetCluster选项
- 修复kubeconfig请求中集群ID未验证的问题
2026-05-08 00:25:00 +08:00
xiaojunnuo 25ad1e6f86 perf(设置): 添加首页启用开关配置
在系统设置中添加首页启用开关配置项,包括前端路由守卫检查、多语言支持和表单配置,以控制是否显示首页
2026-05-07 23:15:50 +08:00
xiaojunnuo 6b6f1604e9 style(ui): 修复批量操作区域样式溢出问题
为批量操作区域添加 overflow-x-auto 和 whitespace-nowrap 样式,防止文本溢出
2026-05-07 23:01:42 +08:00
xiaojunnuo 63be1c1cbd perf(证书流水线): 添加批量更新证书申请参数功能
实现批量更新证书申请参数功能,包括前端界面和后端处理逻辑
- 添加批量修改证书申请参数的按钮和对话框
- 实现后端批量更新证书申请参数的接口和服务
- 添加相关测试用例验证功能正确性
2026-05-07 22:54:29 +08:00
xiaojunnuo b75c625ddc fix(aliyun-access): 添加阿里云密钥校验失败的错误处理
在测试阿里云访问密钥时,增加对STS错误响应的处理逻辑
添加相关测试用例验证错误处理和正常情况
2026-05-07 22:18:34 +08:00
xiaojunnuo 7083e7aff7 test(tld-client): 添加对rdap.ss回退机制的测试
验证当rdap和whoiser都失败时,TldClient能正确委托RdapSsClient处理
2026-05-07 22:03:33 +08:00
xiaojunnuo 9d2937dd4b perf(domain): 添加域名过期时间同步进度显示功能
添加同步进度对话框,展示同步任务的实时进度和状态
更新中英文翻译文件,添加相关文案
修改AGENTS.md文档中的格式化命令说明
2026-05-07 00:22:41 +08:00
xiaojunnuo a7e281e278 chore: rdap-ss client 重构 2026-05-05 22:53:22 +08:00
xiaojunnuo 72b6597817 chore: agents update 2026-05-05 22:17:09 +08:00
xiaojunnuo 91a1b97550 perf: 域名注册过期时间获取再次优化 2026-05-05 21:56:08 +08:00
xiaojunnuo 9951ab678f chore: rm app.log 2026-05-05 19:26:36 +08:00
xiaojunnuo 930aa355e8 refactor(acme-client): 将acme-client改造成ts包并优化项目结构
重构acme-client模块,将原有JavaScript代码迁移至TypeScript
添加类型定义文件(.d.ts)和类型检查
更新构建配置和脚本以支持TypeScript编译
优化项目目录结构和模块导出方式
更新相关依赖和开发工具配置
2026-05-05 19:17:44 +08:00
xiaojunnuo e0143fa540 chore: 补充单元测试 2026-05-05 18:44:43 +08:00
xiaojunnuo 7c1d92ff4b chore: 为多个package.json添加test:unit脚本并更新全局测试命令
在多个库和插件的package.json中添加统一的test:unit脚本
同时在根package.json中添加全局test:unit命令以并行运行所有子项目的单元测试
2026-05-01 09:25:53 +08:00
xiaojunnuo 0a0f1e90e1 chore: 补充单元测试 2026-05-01 09:16:46 +08:00
xiaojunnuo 80092823db docs(AGENTS): 更新文档以明确文件读取时的编码设置
添加关于在PowerShell中读取中文和文档文件时使用UTF-8编码的说明,并更新示例命令
2026-05-01 00:21:28 +08:00
xiaojunnuo 146098d9ce docs(AGENTS.md): 更新开发指南和本地化文件格式
修复中文语言文件缩进问题并更新自定义页脚说明
添加前端文件修改后的格式化建议到开发指南
2026-05-01 00:18:11 +08:00
xiaojunnuo 519743dbdb style(locales): 调整多语言文件缩进格式并添加自定义页脚字段
调整多个语言文件的缩进格式以保持一致性
在系统设置中添加自定义页脚相关的中英文翻译字段
2026-05-01 00:15:47 +08:00
xiaojunnuo 7ab661ecd7 Merge branch 'v2-dev' into codex_i18n 2026-05-01 00:07:29 +08:00
xiaojunnuo bb46cb08f7 perf: 支持火山云vke 2026-05-01 00:05:58 +08:00
xiaojunnuo 028932c04a refactor: organize certd client i18n translations 2026-04-30 23:48:48 +08:00
xiaojunnuo 73e6480853 chore: add shared codex skills link 2026-04-30 23:40:58 +08:00
xiaojunnuo aa176b081a perf: 新增agents.md 2026-04-30 23:31:12 +08:00
xiaojunnuo 267243e71b chore: i18n first 2026-04-30 22:57:00 +08:00
xiaojunnuo 33fbef8380 build: release 2026-04-29 23:28:39 +08:00
xiaojunnuo 45a128a050 build: publish 2026-04-29 23:12:05 +08:00
xiaojunnuo 2ddc668954 build: trigger build image 2026-04-29 23:11:53 +08:00
xiaojunnuo ad76c5177c chore: 1 2026-04-28 12:12:34 +08:00
215 changed files with 7425 additions and 2477 deletions
+1
View File
@@ -0,0 +1 @@
../.trae/skills
+220
View File
@@ -0,0 +1,220 @@
# Certd 开发 Agent 上下文
这个文件是给在本仓库工作的开发 agent 看的常驻项目说明。后续会话进入仓库后,应先读取它,再按任务需要查看具体代码,避免每次都重新全量扫描项目。
## 项目用途
Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。它提供 Web 管理台和后端服务,用于证书申请、续期、部署、监控、通知和开放 API 集成。
它不只是一个简单的 ACME 客户端。项目的核心产品模型是“证书流水线”:
- 通过 ACME 申请证书
- 支持 DNS-01、HTTP-01、CNAME 代理以及各类服务商集成来完成域名验证
- 支持将证书转换或导出为 pem、pfx、der、jks、p7b 等格式
- 支持把证书部署到主机、Nginx、Kubernetes、CDN、云厂商、面板等目标
- 支持通知用户,并监控站点证书过期时间
由于系统会保存证书、云厂商凭据、SSH 信息、API Key 等敏感数据,产品定位上强烈建议私有化/本地部署。
## 仓库结构
这是一个 pnpm + lerna 的 monorepo。
- `package.json`:根脚本和 workspace 元信息
- `pnpm-workspace.yaml`workspace 包匹配规则
- `lerna.json`lerna-lite 配置
- `docs/`VitePress 文档站
- `docker/`Docker 安装和运行相关文件
- `packages/core/acme-client/`:ACME 协议客户端,风格接近 node-acme-client
- `packages/core/basic/`:共享基础工具和基础设施
- `packages/core/pipeline/`:流水线核心、注册表、装饰器、插件模型、上下文、服务、通知等
- `packages/libs/`:共享集成与辅助库,例如 server、Huawei、JDCloud、Kubernetes、iframe
- `packages/plugins/plugin-lib/`:通用插件辅助能力和证书相关共享代码
- `packages/plugins/plugin-cert/`:证书流水线插件包
- `packages/pro/`:商业版/专业版相关包
- `packages/ui/certd-server/`:后端服务
- `packages/ui/certd-client/`:前端 Web 管理台
## 后端
主要后端包:`packages/ui/certd-server`
技术栈:
- Node.js、ESM、TypeScript
- MidwayJS 3
- Koa
- TypeORM
- 默认使用 better-sqlite3,同时支持 PostgreSQL 和 MySQL
- 通过 `@certd/midway-flyway-js` 使用类似 Flyway 的 SQL 迁移机制
重要位置:
- `packages/ui/certd-server/src/config/config.default.ts`:默认服务、静态文件、数据库、定时任务、认证、上传、Swagger 配置
- `packages/ui/certd-server/src/config/`:环境与配置加载逻辑
- `packages/ui/certd-server/src/configuration.ts`:Midway 应用配置、中间件注册、组件导入
- `packages/ui/certd-server/src/modules/`:业务模块,例如 pipeline、cert、cron、monitor、login、open API、sys、plugin、cname、notification
- `packages/ui/certd-server/src/controller/`:按 API 领域划分的控制器
- `packages/ui/certd-server/src/plugins/`:后端内置的具体服务商、部署、通知等插件
- `packages/ui/certd-server/db/migration/`:数据库迁移 SQL
- `packages/ui/certd-server/data/`:本地运行数据,例如 SQLite 数据库和生成文件
- `packages/ui/certd-server/logs/`:运行日志
已观察到的默认开发配置:
- HTTP 端口:`7001`
- HTTPS 端口:`7002`
- 默认 SQLite 数据库:`./data/db.sqlite`
- 默认文件根目录:`./data/files`
常用脚本:
- 根目录 `pnpm run start:server`:以生产模式启动后端包
- 后端 `pnpm run dev`:启动 Midway watch/dev 服务
- 后端 `pnpm run test`:运行后端 mocha 测试
- 后端 `pnpm run build`:构建后端并导出插件元数据
## 前端
主要前端包:`packages/ui/certd-client`
技术栈:
- Vue 3
- Vite
- TypeScript
- Ant Design Vue
- Fast Crud
- Pinia
- vue-router
- vue-i18n
- Tailwind/Windi 相关样式工具
重要位置:
- `packages/ui/certd-client/src/main.ts`:前端启动入口
- `packages/ui/certd-client/src/App.vue`:根组件
- `packages/ui/certd-client/src/api/`API 调用封装
- `packages/ui/certd-client/src/router/`:路由
- `packages/ui/certd-client/src/store/`Pinia store
- `packages/ui/certd-client/src/views/certd/`:核心产品页面,例如流水线、证书、授权、监控、通知、开放 API、项目、支付、插件
- `packages/ui/certd-client/src/components/`:共享 UI 组件
- `packages/ui/certd-client/src/locales/`:国际化
常用脚本:
- 前端 `pnpm dev`:启动 Vite 开发服务
- 前端 `pnpm build`:生产构建
- 前端 `pnpm tsc`:类型检查
- 前端暂不跑单元测试;当前 `test:unit` 只是占位脚本
## 流水线与插件模型
项目最关键的架构概念是证书流水线。
可以从 `packages/core/pipeline/src/index.ts` 入手,它导出:
- `core`
- `dt`
- `access`
- `registry`
- `plugin`
- `context`
- `decorator`
- `service`
- `notification`
插件是核心能力,不是边缘功能。新增服务商、DNS 验证、证书部署、通知方式等能力,通常应该放在插件包里,或放在 `packages/ui/certd-server/src/plugins/<plugin-name>/` 下。
后端已看到的插件类型包括:
- DNS 和注册商服务商:Aliyun、Tencent、Cloudflare、Huawei、JDCloud、AWS、Azure、Google、GoDaddy、Namesilo、Xinnet、West、UCloud、Qiniu、Upyun、Volcengine 等
- 部署目标:host、Kubernetes、Nginx Proxy Manager、APISIX、Proxmox、QNAP、Dokploy、GoEdge、各类 CDN、各类面板
- 系统/产品插件:notification、captcha、oauth、admin、plus/pro、demo/template
当修改证书申请、验证、部署或通知行为时,先判断改动属于哪里:
- ACME client 代码
- pipeline 核心抽象
- 后端 module/service/entity/controller
- 某个具体插件实现
- 前端 view/form/schema
如果只是某个服务商或部署目标的问题,不要轻易修改共享 pipeline/core 行为,除非确实是可复用的公共能力。
### ACME / EAB 注意事项
- 公共 EAB(尤其是 Google EAB)可能只能创建一次 ACME 账号。要跨用户复用公共 EAB,应保存并复用同一个 ACME account private key`accountUrl` 如果存到 `userContext` 里,只能视为当前用户缓存,因为 `userContext` 跟用户 id 走。
- ACME 协议的 `newAccount` 支持 `onlyReturnExisting`。使用同一个 account private key 调用 `newAccount({ onlyReturnExisting: true })` 可以取回已创建账号的 URL,且不会再次消费 EAB。
- 修改 EAB 的 `kid` 后,应重新生成绑定该 `kid` 的 account private key;否则应阻止继续申请并提示用户刷新账号私钥。
## 数据与迁移
后端使用 TypeORM 实体加 SQL 迁移。
重点查看:
- `packages/ui/certd-server/src/modules/**/entity/*.ts`
- `packages/ui/certd-server/db/migration/*.sql`
默认配置中 `synchronize: false`,所以涉及表结构变更时,通常应该添加或更新迁移脚本,而不是依赖 TypeORM 自动同步。
## 开发注意事项
- 中文 README 在部分 PowerShell 环境中可能显示乱码;`README_en.md` 可读性更好,且包含同样的高层项目说明。
- 初次整理时观察到当前分支为 `v2-dev`
- 根包管理器是 pnpm,不要引入 npm/yarn lockfile。
- 优先沿用现有模块、插件、服务模式,再考虑新增抽象。
- `packages/ui/certd-server/data/``logs/`、生成的 metadata/dist 等通常视为运行时或构建产物,除非任务明确要求处理它们。
- 注意本地数据和配置里可能包含凭据、证书材料等敏感信息。
- 本仓库代码注释优先使用中文,尤其是解释业务规则、兼容逻辑、协议细节和隐藏风险时;除非文件已有明确英文注释风格或引用外部英文术语,否则不要新增英文说明性注释。
## 插件开发技能
仓库内置了 Certd 插件开发技能,供 Trae 和 Codex 共用:
- Trae 入口:`.trae/skills`
- Codex 入口:`.codex/skills`
其中 `.codex/skills` 是指向 `.trae/skills` 的目录链接,不要复制出第二份技能内容。更新技能时只维护 `.trae/skills` 下的原始文件,Codex 会通过 `.codex/skills` 读取同一份内容。
当前技能包括:
- `access-plugin-dev`:开发 Access 授权插件
- `dns-provider-dev`:开发 DNS Provider 插件
- `task-plugin-dev`:开发 Task 部署任务插件
- `plugin-converter`:将插件转换为 YAML 配置
做插件相关任务时,先读取对应技能目录下的 `SKILL.md`,再进入具体实现。若用户在插件开发中指出更好的做法,应总结并更新对应技能。
## 快速定向命令
进入项目后,优先使用这些有目标的读取命令,而不是立刻全仓库扫描:
```powershell
Get-Content -Encoding utf8 package.json
Get-Content -Encoding utf8 pnpm-workspace.yaml
Get-Content -Encoding utf8 lerna.json
Get-Content -Encoding utf8 README_en.md -TotalCount 180
Get-Content -Encoding utf8 packages\ui\certd-server\package.json
Get-Content -Encoding utf8 packages\ui\certd-client\package.json
Get-ChildItem packages\ui\certd-server\src\modules
Get-ChildItem packages\ui\certd-server\src\plugins
Get-ChildItem packages\ui\certd-client\src\views\certd
```
## 本仓库 Agent 工作方式
- 先读本文件,再按用户任务查看相关 package/module。
- 在 PowerShell 中读取中文、Markdown、locale、文档类文件时,显式使用 `Get-Content -Encoding utf8`;如果仍然显示乱码,再先执行 `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()` 后重试。
- 做后端任务时,先定位 `packages/ui/certd-server/src/modules` 下的模块,以及相关 entity/service/controller。
- 做前端任务时,先定位 `packages/ui/certd-client/src/views/certd` 下的页面,再找对应 `src/api`
- 做服务商、DNS、部署、通知相关任务时,先看 `packages/ui/certd-server/src/plugins`,再看 `packages/plugins/plugin-lib` 里的共享辅助能力。
- 做数据库结构变更时,添加或更新迁移脚本,不要依赖 TypeORM 自动同步。
- 实现新功能或修复行为缺陷前,先补对应单元测试,并先运行测试确认它处于失败状态;再实现功能或修复代码,反复运行聚焦单元测试直到通过。若某项改动确实不适合先写单元测试,应在回复中说明原因和替代验证方式。
- 后补单元测试时,应先基于对正确行为的实际预期编写测试,而不是为了迎合现有实现改写预期;如果运行后出现红灯,且通过测试需要修改已有实现,应先向用户确认这是确实的 bug,还是原本需求/既有行为就是如此;确认后再修改原始实现,避免把测试补充变成未经确认的行为改动。
- 后端纯单元测试用例放在 `src` 目录内,并尽量与被测文件相邻,例如 `src/utils/random.test.ts`;对应 `test:unit` 只跑 `src/**/*.test.ts`,构建/打包配置应排除这些 `*.test.ts` 文件。
- 单元测试需要 mock ESM 静态 import 时,优先使用 `esmock`,不要为了测试把业务代码改成构造函数注入或把逻辑挪到调用方;各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest`
- 单个 monorepo 包运行单元测试时,优先使用 `corepack pnpm --dir <包目录> test:unit`,例如 `corepack pnpm --dir packages\ui\certd-server test:unit``corepack pnpm --dir packages\core\basic test:unit``corepack pnpm --dir packages\plugins\plugin-lib test:unit`;也可以用包名过滤,例如 `corepack pnpm --filter @certd/ui-server test:unit`。前端 `packages\ui\certd-client` 暂时不跑单元测试。
- 前端 TS/Vue/locale 等文件改动后,优先只对本次改动文件运行项目现有自动格式化/修复;Windows/PowerShell 下 Prettier 已验证可用命令为 `packages\ui\certd-client\node_modules\.bin\prettier.cmd --write <files>`ESLint 可用命令为 `packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix <files>`;不要为了格式化无关文件而扩大 diff。项目保留了 `tslint` 依赖,但当前主要使用 ESLint + Prettier。
- 优先对改动包运行聚焦的测试或类型检查;只有跨包影响明显时再考虑全 monorepo 构建。
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Bug Fixes
* **aliyun-access:** 添加阿里云密钥校验失败的错误处理 ([b75c625](https://github.com/certd/certd/commit/b75c625ddcc0b3110699d8e6175681ef157b25df))
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
* **plugin-aliyun:** 过滤非CAS证书并优化日志信息 ([c4b01da](https://github.com/certd/certd/commit/c4b01da384bc40a241a673ea8bc01ca733c04d83))
### Performance Improvements
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
* 新增agents.md ([aa176b0](https://github.com/certd/certd/commit/aa176b081a92837d2d6809d16546a8dfc2e5dd36))
* **用户资料:** 新增手机号邮箱绑定功能 ([e0eb0e2](https://github.com/certd/certd/commit/e0eb0e21f6dae24b639c944f9aba2c90496ab1c0))
* 域名注册过期时间获取再次优化 ([91a1b97](https://github.com/certd/certd/commit/91a1b9755066bf280e194dabf7c3a9f936e2643f))
* **证书流水线:** 添加批量更新证书申请参数功能 ([63be1c1](https://github.com/certd/certd/commit/63be1c1cbd9b09a3b48f26130c296b1cedcca1ac))
* 支持火山云vke ([bb46cb0](https://github.com/certd/certd/commit/bb46cb08f71f6ae921543f7e4a6c5f4e0190556e))
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
* **domain:** 添加域名过期时间同步进度显示功能 ([9d2937d](https://github.com/certd/certd/commit/9d2937dd4b14ffab73e9b096edd2aa8539811182))
* **plugin-volcengine:** 支持火山引擎VKE部署插件 ([b8a64a6](https://github.com/certd/certd/commit/b8a64a6b5bf3691a47177de42bc49b798e795feb))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Bug Fixes
+15
View File
@@ -3,6 +3,21 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Bug Fixes
* 调整手机版首页标题被挤开的bug ([eab66e2](https://github.com/certd/certd/commit/eab66e2d1988635985745f2d1b227b958969ee00))
* 修复腾讯云clb部署报缺少sslmode参数的bug ([2f1ad72](https://github.com/certd/certd/commit/2f1ad7201f5ed9e00368a28b9e40907d4b415852))
### Performance Improvements
* 524错误时重试3次 ([00e6d58](https://github.com/certd/certd/commit/00e6d580c2f54af70fe96a214aff87c4b96426c2))
* 阿里云证书订单支持获取2.0的订单 ([64b3184](https://github.com/certd/certd/commit/64b3184b286fee996002d857b0de588452abdadd))
* 优化流水线执行时的状态保存性能 ([e00830b](https://github.com/certd/certd/commit/e00830bebcfe6344499e490bc174de96f9fb22d6))
* 增加权威NS检查开关,某些用户服务器禁止向黑名单NS服务器发请求 ([1aa50cf](https://github.com/certd/certd/commit/1aa50cf53a0deab752f35ec973912e41ab8161b6))
* 支持页脚自定义 ([c985a13](https://github.com/certd/certd/commit/c985a13544aa31b0eb0783f9a3193a7e8bdc6ed6))
## [1.39.11](https://github.com/certd/certd/compare/v1.39.10...v1.39.11) (2026-04-26)
### Bug Fixes
+3 -2
View File
@@ -134,8 +134,9 @@
| 4.| **火山引擎-部署证书至DCDN** | 部署至火山引擎全站加速 |
| 5.| **火山引擎-部署证书至Live** | 部署至火山引擎视频直播 |
| 6.| **火山引擎-部署证书至TOS自定义域名** | 仅限TOS自定义域名,加速域名请选择火山引擎的CDN插件 |
| 7.| **火山引擎-部署证书至VOD** | 部署至火山引擎视频点播 |
| 8.| **火山引擎-上传证书至证书中心** | 上传证书至火山引擎证书中心 |
| 7.| **火山引擎-替换VKE证书** | 替换火山引擎VKE集群中的TLS Secret证书 |
| 8.| **火山引擎-部署证书至VOD** | 部署至火山引擎视频点播 |
| 9.| **火山引擎-上传证书至证书中心** | 上传证书至火山引擎证书中心 |
## 9. 京东云
| 序号 | 名称 | 说明 |
+1 -1
View File
@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.39.12"
"version": "1.39.13"
}
+4 -2
View File
@@ -9,6 +9,7 @@
"@lerna-lite/run": "^3.9.3",
"@lerna-lite/version": "^3.9.3",
"axios": "^1.9.0",
"cross-env": "^7.0.3",
"medium-zoom": "^1.1.0",
"vitepress": "^2.0.0-alpha.4",
"vitepress-plugin-lightbox": "^1.0.2"
@@ -25,8 +26,8 @@
"commitAll": "git add . && git commit -m \"build: publish\" && git push && pnpm run commitPro",
"commitPro": "cd ./packages/pro/ && git add . && git commit -m \"build: publish\" && git push",
"copylogs": "copyfiles \"CHANGELOG.md\" ./docs/guide/changelogs/",
"prepublishOnly1": "pnpm run check && lerna run build ",
"prepublishOnly2": "pnpm run check && pnpm run before-build && lerna run build && pnpm run plugin-doc-gen",
"prepublishOnly1": "pnpm run test:unit && pnpm run check && lerna run build ",
"prepublishOnly2": "pnpm run test:unit && pnpm run check && pnpm run before-build && lerna run build && pnpm run plugin-doc-gen",
"before-build": "pnpm run transform-sql && cd ./packages/core/basic && time /t >build.md && git add ./build.md && git commit -m \"build: prepare to build\"",
"deploy1": "node --experimental-json-modules ./scripts/deploy.js ",
"check": "node --experimental-json-modules ./scripts/publish-check.js",
@@ -35,6 +36,7 @@
"docs:dev": "vitepress dev docs",
"docs:build": "pnpm run copylogs && vitepress build docs",
"docs:preview": "vitepress preview docs",
"test:unit": "cross-env NODE_ENV=unittest pnpm -r --workspace-concurrency=1 run test:unit",
"pub": "echo 1",
"dev": "pnpm run -r --parallel compile ",
"pub_all":"pnpm run -r --parallel pub ",
+12
View File
@@ -1,4 +1,13 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"ignorePatterns": [
"dist",
"node_modules"
],
"extends": [
"plugin:prettier/recommended",
"prettier"
@@ -7,9 +16,12 @@
"eslint-plugin-import"
],
"env": {
"node": true,
"es2022": true,
"mocha": true
},
"rules": {
"prettier/prettier": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
+5
View File
@@ -4,3 +4,8 @@ node_modules/
npm-debug.log
package-lock.json
/.idea/
/dist/
/dist-test/
/logs/
/tsconfig.tsbuildinfo
/tsconfig.test.tsbuildinfo
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/publishlab/node-acme-client/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/publishlab/node-acme-client/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/publishlab/node-acme-client/compare/v1.39.11...v1.39.12) (2026-04-29)
### Performance Improvements
+18 -12
View File
@@ -3,22 +3,22 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"module": "scr/index.js",
"main": "src/index.js",
"types": "types/index.d.ts",
"module": "./dist/index.js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"license": "MIT",
"homepage": "https://github.com/publishlab/node-acme-client",
"engines": {
"node": ">= 18"
},
"files": [
"src",
"dist",
"types"
],
"dependencies": {
"@certd/basic": "^1.39.12",
"@certd/basic": "^1.39.13",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -35,10 +35,12 @@
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.4.1",
"chai-as-promised": "^7.1.2",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"jsdoc-to-markdown": "^8.0.1",
"mocha": "^10.6.0",
"nock": "^13.5.4",
@@ -47,13 +49,17 @@
"typescript": "^5.4.2"
},
"scripts": {
"build-docs": "jsdoc2md src/client.js > docs/client.md && jsdoc2md src/crypto/index.js > docs/crypto.md && jsdoc2md src/crypto/forge.js > docs/forge.md",
"lint": "eslint .",
"lint-types": "tsd",
"prepublishOnly": "npm run build-docs",
"before-build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"",
"build": "npm run before-build && tsc --skipLibCheck",
"build-docs": "jsdoc2md dist/client.js > docs/client.md && jsdoc2md dist/crypto/index.js > docs/crypto.md && jsdoc2md dist/crypto/forge.js > docs/forge.md",
"lint": "eslint \"src/**/*.ts\" \"types/**/*.ts\"",
"lint-types": "tsd --files \"types/index.test-d.ts\"",
"prepublishOnly": "npm run build && npm run build-docs",
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
"before-test:unit": "node -e \"const fs=require('fs');fs.rmSync('dist-test',{recursive:true,force:true});fs.rmSync('tsconfig.test.tsbuildinfo',{force:true});\"",
"test:unit": "cross-env NODE_ENV=unittest npm run before-test:unit && cross-env NODE_ENV=unittest tsc -p tsconfig.test.json --skipLibCheck && cross-env NODE_ENV=unittest mocha -t 60000 \"dist-test/**/*.test.js\"",
"pub": "npm publish",
"compile": "echo '1'"
"compile": "tsc --skipLibCheck --watch"
},
"repository": {
"type": "git",
@@ -70,5 +76,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
}
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* ACME API client
*/
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* ACME auto helper
*/
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* Axios instance
*/
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* ACME client
*
@@ -570,7 +571,7 @@ class AcmeClient {
* ```
*/
async waitForValidStatus(item,d) {
async waitForValidStatus(item, d?) {
if (!item.url) {
throw new Error(`[${d}] Unable to verify status of item, URL not found`);
}
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* Legacy node-forge crypto interface
*
@@ -112,7 +113,7 @@ function parseDomains(obj) {
* ```
*/
export async function createPrivateKey(size = 2048) {
export async function createPrivateKey(size = 2048): Promise<Buffer> {
const keyPair = await generateKeyPair({ bits: size });
const pemKey = forge.pki.privateKeyToPem(keyPair.privateKey);
return Buffer.from(pemKey);
@@ -131,7 +132,7 @@ export async function createPrivateKey(size = 2048) {
* ```
*/
export const createPublicKey = async (key) => {
export const createPublicKey = async (key): Promise<Buffer> => {
const privateKey = forge.pki.privateKeyFromPem(key);
const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e);
const pemKey = forge.pki.publicKeyToPem(publicKey);
@@ -174,7 +175,7 @@ export const splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode
* ```
*/
export const getModulus = async (input) => {
export const getModulus = async (input): Promise<Buffer> => {
if (!Buffer.isBuffer(input)) {
input = Buffer.from(input);
}
@@ -197,7 +198,7 @@ export const getModulus = async (input) => {
* ```
*/
export const getPublicExponent = async (input) => {
export const getPublicExponent = async (input): Promise<Buffer> => {
if (!Buffer.isBuffer(input)) {
input = Buffer.from(input);
}
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* Native Node.js crypto interface
*
@@ -67,7 +68,7 @@ function getKeyInfo(keyPem) {
* ```
*/
export async function createPrivateRsaKey(modulusLength = 2048, encodingType = 'pkcs8') {
export async function createPrivateRsaKey(modulusLength = 2048, encodingType = 'pkcs8'): Promise<Buffer> {
const pair = await generateKeyPair('rsa', {
modulusLength,
privateKeyEncoding: {
@@ -105,7 +106,7 @@ export const createPrivateKey = createPrivateRsaKey;
* ```
*/
export const createPrivateEcdsaKey = async (namedCurve = 'P-256', encodingType = 'pkcs8') => {
export const createPrivateEcdsaKey = async (namedCurve = 'P-256', encodingType = 'pkcs8'): Promise<Buffer> => {
const pair = await generateKeyPair('ec', {
namedCurve,
privateKeyEncoding: {
@@ -129,7 +130,7 @@ export const createPrivateEcdsaKey = async (namedCurve = 'P-256', encodingType =
* ```
*/
export const getPublicKey = (keyPem) => {
export const getPublicKey = (keyPem): Buffer => {
const info = getKeyInfo(keyPem);
const publicKey = info.publicKey.export({
@@ -1,3 +1,4 @@
// @ts-nocheck
export class CancelError extends Error {
constructor(message) {
super(message);
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* ACME HTTP client
*/
@@ -0,0 +1,19 @@
import assert from "node:assert/strict";
import { directory, getAllSslProviderDomains, getDirectoryUrl } from "./index.js";
declare const describe: any;
declare const it: any;
describe("directory helpers", () => {
it("selects the provider specific directory endpoint", () => {
assert.equal(getDirectoryUrl({ sslProvider: "sslcom", pkType: "ec" }), directory.sslcom.ec);
assert.equal(getDirectoryUrl({ sslProvider: "letsencrypt", pkType: "rsa" }), directory.letsencrypt.production);
});
it("includes configured provider domains", () => {
const domains = getAllSslProviderDomains();
assert.ok(domains.includes("acme.litessl.com"));
assert.ok(domains.includes("acme.ssl.com"));
});
});
@@ -1,8 +1,9 @@
// @ts-nocheck
/**
* acme-client
*/
import AcmeClinet from './client.js'
export const Client = AcmeClinet
export { default as Client } from './client.js'
export type * from './types.js'
/**
* Directory URLs
@@ -103,4 +104,4 @@ export * from './logger.js'
export * from './verify.js'
export * from './error.js'
export * from './util.js'
export * from './util.js'
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* ACME logger
*/
+123
View File
@@ -0,0 +1,123 @@
/**
* Account
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.2
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
*/
export interface Account {
status: "valid" | "deactivated" | "revoked";
orders: string;
contact?: string[];
termsOfServiceAgreed?: boolean;
externalAccountBinding?: object;
}
export interface AccountCreateRequest {
contact?: string[];
termsOfServiceAgreed?: boolean;
onlyReturnExisting?: boolean;
externalAccountBinding?: object;
}
export interface AccountUpdateRequest {
status?: string;
contact?: string[];
termsOfServiceAgreed?: boolean;
}
/**
* Order
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
*/
export interface Order {
status: "pending" | "ready" | "processing" | "valid" | "invalid";
identifiers: Identifier[];
authorizations: string[];
finalize: string;
expires?: string;
notBefore?: string;
notAfter?: string;
error?: object;
certificate?: string;
}
export interface OrderCreateRequest {
identifiers: Identifier[];
notBefore?: string;
notAfter?: string;
}
/**
* Authorization
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.4
*/
export interface Authorization {
identifier: Identifier;
status: "pending" | "valid" | "invalid" | "deactivated" | "expired" | "revoked";
challenges: Challenge[];
expires?: string;
wildcard?: boolean;
}
export interface Identifier {
type: string;
value: string;
}
/**
* Challenge
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-8
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.3
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
*/
export interface ChallengeAbstract {
type: string;
url: string;
status: "pending" | "processing" | "valid" | "invalid";
validated?: string;
error?: object;
}
export interface HttpChallenge extends ChallengeAbstract {
type: "http-01";
token: string;
}
export interface DnsChallenge extends ChallengeAbstract {
type: "dns-01";
token: string;
}
export type Challenge = HttpChallenge | DnsChallenge;
/**
* Certificate
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
*/
export enum CertificateRevocationReason {
Unspecified = 0,
KeyCompromise = 1,
CACompromise = 2,
AffiliationChanged = 3,
Superseded = 4,
CessationOfOperation = 5,
CertificateHold = 6,
RemoveFromCRL = 8,
PrivilegeWithdrawn = 9,
AACompromise = 10,
}
export interface CertificateRevocationRequest {
reason?: CertificateRevocationReason;
}
+131
View File
@@ -0,0 +1,131 @@
import type * as rfc8555 from "./rfc8555.js";
import type { Challenge } from "./rfc8555.js";
export type * from "./rfc8555.js";
export type PrivateKeyBuffer = Buffer;
export type PublicKeyBuffer = Buffer;
export type CertificateBuffer = Buffer;
export type CsrBuffer = Buffer;
export type PrivateKeyString = string;
export type PublicKeyString = string;
export type CertificateString = string;
export type CsrString = string;
export interface Order extends rfc8555.Order {
url: string;
}
export interface Authorization extends rfc8555.Authorization {
url: string;
}
export type UrlMapping = {
enabled: boolean;
mappings: Record<string, string>;
};
export interface ClientExternalAccountBindingOptions {
kid: string;
hmacKey: string;
}
export interface ClientOptions {
sslProvider: string;
directoryUrl: string;
accountKey: PrivateKeyBuffer | PrivateKeyString;
accountUrl?: string;
externalAccountBinding?: ClientExternalAccountBindingOptions;
backoffAttempts?: number;
backoffMin?: number;
backoffMax?: number;
urlMapping?: UrlMapping;
signal?: AbortSignal;
logger?: any;
}
export interface ClientAutoOptions {
csr: CsrBuffer | CsrString;
challengeCreateFn: (
authz: Authorization,
keyAuthorization: (challenge: Challenge) => Promise<string>
) => Promise<{ recordReq?: any; recordRes?: any; dnsProvider?: any; challenge: Challenge; keyAuthorization: string }>;
challengeRemoveFn: (authz: Authorization, challenge: Challenge, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider: any, httpUploader: any) => Promise<any>;
email?: string;
termsOfServiceAgreed?: boolean;
skipChallengeVerification?: boolean;
challengePriority?: string[];
preferredChain?: string;
signal?: AbortSignal;
profile?: string;
waitDnsDiffuseTime?: number;
}
export interface CertificateDomains {
commonName: string;
altNames: string[];
}
export interface CertificateIssuer {
commonName: string;
}
export interface CertificateInfo {
issuer: CertificateIssuer;
domains: CertificateDomains;
notAfter: Date;
notBefore: Date;
}
export interface CsrOptions {
keySize?: number;
commonName?: string;
altNames?: string[];
country?: string;
state?: string;
locality?: string;
organization?: string;
organizationUnit?: string;
emailAddress?: string;
}
export interface RsaPublicJwk {
e: string;
kty: string;
n: string;
}
export interface EcdsaPublicJwk {
crv: string;
kty: string;
x: string;
y: string;
}
export interface CryptoInterface {
createPrivateKey(keySize?: number, encodingType?: string): Promise<PrivateKeyBuffer>;
createPrivateRsaKey(keySize?: number, encodingType?: string): Promise<PrivateKeyBuffer>;
createPrivateEcdsaKey(namedCurve?: "P-256" | "P-384" | "P-521", encodingType?: string): Promise<PrivateKeyBuffer>;
getPublicKey(keyPem: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString): PublicKeyBuffer;
getJwk(keyPem: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString): RsaPublicJwk | EcdsaPublicJwk;
splitPemChain(chainPem: CertificateBuffer | CertificateString): string[];
getPemBodyAsB64u(pem: CertificateBuffer | CertificateString): string;
readCsrDomains(csrPem: CsrBuffer | CsrString): CertificateDomains;
readCertificateInfo(certPem: CertificateBuffer | CertificateString): CertificateInfo;
createCsr(data: CsrOptions, keyPem?: PrivateKeyBuffer | PrivateKeyString, encodingType?: string): Promise<[PrivateKeyBuffer, CsrBuffer]>;
createAlpnCertificate(authz: Authorization, keyAuthorization: string, keyPem?: PrivateKeyBuffer | PrivateKeyString): Promise<[PrivateKeyBuffer, CertificateBuffer]>;
isAlpnCertificateAuthorizationValid(certPem: CertificateBuffer | CertificateString, keyAuthorization: string): boolean;
}
export interface CryptoLegacyInterface {
createPrivateKey(size?: number): Promise<PrivateKeyBuffer>;
createPublicKey(key: PrivateKeyBuffer | PrivateKeyString): Promise<PublicKeyBuffer>;
getPemBody(str: string): string;
splitPemChain(str: string): string[];
getModulus(input: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString | CertificateBuffer | CertificateString | CsrBuffer | CsrString): Promise<Buffer>;
getPublicExponent(input: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString | CertificateBuffer | CertificateString | CsrBuffer | CsrString): Promise<Buffer>;
readCsrDomains(csr: CsrBuffer | CsrString): Promise<CertificateDomains>;
readCertificateInfo(cert: CertificateBuffer | CertificateString): Promise<CertificateInfo>;
createCsr(data: CsrOptions, key?: PrivateKeyBuffer | PrivateKeyString): Promise<[PrivateKeyBuffer, CsrBuffer]>;
}
@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import { formatResponseError, parseRetryAfterHeader, retry } from "./util.js";
declare const describe: any;
declare const it: any;
describe("util helpers", () => {
it("parses retry-after values", () => {
assert.equal(parseRetryAfterHeader("120"), 120);
assert.equal(parseRetryAfterHeader("invalid"), 0);
assert.equal(parseRetryAfterHeader("Wed, 21 Oct 2015 07:28:00 GMT"), 0);
});
it("formats response errors without newlines", () => {
const error = formatResponseError({
data: {
error: {
detail: "line 1\nline 2",
},
},
});
assert.equal(error, "line 1line 2");
});
it("retries until success", async () => {
const delays: number[] = [];
const originalSetTimeout = globalThis.setTimeout;
let attempts = 0;
(globalThis as any).setTimeout = (fn: (...args: any[]) => void, delay?: number) => {
delays.push(Number(delay));
return originalSetTimeout(fn, 0);
};
try {
const result = await retry(
async () => {
attempts += 1;
if (attempts < 3) {
throw new Error(`boom-${attempts}`);
}
return "ok";
},
{ attempts: 3, min: 10, max: 20 },
() => {}
);
assert.equal(result, "ok");
assert.equal(attempts, 3);
assert.deepEqual(delays, [10, 20]);
} finally {
(globalThis as any).setTimeout = originalSetTimeout;
}
});
});
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* Utility methods
*/
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* ACME challenge verification
*/
@@ -1,3 +1,4 @@
// @ts-nocheck
export async function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
+29
View File
@@ -0,0 +1,29 @@
{
"compileOnSave": true,
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"inlineSourceMap": false,
"sourceMap": false,
"noImplicitThis": false,
"noUnusedLocals": false,
"stripInternal": true,
"skipLibCheck": true,
"pretty": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": "src",
"composite": false,
"useDefineForClassFields": false,
"strict": false,
"resolveJsonModule": true,
"isolatedModules": false,
"lib": ["ESNext", "DOM"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.json"],
"exclude": ["dist", "node_modules", "src/**/*.test.ts", "test"]
}
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist-test",
"declaration": false,
"declarationMap": false,
"emitDeclarationOnly": false
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "dist-test", "node_modules", "test"]
}
+14 -5
View File
@@ -4,8 +4,6 @@
import { AxiosInstance } from 'axios';
import * as rfc8555 from './rfc8555';
import {CancelError} from '../src/error.js'
export * from '../src/error.js'
export type PrivateKeyBuffer = Buffer;
export type PublicKeyBuffer = Buffer;
@@ -115,6 +113,15 @@ export const directory: {
zerossl: {
staging: string,
production: string
},
sslcom: {
staging: string,
production: string,
ec: string
},
litessl: {
staging: string,
production: string
}
};
@@ -211,14 +218,16 @@ export const agents: any;
* Logger
*/
export class CancelError extends Error {
constructor(message?: string);
}
export function setLogger(fn: (message: any, ...args: any[]) => void): void;
export function createChallengeFn(opts?: {logger?:any}): any;
// export function walkTxtRecord(record: any): Promise<string[]>;
export function getAuthoritativeDnsResolver(record:string): Promise<any>;
export const CancelError: typeof CancelError;
export function resolveDomainBySoaRecord(domain: string): Promise<string>;
export function setWalkFromAuthoritative(value = true): void;
export function setWalkFromAuthoritative(value?: boolean): void;
@@ -2,7 +2,7 @@
* acme-client type definition tests
*/
import * as acme from 'acme-client';
import * as acme from '..';
(async () => {
/* Client */
@@ -10,6 +10,7 @@ import * as acme from 'acme-client';
const client = new acme.Client({
accountKey,
sslProvider: 'letsencrypt',
directoryUrl: acme.directory.letsencrypt.staging
});
@@ -52,7 +53,10 @@ import * as acme from 'acme-client';
/* Auto */
await client.auto({
csr: certCsr,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {},
challengeCreateFn: async (authz, keyAuthorization) => ({
challenge: authz.challenges[0],
keyAuthorization: await keyAuthorization(authz.challenges[0])
}),
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {}
});
@@ -63,7 +67,10 @@ import * as acme from 'acme-client';
skipChallengeVerification: false,
challengePriority: ['http-01', 'dns-01'],
preferredChain: 'DST Root CA X3',
challengeCreateFn: async (authz, challenge, keyAuthorization) => {},
challengeCreateFn: async (authz, keyAuthorization) => ({
challenge: authz.challenges[0],
keyAuthorization: await keyAuthorization(authz.challenges[0])
}),
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {}
});
})();
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Performance Improvements
+1 -1
View File
@@ -1 +1 @@
23:06
00:21
+7 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -13,6 +13,7 @@
"dev-build": "npm run build",
"preview": "vite preview",
"test": "mocha --loader=ts-node/esm",
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
@@ -39,13 +40,17 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "4.3.10",
"cross-env": "^7.0.3",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
}
@@ -0,0 +1,31 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { amountUtils } from "./util.amount.js";
describe("amountUtils", () => {
describe("toCent", () => {
it("converts yuan values to cents", () => {
expect(amountUtils.toCent(1)).to.equal(100);
expect(amountUtils.toCent(12.34)).to.equal(1234);
});
it("rounds to the nearest cent", () => {
expect(amountUtils.toCent(1.235)).to.equal(124);
expect(amountUtils.toCent(1.234)).to.equal(123);
});
});
describe("toYuan", () => {
it("converts cent values to yuan", () => {
expect(amountUtils.toYuan(100)).to.equal(1);
expect(amountUtils.toYuan(1234)).to.equal(12.34);
});
it("rounds yuan values to two decimal places", () => {
expect(amountUtils.toYuan(1235)).to.equal(12.35);
expect(amountUtils.toYuan(1)).to.equal(0.01);
});
});
});
@@ -0,0 +1,43 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { domainUtils } from "./util.domain.js";
describe("domainUtils", () => {
describe("match", () => {
it("matches exact domains", () => {
expect(domainUtils.match("example.com", ["example.com"])).to.equal(true);
expect(domainUtils.match("api.example.com", ["example.com"])).to.equal(false);
});
it("matches wildcard domains by suffix", () => {
expect(domainUtils.match("api.example.com", ["*.example.com"])).to.equal(true);
expect(domainUtils.match("deep.api.example.com", ["*.example.com"])).to.equal(false);
expect(domainUtils.match("example.com", ["*.example.com"])).to.equal(false);
});
it("requires every target domain to match", () => {
expect(domainUtils.match(["api.example.com", "admin.example.com"], ["*.example.com"])).to.equal(true);
expect(domainUtils.match(["api.example.com", "other.com"], ["*.example.com"])).to.equal(false);
});
});
describe("isIp", () => {
it("detects valid IPv4 addresses", () => {
expect(domainUtils.isIpv4("127.0.0.1")).to.equal(true);
expect(domainUtils.isIpv4("255.255.255.255")).to.equal(true);
});
it("rejects invalid IPv4 addresses", () => {
expect(domainUtils.isIpv4("999.1.1.1")).to.equal(false);
expect(domainUtils.isIpv4("1.2.3")).to.equal(false);
expect(domainUtils.isIpv4("example.com")).to.equal(false);
});
it("detects IPv6 addresses", () => {
expect(domainUtils.isIpv6("2001:db8::1")).to.equal(true);
expect(domainUtils.isIp("2001:db8::1")).to.equal(true);
});
});
});
+4 -1
View File
@@ -51,7 +51,10 @@ function isIpv4(d: string) {
return false;
}
const isIPv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
return isIPv4Regex.test(d);
if (!isIPv4Regex.test(d)) {
return false;
}
return d.split(".").every(item => Number(item) <= 255);
}
function isIpv6(d: string) {
@@ -0,0 +1,42 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { isDev } from "./util.env.js";
describe("isDev", () => {
const originalNodeEnv = process.env.NODE_ENV;
afterEach(() => {
if (originalNodeEnv == null) {
delete process.env.NODE_ENV;
} else {
process.env.NODE_ENV = originalNodeEnv;
}
});
it("treats missing NODE_ENV as development", () => {
delete process.env.NODE_ENV;
expect(isDev()).to.equal(true);
});
it("detects development-like NODE_ENV values", () => {
process.env.NODE_ENV = "development";
expect(isDev()).to.equal(true);
process.env.NODE_ENV = "local";
expect(isDev()).to.equal(true);
process.env.NODE_ENV = "dev-test";
expect(isDev()).to.equal(true);
});
it("rejects production-like NODE_ENV values", () => {
process.env.NODE_ENV = "production";
expect(isDev()).to.equal(false);
process.env.NODE_ENV = "test";
expect(isDev()).to.equal(false);
});
});
@@ -0,0 +1,45 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { hashUtils } from "./util.hash.js";
describe("hashUtils", () => {
describe("digest helpers", () => {
it("generates md5 and sha256 hex digests by default", () => {
expect(hashUtils.md5("certd")).to.equal("3f3d9f715fcc63d54a4a224e0939a233");
expect(hashUtils.sha256("certd")).to.equal("26a6366060d2a6477185c05075155769cb438c6c71f61f509535b8516594ad92");
});
it("supports alternate digest encodings", () => {
expect(hashUtils.md5("certd", "base64")).to.equal("Pz2fcV/MY9VKSiJOCTmiMw==");
expect(hashUtils.sha256("certd", "base64")).to.equal("JqY2YGDSpkdxhcBQdRVXactDjGxx9h9QlTW4UWWUrZI=");
});
});
describe("hmac helpers", () => {
it("signs data with a provided key", () => {
expect(hashUtils.hmacSha256WithKey("secret", "certd")).to.equal("kh/kUD/Ji8FHfpt4vYUHZx+1BZvKSyyklZIiuS+Rzlg=");
});
it("uses an empty payload when only the key is provided", () => {
expect(hashUtils.hmacSha256("secret")).to.equal("+eZuF5tnR65UEI+C+K3os8Jddv0wr95sOVgixTAZYWk=");
});
});
describe("encoding helpers", () => {
it("round trips base64 values", () => {
const encoded = hashUtils.base64("证书-certd");
expect(encoded).to.equal("6K+B5LmmLWNlcnRk");
expect(hashUtils.base64Decode(encoded)).to.equal("证书-certd");
});
it("converts strings and numbers to hex", () => {
expect(hashUtils.toHex("certd")).to.equal("6365727464");
expect(hashUtils.hexToStr("6365727464")).to.equal("certd");
expect(hashUtils.toHex(255)).to.equal("ff");
expect(hashUtils.hexToNumber("ff")).to.equal(255);
});
});
});
@@ -0,0 +1,18 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { randomNumber, simpleNanoId } from "./util.id.js";
describe("id utils", () => {
it("generates a four digit random number string", () => {
expect(randomNumber()).to.match(/^\d{4}$/);
});
it("generates a twelve character simple nano id", () => {
const id = simpleNanoId();
expect(id).to.have.length(12);
expect(id).to.match(/^[0-9a-zA-Z]+$/);
});
});
@@ -0,0 +1,78 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { mergeUtils, UnMergeable } from "./util.merge.js";
describe("mergeUtils", () => {
describe("merge", () => {
it("deep merges plain objects", () => {
const target = { acme: { email: "admin@example.com" }, deploy: { retries: 1 } };
const result = mergeUtils.merge(target, { acme: { dnsProvider: "cloudflare" } });
expect(result).to.equal(target);
expect(result).to.deep.equal({
acme: { email: "admin@example.com", dnsProvider: "cloudflare" },
deploy: { retries: 1 },
});
});
it("replaces arrays instead of merging them by index", () => {
const result = mergeUtils.merge({ domains: ["old.example.com", "legacy.example.com"] }, { domains: ["new.example.com"] });
expect(result).to.deep.equal({ domains: ["new.example.com"] });
});
it("allows null to clear nested values", () => {
const result = mergeUtils.merge({ cert: { name: "certd" } }, { cert: null });
expect(result).to.deep.equal({ cert: null });
});
it("keeps undefined sources from overwriting existing nested values", () => {
const result = mergeUtils.merge({ cert: { name: "certd" } }, { cert: undefined });
expect(result).to.deep.equal({ cert: { name: "certd" } });
});
it("returns an UnMergeable source directly when it is merged at the top level", () => {
const source = new UnMergeable();
const result = mergeUtils.merge({ enabled: true }, source);
expect(result).to.equal(source);
});
it("replaces nested values marked as UnMergeable", () => {
const source = new UnMergeable();
const result = mergeUtils.merge({ plugin: { enabled: true } }, { plugin: source });
expect(result.plugin).to.equal(source);
});
});
describe("cloneDeep", () => {
it("deep clones plain values", () => {
const source = { acme: { email: "admin@example.com" }, domains: ["example.com"] };
const result = mergeUtils.cloneDeep(source);
expect(result).to.deep.equal(source);
expect(result).not.to.equal(source);
expect(result.acme).not.to.equal(source.acme);
expect(result.domains).not.to.equal(source.domains);
});
it("preserves references marked as not cloneable", () => {
const uncloneable = new UnMergeable();
const source = { plugin: uncloneable };
const result = mergeUtils.cloneDeep(source);
expect(result).not.to.equal(source);
expect(result.plugin).to.equal(uncloneable);
});
});
});
@@ -0,0 +1,40 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { optionsUtils } from "./util.options.js";
describe("optionsUtils", () => {
describe("groupByDomain", () => {
it("splits options by domain match", () => {
const matchedOption = { value: "matched", domain: "api.example.com" };
const wildcardMatchedOption = { value: "wildcard", domain: "admin.example.com" };
const unmatchedOption = { value: "unmatched", domain: "other.com" };
const result = optionsUtils.groupByDomain([matchedOption, wildcardMatchedOption, unmatchedOption], ["api.example.com", "*.example.com"]);
expect(result.matched).to.deep.equal([matchedOption, wildcardMatchedOption]);
expect(result.notMatched).to.deep.equal([unmatchedOption]);
});
it("treats options without matching domains as not matched", () => {
const optionWithoutDomain = { value: "empty" };
const result = optionsUtils.groupByDomain([optionWithoutDomain], ["example.com"]);
expect(result.matched).to.deep.equal([]);
expect(result.notMatched).to.deep.equal([optionWithoutDomain]);
});
});
describe("buildGroupOptions", () => {
it("builds disabled group labels around matched and unmatched options", () => {
const matchedOption = { value: "matched", domain: "api.example.com" };
const unmatchedOption = { value: "unmatched", domain: "other.com" };
const result = optionsUtils.buildGroupOptions([matchedOption, unmatchedOption], ["api.example.com"]);
expect(result).to.deep.equal([{ value: "matched", disabled: true, label: "----已匹配----" }, matchedOption, { value: "unmatched", disabled: true, label: "----未匹配----" }, unmatchedOption]);
});
});
});
@@ -0,0 +1,90 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { logger } from "./util.log.js";
import { promises } from "./util.promise.js";
describe("promises", () => {
describe("TimeoutPromise", () => {
it("resolves when the callback finishes before the timeout", async () => {
let completed = false;
await promises.TimeoutPromise(async () => {
completed = true;
}, 50);
expect(completed).to.equal(true);
});
it("rejects when the callback exceeds the timeout", async () => {
try {
await promises.TimeoutPromise(() => new Promise<void>(resolve => setTimeout(resolve, 30)), 5);
expect.fail("expected TimeoutPromise to reject");
} catch (e: any) {
expect(e.message).to.equal("Task timeout in 5 ms");
}
});
});
describe("safePromise", () => {
it("resolves values provided by the callback", async () => {
const result = await promises.safePromise<string>(resolve => {
resolve("ok");
});
expect(result).to.equal("ok");
});
it("rejects synchronous errors thrown by the callback", async () => {
const oldLevel = logger.level;
logger.level = "fatal";
try {
await promises.safePromise(() => {
throw new Error("boom");
});
expect.fail("expected safePromise to reject");
} catch (e: any) {
expect(e.message).to.equal("boom");
} finally {
logger.level = oldLevel;
}
});
});
describe("promisify", () => {
it("resolves callback data", async () => {
const readValue = promises.promisify((prefix: string, callback: (err: Error | null, data?: string) => void) => {
callback(null, `${prefix}-value`);
});
expect(await readValue("certd")).to.equal("certd-value");
});
it("rejects callback errors", async () => {
const failing = promises.promisify((callback: (err: Error) => void) => {
callback(new Error("callback failed"));
});
try {
await failing();
expect.fail("expected promisified function to reject");
} catch (e: any) {
expect(e.message).to.equal("callback failed");
}
});
it("rejects synchronous errors", async () => {
const failing = promises.promisify(() => {
throw new Error("sync failed");
});
try {
await failing();
expect.fail("expected promisified function to reject");
} catch (e: any) {
expect(e.message).to.equal("sync failed");
}
});
});
});
@@ -26,7 +26,7 @@ export class HttpError extends Error {
return;
}
let message = error?.message || error?.response.statusText || error?.code;
let message = error?.message || error?.response?.statusText || error?.code;
if (message && typeof message === "string" && message.indexOf) {
for (const key in errorMap) {
if (message.indexOf(key) > -1) {
@@ -267,7 +267,7 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
logger.error(`请求出错:${errorMessage} status:${status},statusText:${error.response?.statusText || error.code},url:${error.config?.url},method:${error.config?.method}`);
logger.error("返回数据:", JSON.stringify(error.response?.data));
if (error.response?.data) {
const message = error.response.data.message || error.response.data.msg || error.response.data.error;
const message = error.response?.data?.message || error.response?.data?.msg || error.response?.data?.error;
if (typeof message === "string") {
error.message = message;
}
@@ -0,0 +1,36 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { stringUtils } from "./util.string.js";
describe("stringUtils", () => {
describe("maxLength", () => {
it("returns an empty string for empty input", () => {
expect(stringUtils.maxLength()).to.equal("");
expect(stringUtils.maxLength("")).to.equal("");
});
it("returns the original string when it is within the limit", () => {
expect(stringUtils.maxLength("certd", 5)).to.equal("certd");
expect(stringUtils.maxLength("certd", 6)).to.equal("certd");
});
it("truncates strings longer than the limit and appends ellipsis", () => {
expect(stringUtils.maxLength("certificate", 4)).to.equal("cert...");
});
});
describe("appendTimeSuffix", () => {
it("returns an empty string for empty input", () => {
expect(stringUtils.appendTimeSuffix()).to.equal("");
expect(stringUtils.appendTimeSuffix("")).to.equal("");
});
it("appends a millisecond timestamp suffix", () => {
const result = stringUtils.appendTimeSuffix("certd");
expect(result).to.match(/^certd-\d{17}$/);
});
});
});
+5 -15
View File
@@ -7,7 +7,7 @@
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"inlineSourceMap":false,
"inlineSourceMap": false,
"sourceMap": false,
"noImplicitThis": true,
"noUnusedLocals": true,
@@ -22,21 +22,11 @@
"composite": false,
"useDefineForClassFields": true,
"strict": true,
"typeRoots": [ "./typings", "./node_modules/@types"],
"typeRoots": ["./typings", "./node_modules/@types"],
"resolveJsonModule": true,
"isolatedModules": false,
"lib": ["ESNext", "DOM"],
"lib": ["ESNext", "DOM"]
},
"include": [
"src/**/*.ts",
"src/**/*.json"
],
"exclude": [
"*.js",
"*.ts",
"*.spec.ts",
"dist",
"node_modules",
"test"
],
"include": ["src/**/*.ts", "src/**/*.json"],
"exclude": ["*.js", "*.ts", "*.spec.ts", "dist", "node_modules", "src/**/*.test.ts", "test"]
}
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Bug Fixes
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Performance Improvements
+8 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -14,12 +14,13 @@
"build3": "rollup -c",
"preview": "vite preview",
"test": "mocha --loader=ts-node/esm",
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/basic": "^1.39.12",
"@certd/plus-core": "^1.39.12",
"@certd/basic": "^1.39.13",
"@certd/plus-core": "^1.39.13",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13"
@@ -36,14 +37,17 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "4.3.10",
"cross-env": "^7.0.3",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
}
@@ -0,0 +1,23 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { PluginGroup, pluginGroups } from "./group.js";
describe("PluginGroup", () => {
it("initializes a group with defaults", () => {
const group = new PluginGroup("custom", "Custom");
expect(group.key).to.equal("custom");
expect(group.title).to.equal("Custom");
expect(group.order).to.equal(0);
expect(group.icon).to.equal("");
expect(group.plugins).to.deep.equal([]);
});
it("exposes built-in groups with stable keys", () => {
expect(pluginGroups.cert.key).to.equal("cert");
expect(pluginGroups.host.key).to.equal("host");
expect(pluginGroups.other.order).to.equal(10);
});
});
@@ -3,6 +3,7 @@ import { IAccess } from "../access/index.js";
export type CnameProvider = {
id: any;
domain: string;
subdomain?: string;
title?: string;
dnsProviderType?: string;
access?: IAccess;
+5 -16
View File
@@ -7,7 +7,7 @@
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"inlineSourceMap":false,
"inlineSourceMap": false,
"sourceMap": false,
"noImplicitThis": true,
"noUnusedLocals": true,
@@ -22,22 +22,11 @@
"composite": false,
"useDefineForClassFields": true,
"strict": true,
// "sourceMap": true,
// "sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": false,
"lib": ["ESNext", "DOM"],
"lib": ["ESNext", "DOM"]
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.json"
],
"exclude": [
"*.js",
"*.ts",
"*.spec.ts",
"dist",
"node_modules",
"test"
],
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.json"],
"exclude": ["*.js", "*.ts", "*.spec.ts", "dist", "node_modules", "src/**/*.test.ts", "test"]
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/lib-huawei
+5 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -11,6 +11,7 @@
"build": "npm run before-build && rollup -c ",
"dev-build": "npm run build",
"preview": "vite preview",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish"
},
"dependencies": {
@@ -21,8 +22,10 @@
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"esmock": "^2.7.5",
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/lib-iframe
+5 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -14,6 +14,7 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish"
},
"dependencies": {
@@ -23,13 +24,15 @@
"@types/chai": "^4.3.3",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/jdcloud
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.39.12",
"version": "1.39.13",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -8,6 +8,7 @@
"scripts": {
"build": "rollup -c ",
"dev-build": "npm run build",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish"
},
"author": "",
@@ -29,7 +30,8 @@
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.1.2",
"config": "^1.30.0",
"cross-env": "^5.1.4",
"cross-env": "^7.0.3",
"esmock": "^2.7.5",
"js-yaml": "^3.11.0",
"mocha": "^5.0.0",
"prettier": "^2.8.8",
@@ -56,5 +58,5 @@
"fetch"
]
},
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/lib-k8s
+6 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -14,24 +14,27 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/basic": "^1.39.12",
"@certd/basic": "^1.39.13",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
"@types/chai": "^4.3.3",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
}
+7
View File
@@ -3,6 +3,13 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Performance Improvements
+13 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.39.12",
"version": "1.39.13",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -12,6 +12,7 @@
"build": "npm run before-build && tsc --skipLibCheck",
"dev-build": "npm run build",
"test": "midway-bin test --ts -V",
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"test1": "midway-bin test --ts -V -f test/blank.test.ts -t 'hash-check'",
"cov": "midway-bin cov --ts",
"lint": "mwts check",
@@ -28,11 +29,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.39.12",
"@certd/basic": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/plugin-lib": "^1.39.12",
"@certd/plus-core": "^1.39.12",
"@certd/acme-client": "^1.39.13",
"@certd/basic": "^1.39.13",
"@certd/pipeline": "^1.39.13",
"@certd/plugin-lib": "^1.39.13",
"@certd/plus-core": "^1.39.13",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -43,7 +44,6 @@
"@midwayjs/upload": "3.20.13",
"@midwayjs/validate": "3.20.13",
"better-sqlite3": "^11.1.2",
"cross-env": "^7.0.3",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"mwts": "^1.3.0",
@@ -52,17 +52,22 @@
},
"devDependencies": {
"@types/chai": "^4.3.3",
"@types/mocha": "^10.0.1",
"@types/node": "^18",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
}
@@ -0,0 +1,32 @@
/// <reference types="mocha" />
/// <reference types="node" />
import assert from "node:assert/strict";
import { Constants } from "./constants.js";
import { ParamException } from "./exception/param-exception.js";
import { Result } from "./result.js";
describe("lib-server basic helpers", () => {
it("builds success and error results", () => {
const success = Result.success("ok", { id: 1 });
assert.ok(success instanceof Result);
assert.equal(success.code, 0);
assert.equal(success.message, "ok");
assert.deepEqual(success.data, { id: 1 });
const error = Result.error(400, "bad request");
assert.ok(error instanceof Result);
assert.equal(error.code, 400);
assert.equal(error.message, "bad request");
assert.equal(error.data, undefined);
});
it("uses default param exception metadata", () => {
const error = new ParamException(undefined);
assert.equal(error.name, "ParamException");
assert.equal(error.code, Constants.res.param.code);
assert.equal(error.message, Constants.res.param.message);
});
});
@@ -32,6 +32,7 @@ export class SysPublicSettings extends BaseSettings {
customFooter?: string;
robots?: boolean = true;
aiChatEnabled = true;
homePageEnabled = true;
//验证码是否开启
@@ -271,4 +272,3 @@ export class SysSafeSetting extends BaseSettings {
autoHiddenTimes: 5,
};
}
@@ -0,0 +1,30 @@
import assert from "assert";
import { AccessService } from "./access-service.js";
describe("AccessService", () => {
it("does not write id into access setting when updating selected fields", async () => {
let updateParam: any;
const service = new AccessService();
service.info = async () => ({
id: 12,
type: "eab",
} as any);
service.decryptAccessEntity = () => ({
kid: "kid-1",
});
service.update = async (param: any) => {
updateParam = param;
return param;
};
await service.updateAccess({
id: 12,
accountKey: "account-key",
});
assert.deepEqual(JSON.parse(updateParam.setting), {
kid: "kid-1",
accountKey: "account-key",
});
});
});
@@ -123,6 +123,25 @@ export class AccessService extends BaseService<AccessEntity> {
return await super.update(param);
}
async updateAccess(access: any) {
const oldEntity = await this.info(access.id);
if (oldEntity == null) {
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
}
const setting = this.decryptAccessEntity(oldEntity);
for (const key of Object.keys(access)) {
if (key === 'id') {
continue;
}
setting[key] = access[key];
}
return await this.update({
id: access.id,
type: oldEntity.type,
setting: JSON.stringify(setting),
});
}
async getSimpleInfo(id: number) {
const entity = await this.info(id);
if (entity == null) {
+5 -15
View File
@@ -8,7 +8,7 @@
"isolatedModules": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"inlineSourceMap":false,
"inlineSourceMap": false,
"sourceMap": false,
"noImplicitThis": true,
"noUnusedLocals": true,
@@ -17,25 +17,15 @@
"pretty": true,
"declaration": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": [ "./typings", "./node_modules/@types"],
"typeRoots": ["./typings", "./node_modules/@types"],
"outDir": "dist",
"rootDir": "src",
"composite": false,
"useDefineForClassFields": true,
"strict": false,
"resolveJsonModule": true,
"lib": ["ESNext", "DOM"],
"lib": ["ESNext", "DOM"]
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.json"
],
"exclude": [
"*.js",
"*.ts",
"dist",
"node_modules",
"test"
],
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.json"],
"exclude": ["*.js", "*.ts", "dist", "node_modules", "src/**/*.test.ts", "test"]
}
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/midway-flyway-js
+5 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.39.12",
"version": "1.39.13",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -13,6 +13,7 @@
"dev-build": "npm run build",
"test": "midway-bin test --ts -V",
"test1": "midway-bin test --ts -V -f test/blank.test.ts -t 'hash-check'",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"cov": "midway-bin cov --ts",
"prepublish": "npm run build",
"pub": "npm publish"
@@ -35,16 +36,18 @@
"@types/node": "^18",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"tslib": "^2.8.1",
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
}
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/plugin-cert
+9 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -13,14 +13,15 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/acme-client": "^1.39.12",
"@certd/basic": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/plugin-lib": "^1.39.12",
"@certd/acme-client": "^1.39.13",
"@certd/basic": "^1.39.13",
"@certd/pipeline": "^1.39.13",
"@certd/plugin-lib": "^1.39.13",
"psl": "^1.9.0",
"punycode.js": "^2.3.1"
},
@@ -30,13 +31,15 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.3.6",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.1.0",
"prettier": "^2.8.8",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/plugin-lib
+10 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -13,6 +13,7 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
@@ -22,10 +23,10 @@
"@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.11",
"@aws-sdk/client-s3": "^3.964.0",
"@certd/acme-client": "^1.39.12",
"@certd/basic": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/plus-core": "^1.39.12",
"@certd/acme-client": "^1.39.13",
"@certd/basic": "^1.39.13",
"@certd/pipeline": "^1.39.13",
"@certd/plus-core": "^1.39.13",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5",
@@ -49,13 +50,16 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.3.6",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.1.0",
"prettier": "^2.8.8",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
}
@@ -0,0 +1,55 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "./util.js";
describe("plugin-lib common util", () => {
it("builds cert domain getter input define with defaults", () => {
const define = createCertDomainGetterInputDefine();
expect(define.title).to.equal("当前证书域名");
expect(define.component.name).to.equal("cert-domains-getter");
expect(define.required).to.equal(true);
expect(define.template).to.equal(false);
expect(define.mergeScript).to.contain("form.cert");
});
it("allows overriding cert input key and props", () => {
const define = createCertDomainGetterInputDefine({
certInputKey: "customCert",
props: {
title: "自定义域名",
required: false,
},
});
expect(define.title).to.equal("自定义域名");
expect(define.required).to.equal(false);
expect(define.mergeScript).to.contain("form.customCert");
});
it("builds remote select input define with expected component options", () => {
const define = createRemoteSelectInputDefine({
title: "选择资源",
action: "ListResource",
typeName: "resource",
multi: false,
search: true,
watches: ["region"],
});
expect(define.title).to.equal("选择资源");
expect(define.required).to.equal(true);
expect(define.component).to.include({
name: "remote-select",
vModel: "value",
action: "ListResource",
typeName: "resource",
mode: "default",
multi: false,
search: true,
});
expect(define.component.watches).to.deep.equal(["certDomains", "accessId", "region"]);
});
});
+6 -16
View File
@@ -7,7 +7,7 @@
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"inlineSourceMap":false,
"inlineSourceMap": false,
"sourceMap": false,
"noImplicitThis": true,
"noUnusedLocals": true,
@@ -16,27 +16,17 @@
"pretty": true,
"declaration": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": [ "./typings", "./node_modules/@types"],
"typeRoots": ["./typings", "./node_modules/@types"],
"outDir": "dist",
"rootDir": "src",
"composite": false,
"useDefineForClassFields": true,
"strict": false,
// "sourceMap": true,
// "sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": false,
"lib": ["ESNext", "DOM"],
"lib": ["ESNext", "DOM"]
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.json"
],
"exclude": [
"*.js",
"*.ts",
"dist",
"node_modules",
"test"
],
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.json"],
"exclude": ["*.js", "*.ts", "dist", "node_modules", "src/**/*.test.ts", "test"]
}
+15
View File
@@ -3,6 +3,21 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Bug Fixes
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
### Performance Improvements
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
* **用户资料:** 新增手机号邮箱绑定功能 ([e0eb0e2](https://github.com/certd/certd/commit/e0eb0e21f6dae24b639c944f9aba2c90496ab1c0))
* 域名注册过期时间获取再次优化 ([91a1b97](https://github.com/certd/certd/commit/91a1b9755066bf280e194dabf7c3a9f936e2643f))
* **证书流水线:** 添加批量更新证书申请参数功能 ([63be1c1](https://github.com/certd/certd/commit/63be1c1cbd9b09a3b48f26130c296b1cedcca1ac))
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
* **domain:** 添加域名过期时间同步进度显示功能 ([9d2937d](https://github.com/certd/certd/commit/9d2937dd4b14ffab73e9b096edd2aa8539811182))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Bug Fixes
+7 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.39.12",
"version": "1.39.13",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -12,7 +12,8 @@
"debug:force": "vite --force --mode debug",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=40960 vite build ",
"dev-build": "echo 1",
"test:unit": "vitest",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"test:vue": "vitest run",
"serve": "vite preview",
"preview": "vite preview",
"pretty-quick": "pretty-quick",
@@ -61,7 +62,6 @@
"cos-js-sdk-v5": "^1.7.0",
"cron-parser": "^4.9.0",
"cropperjs": "^1.6.1",
"cross-env": "^7.0.3",
"cssnano": "^7.0.6",
"dayjs": "^1.11.7",
"defu": "^6.1.4",
@@ -106,8 +106,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/lib-iframe": "^1.39.13",
"@certd/pipeline": "^1.39.13",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -126,6 +126,7 @@
"autoprefixer": "^10.4.21",
"caller-path": "^4.0.0",
"chai": "^5.1.0",
"cross-env": "^7.0.3",
"dependency-cruiser": "^16.2.3",
"dot": "^1.1.3",
"eslint": "8.57.0",
@@ -135,6 +136,7 @@
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.23.0",
"esmock": "^2.7.5",
"less": "^4.2.0",
"less-loader": "^12.2.0",
"postcss": "^8.4.35",
@@ -1,17 +1,18 @@
<template>
<div class="flex">
<a-input :value="valueRef" placeholder="请输入图片验证码" autocomplete="off" @update:value="onChange">
<a-input :value="valueRef" :placeholder="t('certd.captcha.inputImageCode')" autocomplete="off" @update:value="onChange">
<template #prefix>
<fs-icon icon="ion:image-outline"></fs-icon>
</template>
</a-input>
<div class="input-right pointer" title="点击刷新">
<div class="input-right pointer" :title="t('certd.captcha.refresh')">
<img class="image-code" :src="imageCodeSrc" @click="resetImageCode" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{
modelValue: any;
@@ -20,6 +21,7 @@ const props = defineProps<{
defineOptions({
name: "ImageCaptcha",
});
const { t } = useI18n();
const emit = defineEmits(["update:modelValue", "change"]);
const valueRef = ref("");
@@ -4,8 +4,8 @@
<div class="sweep-animation"></div>
<div class="box-content">
<div class="box-icon"></div>
<span v-if="modelValue == null" class="status-text">点击进行验证</span>
<span v-else class="status-text">验证成功</span>
<span v-if="modelValue == null" class="status-text">{{ t("certd.captcha.clickToVerify") }}</span>
<span v-else class="status-text">{{ t("certd.captcha.verifySuccess") }}</span>
</div>
</div>
</div>
@@ -13,8 +13,10 @@
<script setup lang="ts">
import { notification } from "ant-design-vue";
import { ref, Ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { loadScript } from "vue-plugin-load-script";
const { t } = useI18n();
const loaded = ref(false);
async function loadCaptchaScript() {
// js
@@ -56,7 +58,7 @@ function callback(res: { ret: number; ticket: string; randstr: string; errorCode
if (res.errorCode && res.errorCode > 0) {
notification.error({
message: `验证码验证失败:${res.errorMessage || res.errorCode}`,
message: t("certd.captcha.verifyFailed", { message: res.errorMessage || res.errorCode }),
});
}
@@ -83,13 +85,13 @@ function loadErrorCallback(error: any) {
// errorMessage: "jsload_error",
// });
notification.error({
message: `验证码加载失败:${error?.message || error}`,
message: t("certd.captcha.loadFailed", { message: error?.message || error }),
});
}
async function triggerCaptcha() {
if (!loaded.value) {
notification.error({
message: "验证码还未加载完成,请稍后再试",
message: t("certd.captcha.notLoaded"),
});
return;
}
@@ -4,7 +4,7 @@
<!-- <td class="domain">-->
<!-- {{ props.domain }}-->
<!-- </td>-->
<td class="host-record" :title="'域名:' + props.domain">
<td class="host-record" :title="t('certd.verifyPlan.domainTitle', { domain: props.domain })">
<fs-copyable v-model="cnameRecord.hostRecord"></fs-copyable>
</td>
<td style="text-align: center">CNAME</td>
@@ -16,17 +16,17 @@
<a-tooltip v-if="cnameRecord.error" :title="cnameRecord.error">
<fs-icon class="ml-5 color-red" icon="ion:warning-outline"></fs-icon>
</a-tooltip>
<a-tooltip v-if="cnameRecord.status === 'valid'" title="重置校验状态,重新校验">
<a-tooltip v-if="cnameRecord.status === 'valid'" :title="t('certd.verifyPlan.resetStatusTooltip')">
<fs-icon class="ml-2 color-yellow text-md pointer" icon="solar:undo-left-square-bold" @click="resetStatus"></fs-icon>
</a-tooltip>
</td>
<td class="center">
<template v-if="cnameRecord.status !== 'valid'">
<a-button type="primary" size="small" :loading="loading" @click="doVerify">点击验证</a-button>
<a-button type="primary" size="small" :loading="loading" @click="doVerify">{{ t("certd.verifyPlan.clickToValidate") }}</a-button>
<cname-tip :record="cnameRecord"></cname-tip>
</template>
<div v-else class="helper" title="后续自动申请证书需要">不要删除CNAME</div>
<div v-else class="helper" :title="t('certd.verifyPlan.keepCnameTitle')">{{ t("certd.verifyPlan.keepCname") }}</div>
</td>
</tr>
</tbody>
@@ -35,18 +35,20 @@
<script lang="ts" setup>
import { CnameRecord, GetByDomain } from "/@/components/plugins/cert/domains-verify-plan-editor/api";
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { dict } from "@fast-crud/fast-crud";
import * as api from "./api.js";
import CnameTip from "./cname-tip.vue";
import { Modal } from "ant-design-vue";
import { utils } from "/@/utils/index.js";
const { t } = useI18n();
const statusDict = dict({
data: [
{ label: "待设置CNAME", value: "cname", color: "warning" },
{ label: "验证中", value: "validating", color: "blue" },
{ label: "验证成功", value: "valid", color: "green" },
{ label: "验证失败", value: "failed", color: "red" },
{ label: "验证超时", value: "timeout", color: "red" },
{ label: t("certd.verifyPlan.status.pendingCname"), value: "cname", color: "warning" },
{ label: t("certd.verifyPlan.status.validating"), value: "validating", color: "blue" },
{ label: t("certd.verifyPlan.status.valid"), value: "valid", color: "green" },
{ label: t("certd.verifyPlan.status.failed"), value: "failed", color: "red" },
{ label: t("certd.verifyPlan.status.timeout"), value: "timeout", color: "red" },
],
});
@@ -125,8 +127,8 @@ async function doVerify() {
async function resetStatus() {
Modal.confirm({
title: "重置状态",
content: "确定要重置校验状态吗?",
title: t("certd.verifyPlan.resetStatus"),
content: t("certd.verifyPlan.confirmResetStatus"),
onOk: async () => {
await api.ResetStatus(cnameRecord.value.id);
await loadRecord();
@@ -2,17 +2,17 @@
<a-tooltip :overlay-style="{ maxWidth: '400px' }">
<template #title>
<div>
<div>多试几次如果仍然无法验证通过请按如下步骤排查问题</div>
<div>1. 解析记录应该添加在{{ record.domain }}域名下</div>
<div>2. 要添加的是CNAME类型的记录不是TXT</div>
<div>3. 核对记录值是否是:{{ record.recordValue }}</div>
<div>{{ t("certd.verifyPlan.cnameTip.intro") }}</div>
<div>{{ t("certd.verifyPlan.cnameTip.step1", { domain: record.domain }) }}</div>
<div>{{ t("certd.verifyPlan.cnameTip.step2") }}</div>
<div>{{ t("certd.verifyPlan.cnameTip.step3", { value: record.recordValue }) }}</div>
<div>
4. 在验证中状态下运行下面的命令,查看cname和txt解析是否正确
{{ t("certd.verifyPlan.cnameTip.step4") }}
<fs-copyable :style="{ color: '#52c41a' }" :model-value="nslookupCmd"></fs-copyable>
或者
{{ t("certd.verifyPlan.cnameTip.or") }}
<fs-copyable :style="{ color: '#52c41a' }" :model-value="digCmd"></fs-copyable>
</div>
<div>5. 如果以上检查都没有问题则可能是DNS解析生效时间比较慢某些提供商延迟可能高达几个小时</div>
<div>{{ t("certd.verifyPlan.cnameTip.step5") }}</div>
</div>
</template>
<fs-icon class="ml-5 pointer" icon="mingcute:question-line"></fs-icon>
@@ -21,9 +21,11 @@
<script lang="ts" setup>
import { computed } from "vue";
import { useI18n } from "vue-i18n";
const props = defineProps<{
record: any;
}>();
const { t } = useI18n();
const nslookupCmd = computed(() => {
return `nslookup -q=txt _acme-challenge.${props.record.domain}`;
@@ -2,11 +2,11 @@
<table class="cname-verify-plan">
<thead>
<tr>
<td style="width: 160px">主机记录</td>
<td style="width: 100px; text-align: center">记录类型</td>
<td style="width: 250px">请设置CNAME记录验证成功以后不要删除</td>
<td style="width: 120px" class="center">状态</td>
<td style="width: 90px" class="center">操作</td>
<td style="width: 160px">{{ t("certd.verifyPlan.hostRecord") }}</td>
<td style="width: 100px; text-align: center">{{ t("certd.verifyPlan.recordType") }}</td>
<td style="width: 250px">{{ t("certd.verifyPlan.setCnameRecord") }}</td>
<td style="width: 120px" class="center">{{ t("certd.status") }}</td>
<td style="width: 90px" class="center">{{ t("certd.verifyPlan.operation") }}</td>
</tr>
</thead>
<template v-for="key in domains" :key="key">
@@ -19,11 +19,14 @@
import { CnameRecord } from "/@/components/plugins/cert/domains-verify-plan-editor/api";
import CnameRecordInfo from "/@/components/plugins/cert/domains-verify-plan-editor/cname-record-info.vue";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
defineOptions({
name: "CnameVerifyPlan",
});
const { t } = useI18n();
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
@@ -2,10 +2,10 @@
<table class="http-verify-plan">
<thead>
<tr>
<td style="width: 160px">网站域名</td>
<td style="width: 100px; text-align: center">上传方式</td>
<td style="width: 150px">上传授权</td>
<td style="width: 200px">网站根目录路径</td>
<td style="width: 160px">{{ t("certd.verifyPlan.websiteDomain") }}</td>
<td style="width: 100px; text-align: center">{{ t("certd.verifyPlan.uploadMethod") }}</td>
<td style="width: 150px">{{ t("certd.verifyPlan.uploadAccess") }}</td>
<td style="width: 200px">{{ t("certd.verifyPlan.websiteRootPath") }}</td>
</tr>
</thead>
<tbody v-if="records" class="http-record-body">
@@ -21,7 +21,7 @@
<access-selector v-model="item.httpUploaderAccess" :type="item.httpUploaderType" @change="onRecordChange"></access-selector>
</td>
<td>
<a-input v-model:value="item.httpUploadRootDir" placeholder="网站根目录,如:/www/wwwroot" @change="onRecordChange"></a-input>
<a-input v-model:value="item.httpUploadRootDir" :placeholder="t('certd.verifyPlan.websiteRootPlaceholder')" @change="onRecordChange"></a-input>
</td>
</tr>
</template>
@@ -31,6 +31,7 @@
<script lang="ts" setup>
import { Ref, ref, watch, nextTick } from "vue";
import { useI18n } from "vue-i18n";
import { HttpRecord } from "/@/components/plugins/cert/domains-verify-plan-editor/type";
import { dict } from "@fast-crud/fast-crud";
import { Dicts } from "/@/components/plugins/lib/dicts";
@@ -39,6 +40,8 @@ defineOptions({
name: "HttpVerifyPlan",
});
const { t } = useI18n();
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
@@ -5,7 +5,7 @@
<div class="plan-box bg-white dark:bg-neutral-700">
<div class="fullscreen-button pointer flex-center" @click="fullscreen = !fullscreen">
<span v-if="!fullscreen" style="font-size: 10px" class="flex-center">
这里可以放大
{{ t("certd.verifyPlan.expandTip") }}
<fs-icon icon="ion:arrow-forward-outline"></fs-icon>
</span>
<fs-icon :icon="fullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'"></fs-icon>
@@ -13,9 +13,9 @@
<table class="plan-table">
<thead>
<tr>
<th style="min-width: 100px">主域名</th>
<th>验证方式</th>
<th>验证计划</th>
<th style="min-width: 100px">{{ t("certd.verifyPlan.mainDomain") }}</th>
<th>{{ t("certd.verifyPlan.challengeType") }}</th>
<th>{{ t("certd.verifyPlan.challengePlan") }}</th>
</tr>
</thead>
<tbody>
@@ -30,13 +30,13 @@
<div class="plan">
<div v-if="item.type === 'dns'" class="plan-dns">
<div class="form-item">
<span class="label">DNS类型</span>
<span class="label">{{ t("certd.verifyPlan.dnsType") }}:</span>
<span class="input">
<fs-dict-select
v-model:value="item.dnsProviderType"
size="small"
:dict="dnsProviderTypeDict"
placeholder="DNS提供商"
:placeholder="t('certd.verifyPlan.dnsProvider')"
@change="onPlanChanged"
@selected-change="onDnsProviderChange(item, $event)"
></fs-dict-select>
@@ -44,9 +44,9 @@
</div>
<a-divider type="vertical" />
<div class="form-item">
<span class="label">DNS授权</span>
<span class="label">{{ t("certd.verifyPlan.dnsAccess") }}:</span>
<span class="input">
<access-selector v-model="item.dnsProviderAccessId" size="small" :type="item.dnsProviderAccessType || item.dnsProviderType" placeholder="请选择" @change="onPlanChanged"></access-selector>
<access-selector v-model="item.dnsProviderAccessId" size="small" :type="item.dnsProviderAccessType || item.dnsProviderType" :placeholder="t('certd.verifyPlan.pleaseSelect')" @change="onPlanChanged"></access-selector>
</span>
</div>
</div>
@@ -55,7 +55,7 @@
</div>
<div v-if="item.type === 'http'" class="plan-http">
<http-verify-plan v-model="item.httpVerifyPlan" @change="onPlanChanged" />
<div class="helper">证书颁发机构将请求 https://yourdomain/.well-known/acme-challenge/xxxxxx </div>
<div class="helper">{{ t("certd.verifyPlan.httpHelper") }}</div>
</div>
</div>
</td>
@@ -72,6 +72,7 @@
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { dict, FsDictSelect } from "@fast-crud/fast-crud";
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import CnameVerifyPlan from "./cname-verify-plan.vue";
@@ -84,17 +85,19 @@ defineOptions({
name: "DomainsVerifyPlanEditor",
});
const { t } = useI18n();
const challengeTypeOptions = ref<any[]>([
{
label: "DNS验证",
label: t("certd.verifyPlan.dnsChallenge"),
value: "dns",
},
{
label: "CNAME验证",
label: t("certd.verifyPlan.cnameChallenge"),
value: "cname",
},
{
label: "HTTP验证",
label: t("certd.verifyPlan.httpChallenge"),
value: "http",
},
]);
@@ -1,5 +1,6 @@
import Validator from "async-validator";
import { DomainsVerifyPlanInput } from "./type";
import { $t } from "/@/locales";
function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
if (value == null) {
@@ -13,7 +14,7 @@ function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
for (const subDomain of subDomains) {
const plan = value[domain].cnameVerifyPlan[subDomain];
if (plan.status !== "valid") {
throw new Error(`域名${subDomain}的CNAME未验证通过,请先设置CNAME记录,点击验证按钮`);
throw new Error($t("certd.verifyPlan.errors.cnameNotValid", { domain: subDomain }));
}
}
}
@@ -22,7 +23,7 @@ function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
for (const item of domains) {
//如果有通配符域名则不允许使用http校验
if (item.startsWith("*.")) {
throw new Error(`域名${item}为通配符域名,不支持HTTP校验`);
throw new Error($t("certd.verifyPlan.errors.wildcardNotSupportHttp", { domain: item }));
}
}
@@ -31,19 +32,19 @@ function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
for (const subDomain of subDomains) {
const plan = value[domain].httpVerifyPlan[subDomain];
if (!plan.httpUploaderType) {
throw new Error(`域名${subDomain}的上传方式必须填写`);
throw new Error($t("certd.verifyPlan.errors.uploadMethodRequired", { domain: subDomain }));
}
if (!plan.httpUploaderAccess) {
throw new Error(`域名${subDomain}的上传授权信息必须填写`);
throw new Error($t("certd.verifyPlan.errors.uploadAccessRequired", { domain: subDomain }));
}
if (!plan.httpUploadRootDir) {
throw new Error(`域名${subDomain}的网站根路径必须填写`);
throw new Error($t("certd.verifyPlan.errors.websiteRootRequired", { domain: subDomain }));
}
}
}
} else if (type === "dns") {
if (!value[domain].dnsProviderType || !value[domain].dnsProviderAccessId) {
throw new Error(`DNS模式下,域名${domain}的DNS类型和授权信息必须填写`);
throw new Error($t("certd.verifyPlan.errors.dnsProviderRequired", { domain }));
}
}
}
@@ -1,7 +1,7 @@
<template>
<div class="api-test">
<div>
<fs-button :loading="loading" type="primary" text="测试" icon="ion:refresh-outline" @click="doTest"></fs-button>
<fs-button :loading="loading" type="primary" :text="t('certd.pluginCommon.test')" icon="ion:refresh-outline" @click="doTest"></fs-button>
</div>
<div class="helper" :class="{ error: hasError }">
@@ -12,6 +12,7 @@
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { ref, inject } from "vue";
import { useI18n } from "vue-i18n";
import { Form } from "ant-design-vue";
import { getInputFromForm } from "./utils";
@@ -19,6 +20,8 @@ defineOptions({
name: "ApiTest",
});
const { t } = useI18n();
const fromType: any = inject("getFromType");
const getScope: any = inject("get:scope");
const getPluginType: any = inject("get:plugin:type", () => {
@@ -61,14 +64,14 @@ const doTest = async () => {
{
onError(err: any) {
hasError.value = true;
message.value = `错误:${err.message}`;
message.value = t("certd.pluginCommon.errorWithMessage", { message: err.message });
},
showErrorNotify: false,
}
);
message.value = "测试请求成功";
message.value = t("certd.pluginCommon.testRequestSuccess");
if (res) {
message.value += `,返回:${JSON.stringify(res)}`;
message.value += t("certd.pluginCommon.responseSuffix", { response: JSON.stringify(res) });
}
} finally {
loading.value = false;
@@ -9,6 +9,7 @@
<script setup lang="ts">
import { inject, ref, watch } from "vue";
import { useI18n } from "/@/locales";
defineOptions({
name: "CertDomainsGetter",
@@ -24,6 +25,7 @@ const emit = defineEmits<{
}>();
const pipeline: any = inject("pipeline");
const { t } = useI18n();
function findStepFromPipeline(targetStepId: string) {
for (const stage of pipeline.value.stages) {
@@ -40,7 +42,7 @@ function findStepFromPipeline(targetStepId: string) {
const errorRef = ref("");
function getStepIdFromInputKey(inputKey: string) {
if (!inputKey) {
errorRef.value = "请先选择域名证书";
errorRef.value = t("certd.pluginCommon.selectCertFirst");
return;
}
return inputKey.split(".")[1];
@@ -49,7 +51,7 @@ function getDomainFromPipeline(inputKey: string) {
let targetStepId = getStepIdFromInputKey(inputKey);
let certStep = findStepFromPipeline(targetStepId);
if (!certStep) {
errorRef.value = "找不到目标步骤,请先选择域名证书";
errorRef.value = t("certd.pluginCommon.targetStepNotFound");
return;
}
@@ -58,7 +60,7 @@ function getDomainFromPipeline(inputKey: string) {
targetStepId = getStepIdFromInputKey(firstLevelValue);
certStep = findStepFromPipeline(targetStepId);
if (!certStep) {
errorRef.value = "找不到目标步骤,请先选择域名证书";
errorRef.value = t("certd.pluginCommon.targetStepNotFound");
return;
}
}
@@ -17,12 +17,12 @@
<template v-if="search">
<div class="flex w-full items-center justify-between flex-wrap" style="padding: 4px 8px">
<div class="flex-1 flex flex-row items-center">
<a-input ref="inputRef" v-model:value="searchKeyRef" class="flex-1" allow-clear placeholder="这里可以搜索域名(数据来自“设置->域名管理”),您也可以直接在上面输入框输入" @keydown.enter="doSearch" />
<fs-button type="primary" class="m-1" :loading="loading" icon="mingcute:search-2-line" @click="doSearch"> 查询 </fs-button>
<a-input ref="inputRef" v-model:value="searchKeyRef" class="flex-1" allow-clear :placeholder="t('certd.pluginCommon.domainSearchPlaceholder')" @keydown.enter="doSearch" />
<fs-button type="primary" class="m-1" :loading="loading" icon="mingcute:search-2-line" @click="doSearch">{{ t("certd.pluginCommon.search") }}</fs-button>
</div>
<div class="manager flex flex-row items-center">
<fs-button type="primary" class="m-1" icon="mingcute:vip-1-line" @click="openDomainImportDialog">导入域名</fs-button>
<fs-button class="m-1" type="primary" icon="carbon:gui-management" @click="openDomainManager">管理域名</fs-button>
<fs-button type="primary" class="m-1" icon="mingcute:vip-1-line" @click="openDomainImportDialog">{{ t("certd.pluginCommon.importDomain") }}</fs-button>
<fs-button class="m-1" type="primary" icon="carbon:gui-management" @click="openDomainManager">{{ t("certd.pluginCommon.manageDomain") }}</fs-button>
</div>
</div>
<div v-if="hasError" class="helper p-2" :class="{ error: hasError }">
@@ -47,7 +47,7 @@
</template>
</a-select>
<div class="ml-5">
<fs-button :loading="loading" title="刷新我的域名列表" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
<fs-button :loading="loading" :title="t('certd.pluginCommon.refreshMyDomains')" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
</div>
</div>
<div class="helper" :class="{ error: hasError }">
@@ -57,6 +57,7 @@
</template>
<script setup lang="ts">
import { computed, defineComponent, onMounted, ref, Ref, useAttrs } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { Dicts } from "../lib/dicts";
import { request } from "/@/api/service";
@@ -67,6 +68,8 @@ defineOptions({
name: "DomainSelector",
});
const { t } = useI18n();
const VNodes = defineComponent({
props: {
vnodes: {
@@ -0,0 +1,106 @@
<template>
<div class="refresh-input">
<div class="refresh-input-line">
<a-input class="refresh-input-control" :value="value" :placeholder="placeholder" allow-clear @update:value="emit('update:value', $event)"></a-input>
<fs-button :loading="loading" type="primary" :text="buttonText" :icon="icon" @click="doRefresh"></fs-button>
</div>
<div class="helper" :class="{ error: hasError }">
{{ message }}
</div>
</div>
</template>
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { computed, inject, ref } from "vue";
import { Form } from "ant-design-vue";
import { getInputFromForm } from "./utils";
defineOptions({
name: "RefreshInput",
});
type RefreshInputProps = ComponentPropsType & {
buttonText?: string;
icon?: string;
placeholder?: string;
successMessage?: string;
};
const fromType: any = inject("getFromType");
const getScope: any = inject("get:scope");
const getPluginType: any = inject("get:plugin:type", () => {
return "access";
});
const formItemContext = Form.useInjectFormItemContext();
const props = defineProps<RefreshInputProps>();
const emit = defineEmits<{
"update:value": [value: string];
}>();
const loading = ref(false);
const message = ref("");
const hasError = ref(false);
const action = computed(() => props.action);
const buttonText = computed(() => props.buttonText || "刷新");
const icon = computed(() => props.icon || "ion:refresh-outline");
const placeholder = computed(() => props.placeholder || "");
const successMessage = computed(() => props.successMessage || "刷新成功,请保存配置");
const doRefresh = async () => {
if (loading.value) {
return;
}
if (!action.value) {
hasError.value = true;
message.value = "缺少刷新动作配置";
return;
}
formItemContext.onFieldChange();
const { form } = getScope();
const pluginType = getPluginType();
const { input, record } = getInputFromForm(form, pluginType);
loading.value = true;
message.value = "";
hasError.value = false;
try {
const res = await doRequest(
{
type: pluginType,
typeName: form.type,
action: action.value,
input,
record,
fromType,
},
{
onError(err: any) {
hasError.value = true;
message.value = err.message;
},
showErrorNotify: false,
}
);
emit("update:value", res);
message.value = successMessage.value;
} finally {
loading.value = false;
}
};
</script>
<style lang="less" scoped>
.refresh-input-line {
display: flex;
gap: 8px;
align-items: center;
}
.refresh-input-control {
flex: 1;
}
</style>
@@ -4,7 +4,7 @@
<a-auto-complete class="remote-auto-complete-input" :filter-option="filterOption" :options="optionsRef" :value="value" v-bind="attrs" @click="onClick" @update:value="emit('update:value', $event)">
</a-auto-complete>
<div class="ml-5">
<fs-button :loading="loading" title="刷新选项" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
<fs-button :loading="loading" :title="t('certd.pluginCommon.refreshOptions')" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
</div>
</div>
<div class="helper" :class="{ error: hasError }">
@@ -15,6 +15,7 @@
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { defineComponent, inject, ref, useAttrs, watch, Ref } from "vue";
import { useI18n } from "vue-i18n";
import { PluginDefine } from "@certd/pipeline";
import { getInputFromForm } from "./utils";
@@ -22,6 +23,8 @@ defineOptions({
name: "RemoteAutoComplete",
});
const { t } = useI18n();
const props = defineProps<
{
watches?: string[];
@@ -93,16 +96,16 @@ const getOptions = async () => {
{
onError(err: any) {
hasError.value = true;
message.value = `获取选项出错:${err.message}`;
message.value = t("certd.pluginCommon.getOptionsError", { message: err.message });
},
showErrorNotify: false,
}
);
const list = res?.list || res || [];
if (list.length > 0) {
message.value = "获取数据成功,请从下拉框中选择";
message.value = t("certd.pluginCommon.getDataSuccessSelect");
} else {
message.value = "获取数据成功,没有数据";
message.value = t("certd.pluginCommon.getDataSuccessEmpty");
}
optionsRef.value = list;
@@ -7,6 +7,7 @@
<script setup lang="ts">
import { doRequest } from "/@/components/plugins/lib";
import { inject, ref, useAttrs } from "vue";
import { useI18n } from "vue-i18n";
import { useFormWrapper } from "@fast-crud/fast-crud";
import { notification } from "ant-design-vue";
import { getInputFromForm } from "./utils";
@@ -15,6 +16,7 @@ defineOptions({
name: "RemoteInput",
});
const { openCrudFormDialog } = useFormWrapper();
const { t } = useI18n();
const props = defineProps<{
modelValue: string;
title: string;
@@ -53,7 +55,7 @@ async function openDialog() {
saveRemind: false,
},
afterSubmit() {
notification.success({ message: "操作成功" });
notification.success({ message: t("certd.operationSuccess") });
},
async doSubmit({ form }: any) {
return await doPluginFormSubmit(form);
@@ -5,12 +5,12 @@
<template #dropdownRender="{ menuNode: menu }">
<template v-if="search">
<div class="flex w-full" style="padding: 4px 8px">
<a-input ref="inputRef" v-model:value="searchKeyRef" class="flex-1" allow-clear placeholder="查询关键字" @keydown.enter="doSearch" />
<a-input ref="inputRef" v-model:value="searchKeyRef" class="flex-1" allow-clear :placeholder="t('certd.pluginCommon.searchKeyword')" @keydown.enter="doSearch" />
<a-button class="ml-2" :loading="loading" type="text" @click="doSearch">
<template #icon>
<search-outlined />
</template>
查询
{{ t("certd.pluginCommon.search") }}
</a-button>
</div>
<div v-if="hasError" class="helper p-2" :class="{ error: hasError }">
@@ -26,7 +26,7 @@
</template>
</a-select>
<div class="ml-5 flex flex-row no-wrap">
<fs-button :loading="loading" title="刷新选项" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
<fs-button :loading="loading" :title="t('certd.pluginCommon.refreshOptions')" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
<UploadCert v-if="uploadCert" class="ml-5" v-bind="uploadCert" @submit="refreshOptions"></UploadCert>
</div>
</div>
@@ -38,6 +38,7 @@
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { defineComponent, inject, ref, useAttrs, watch, Ref } from "vue";
import { useI18n } from "vue-i18n";
import { PluginDefine } from "@certd/pipeline";
import { getInputFromForm } from "./utils";
import UploadCert from "./upload-cert.vue";
@@ -47,6 +48,8 @@ defineOptions({
name: "RemoteSelect",
});
const { t } = useI18n();
const selectRef = ref(null);
const VNodes = defineComponent({
@@ -161,7 +164,7 @@ const getOptions = async () => {
{
onError(err: any) {
hasError.value = true;
message.value = `获取选项出错:${err.message}`;
message.value = t("certd.pluginCommon.getOptionsError", { message: err.message });
optionsRef.value = [];
},
showErrorNotify: false,
@@ -169,9 +172,9 @@ const getOptions = async () => {
);
let list = res?.list || res || [];
if (list.length > 0) {
message.value = "获取数据成功,请从下拉框中选择";
message.value = t("certd.pluginCommon.getDataSuccessSelect");
} else {
message.value = "获取数据成功,没有数据";
message.value = t("certd.pluginCommon.getDataSuccessEmpty");
}
list = list.map((item: any) => {
return {
@@ -3,7 +3,7 @@
<div class="flex flex-row">
<a-tree-select class="remote-tree-select-input" :tree-data="optionsRef" :value="value" tree-checkable allow-clear v-bind="attrs" @click="onClick" @update:value="emit('update:value', $event)"> </a-tree-select>
<div class="ml-5">
<fs-button :loading="loading" title="刷新选项" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
<fs-button :loading="loading" :title="t('certd.pluginCommon.refreshOptions')" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
</div>
</div>
<div class="helper" :class="{ error: hasError }">
@@ -14,6 +14,7 @@
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { defineComponent, inject, ref, useAttrs, watch, Ref } from "vue";
import { useI18n } from "vue-i18n";
import { PluginDefine } from "@certd/pipeline";
import { getInputFromForm } from "./utils";
@@ -21,6 +22,8 @@ defineOptions({
name: "RemoteTreeSelect",
});
const { t } = useI18n();
const props = defineProps<
{
watches: string[];
@@ -104,16 +107,16 @@ const getOptions = async () => {
{
onError(err: any) {
hasError.value = true;
message.value = `获取选项出错:${err.message}`;
message.value = t("certd.pluginCommon.getOptionsError", { message: err.message });
},
showErrorNotify: false,
}
);
const list = res?.list || res || [];
if (list.length > 0) {
message.value = "获取数据成功,请从下拉框中选择";
message.value = t("certd.pluginCommon.getDataSuccessSelect");
} else {
message.value = "获取数据成功,没有数据";
message.value = t("certd.pluginCommon.getDataSuccessEmpty");
}
optionsRef.value = list;
pagerRef.value.total = list.length;
@@ -1,12 +1,13 @@
<template>
<div class="upload-cert">
<fs-button v-model:loading="loading" type="primary" text="上传" v-bind="props.button" @click="openUploadCertDialog"></fs-button>
<fs-button v-model:loading="loading" type="primary" :text="t('certd.pluginCommon.upload')" v-bind="props.button" @click="openUploadCertDialog"></fs-button>
</div>
</template>
<script lang="ts" setup>
import { message } from "ant-design-vue";
import { useFormDialog } from "../../../use/use-dialog";
import { computed, inject, ref } from "vue";
import { useI18n } from "vue-i18n";
import { doRequest } from "../lib";
import { getInputFromForm } from "./utils";
import { UploadCertProps } from "./types";
@@ -14,6 +15,7 @@ import { merge } from "lodash-es";
const props = defineProps<UploadCertProps>();
const loading = ref(false);
const { t } = useI18n();
const emit = defineEmits(["submit"]);
const { openFormDialog } = useFormDialog();
@@ -28,18 +30,18 @@ const getScope: any = inject("get:scope", () => {
const getPluginType: any = inject("get:plugin:type", () => {
return "plugin";
});
const title = computed(() => props.title || "上传证书");
const title = computed(() => props.title || t("certd.pluginCommon.uploadCert"));
function openUploadCertDialog() {
const columns = merge(
{
certName: {
title: "证书名称",
title: t("certd.pluginCommon.certName"),
form: {
component: {
name: "a-input",
vModel: "value",
},
helper: "上传后证书显示名称",
helper: t("certd.pluginCommon.certNameHelper"),
},
},
},
@@ -49,7 +51,7 @@ function openUploadCertDialog() {
title: title.value,
columns: {
certName: {
title: "证书名称",
title: t("certd.pluginCommon.certName"),
form: {
component: {
name: "a-input",
@@ -84,7 +86,7 @@ function openUploadCertDialog() {
showErrorNotify: true,
}
);
message.success("上传成功");
message.success(t("certd.pluginCommon.uploadSuccess"));
emit("submit");
} finally {
loading.value = false;
@@ -12,6 +12,7 @@ import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import InputPassword from "./common/input-password.vue";
import CertInfoUpdater from "/@/views/certd/pipeline/cert-upload/index.vue";
import ApiTest from "./common/api-test.vue";
import RefreshInput from "./common/refresh-input.vue";
import ParamsShow from "./common/params-show.vue";
export * from "./cert/index.js";
export default {
@@ -23,6 +24,7 @@ export default {
app.component("CertInfoUpdater", CertInfoUpdater);
app.component("ApiTest", ApiTest);
app.component("RefreshInput", RefreshInput);
app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter);
app.component("RemoteAutoComplete", RemoteAutoComplete);
@@ -1,38 +1,57 @@
import { dict } from "@fast-crud/fast-crud";
import { $t } from "/@/locales";
export const Dicts = {
sslProviderDict: dict({
function createChallengeTypeDict() {
return dict({
data: [
{ value: "letsencrypt", label: "Lets Encrypt" },
{ value: "zerossl", label: "ZeroSSL" },
{ value: "dns", label: $t("certd.verifyPlan.dnsChallenge"), color: "green" },
{ value: "cname", label: $t("certd.verifyPlan.cnameProxyChallenge"), color: "blue" },
{ value: "http", label: $t("certd.verifyPlan.httpChallenge"), color: "yellow" },
],
}),
challengeTypeDict: dict({
data: [
{ value: "dns", label: "DNS校验", color: "green" },
{ value: "cname", label: "CNAME代理校验", color: "blue" },
{ value: "http", label: "HTTP校验", color: "yellow" },
],
}),
dnsProviderTypeDict: dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
}),
uploaderTypeDict: dict({
});
}
function createUploaderTypeDict() {
return dict({
data: [
{ label: "SFTP", value: "sftp" },
{ label: "SCP", value: "scp" },
{ label: "FTP", value: "ftp" },
{ label: "阿里云OSS", value: "alioss" },
{ label: "腾讯云COS", value: "tencentcos" },
{ label: "七牛OSS", value: "qiniuoss" },
{ label: $t("certd.verifyPlan.uploader.aliyunOss"), value: "alioss" },
{ label: $t("certd.verifyPlan.uploader.tencentCos"), value: "tencentcos" },
{ label: $t("certd.verifyPlan.uploader.qiniuOss"), value: "qiniuoss" },
{ label: "S3/Minio", value: "s3" },
{ label: "SSH(已废弃,请选择SFTP方式)", value: "ssh", disabled: true },
{ label: $t("certd.verifyPlan.uploader.sshDeprecated"), value: "ssh", disabled: true },
],
}),
domainFromTypeDict: dict({
});
}
function createDomainFromTypeDict() {
return dict({
data: [
{ value: "manual", label: "手动" },
{ value: "auto", label: "自动" },
{ value: "manual", label: $t("certd.verifyPlan.domainFrom.manual") },
{ value: "auto", label: $t("certd.verifyPlan.domainFrom.auto") },
],
});
}
export const Dicts = {
sslProviderDict: dict({
data: [
{ value: "letsencrypt", label: "Let's Encrypt" },
{ value: "zerossl", label: "ZeroSSL" },
],
}),
get challengeTypeDict() {
return createChallengeTypeDict();
},
dnsProviderTypeDict: dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
}),
get uploaderTypeDict() {
return createUploaderTypeDict();
},
get domainFromTypeDict() {
return createDomainFromTypeDict();
},
};
@@ -1,873 +1,46 @@
//注意: @符号是保留字符,需要用{'@'}替换, AI请务必提醒我修改
import common from "./certd/common";
import navigation from "./certd/navigation";
import dashboard from "./certd/dashboard";
import pipeline from "./certd/pipeline";
import history from "./certd/history";
import monitor from "./certd/monitor";
import certdomain from "./certd/cert-domain";
import cname from "./certd/cname";
import access from "./certd/access";
import open from "./certd/open";
import mine from "./certd/mine";
import notification from "./certd/notification";
import addon from "./certd/addon";
import suite from "./certd/suite";
import project from "./certd/project";
import syssettings from "./certd/sys-settings";
import sysplugin from "./certd/sys-plugin";
import sysauthority from "./certd/sys-authority";
import syscname from "./certd/sys-cname";
import tutorial from "./certd/tutorial";
import cron from "./certd/cron";
// Note: @ is reserved in locale messages; use {'@'} when needed.
export default {
app: {
crud: {
i18n: {
name: "name",
city: "city",
status: "status",
},
},
},
fs: {
rowHandle: {
title: "Operation",
},
},
pipelinePage: {
addMore: "Add More Pipelines",
aliyunSubscriptionPipeline: "Aliyun Subscription Pipeline",
legoCertPipeline: "Lego Certificate Pipeline",
customPipeline: "Custom Pipeline",
batchAddPipeline: "Add Pipeline Use Template",
myPipelinesDesc: "Pipeline Mode: Apply -> Deploy -> Schedule",
},
order: {
confirmTitle: "Order Confirmation",
package: "Package",
description: "Description",
specifications: "Specifications",
pipeline: "Pipeline",
domain: "Domain",
deployTimes: "Deployments",
monitorCount: "DomainMonitors",
duration: "Duration",
price: "Price",
paymentMethod: "Payment Method",
free: "Free",
unit: {
pieces: "pieces",
count: "count",
times: "times",
},
},
framework: {
title: "Framework",
home: "Home",
},
helpDocLink: "Help Docs",
title: "Certificate Automation",
pipeline: "Pipeline",
pipelineEdit: "Edit Pipeline",
history: "Execution History",
certStore: "Certificate Repository",
siteMonitor: "Site Certificate Monitor",
settings: "Settings",
accessManager: "Access Management",
cnameRecord: "CNAME Record Management",
subDomain: "Subdomain Delegation Settings",
pipelineGroup: "Pipeline Group Management",
openKey: "Open API Key",
notification: "Notification Settings",
siteMonitorSetting: "Site Monitor Settings",
userSecurity: "Security Settings",
userProfile: "Account Info",
userGrant: "Grant Delegation",
suite: "Suite",
mySuite: "My Suite",
suiteBuy: "Suite Purchase",
myTrade: "My Orders",
paymentReturn: "Payment Return",
hasExpired: "Expired",
user: {
greeting: "Hello",
profile: "Account Info",
logout: "Logout",
setting: {
grantSetting: "Grant Settings",
saveSuccess: "Save Success",
allowAdminViewCerts: "Allow Admin view and download Certs",
allowAdminViewCertsHelper: "Allow admin view and download all certificates",
},
},
dashboard: {
greeting: "Hello, {name}, welcome to 【{site}】",
latestVersion: "Latest version: {version}",
validUntil: "Valid until:",
tutorialTooltip: "Click to view detailed tutorial",
tutorialText: "Only 3 steps to automatically apply and deploy certificates",
alertMessage: "Certificates and credentials are sensitive. Do not use untrusted online Certd services or images. Always self-host and use official release channels:",
helpDoc: "Help Docs",
pipelineCount: "Number of Certificate Pipelines",
noPipeline: "You have no certificate pipelines yet",
enabledCount: "Enabled",
disabledCount: "Disabled",
certCount: "Number of Certificates",
noCert: "You have no certificates yet",
manageCert: "View Certificates",
certExpiringCount: "Soon-to-Expire",
certExpiredCount: "Expired",
certNoExpireCount: "Not Expired",
createNow: "Create Now",
managePipeline: "Manage Pipelines",
pipelineStatus: "Pipeline Status",
recentRun: "Recent Run Statistics",
runCount: "Run Count",
expiringCerts: "Soon-to-Expire Certificates",
supportedTasks: "Overview of Supported Deployment Tasks",
changeLog: "Change Log",
},
steps: {
createPipeline: "Create Certificate Pipeline",
addTask: "Add Deployment Task",
scheduledRun: "Scheduled Run",
},
customPipeline: "Custom Pipeline",
createCertdPipeline: "Create Certificate Pipeline",
commercialCertHosting: "Commercial Certificate Hosting",
tooltip: {
manualUploadOwnCert: "Manually upload your own certificate for automatic deployment",
noAutoApplyCommercialCert: "Does not automatically apply for commercial certificates",
manualUploadOnUpdate: "Must manually upload once when the certificate is updated",
},
table: {
confirmDeleteTitle: "Are you sure you want to delete?",
confirmDeleteMessage: "This will delete all data related to the pipeline, including execution history, certificate files, and certificate repository records.",
},
play: {
runPipeline: "Run Pipeline",
confirm: "Confirm",
confirmTrigger: "Are you sure you want to trigger the run?",
pipelineStarted: "Pipeline has started running",
},
actions: {
editPipeline: "Edit Pipeline",
editConfigGroup: "Modify Configuration/Group",
viewCertificate: "View Certificate",
downloadCertificate: "Download Certificate",
},
fields: {
userId: "User ID",
pipelineName: "Pipeline Name",
keyword: "Keyword",
required: "This field is required",
pipelineContent: "Pipeline Content",
scheduledTaskCount: "Scheduled Task Count",
deployTaskCount: "Deployment Task Count",
remainingValidity: "Remaining Validity",
effectiveTime: "Effective time",
expiryTime: "Expiry Time",
status: "Status",
lastRun: "Last Run",
enabled: "Enabled",
enabledLabel: "Enabled",
disabledLabel: "Disabled",
group: "Group",
type: "Type",
order: "Order Number",
keepHistoryCount: "History Record Retention Count",
keepHistoryHelper: "Number of history records to keep; excess will be deleted",
createTime: "Creation Time",
updateTime: "Update Time",
triggerType: "Trigger Type",
pipelineId: "Pipeline Id",
nextRunTime: "Next Run Time",
projectName: "Project",
adminId: "Admin",
},
pi: {
validTime: "Piepline Valid Time",
validTimeHelper: "Not filled in means permanent validity",
},
types: {
certApply: "Cert Apply",
certUpload: "Cert Upload",
custom: "Custom",
template: "Template",
},
myPipelines: "My Pipelines",
selectedCount: "Selected {count} items",
batchDelete: "Batch Delete",
batchForceRerun: "Force Rerun",
batchRerun: "Rerun",
applyCertificate: "Apply for Certificate",
pipelineExecutionRecords: "Pipeline Execution Records",
confirm: "Confirm",
confirmBatchDeleteContent: "Are you sure you want to batch delete these {count} records?",
deleteSuccess: "Delete successful",
pleaseSelectRecords: "Please select records first",
triggerTypes: {
manual: "Manual Execution",
timer: "Scheduled Execution",
},
sysResources: {
sysRoot: "System Management",
sysConsole: "Console",
sysSettings: "System Settings",
cnameSetting: "CNAME Service Settings",
emailSetting: "Email Server Settings",
siteSetting: "Site Personalization",
headerMenus: "Top Menu Settings",
sysAccess: "System-level Authorization",
sysPlugin: "Plugin Management",
sysPluginEdit: "Edit Plugin",
sysPluginConfig: "Certificate Plugin Configuration",
accountBind: "Account Binding",
permissionManager: "Permission Management",
roleManager: "Role Management",
userManager: "User Management",
suiteManager: "Suite Management",
suiteSetting: "Suite Settings",
orderManager: "Order Management",
userSuites: "User Suites",
netTest: "Network Test",
enterpriseSetting: "Enterprise Settings",
projectManager: "Project Management",
projectUserManager: "Project User Management",
myProjectManager: "My Projects",
myProjectDetail: "Project Detail",
projectJoin: "Join Project",
currentProject: "Current Project",
projectMemberManager: "Project Member",
domainMonitorSetting: "Domain Monitor Settings",
},
certificateRepo: {
title: "Certificate Repository",
sub: "Certificates generated from pipeline",
},
certificateNotGenerated: "Certificate not yet generated, please run the pipeline first",
viewCertificateTitle: "View Certificate",
close: "Close",
viewCert: {
title: "View Certificate",
},
download: {
title: "Download Certificate",
},
source: "Source Code",
github: "GitHub",
gitee: "Gitee",
cron: {
clearTip: "Clear Selection",
nextTrigger: "Next Trigger Time",
tip: "Please set a valid cron expression first",
},
cronForm: {
title: "Scheduled Script",
helper: "Click the button above to select the time for daily execution.\nIt is recommended to run once a day. Tasks will be skipped if the certificate is not expiring.",
required: "This field is required",
},
email: {
title: "Recipient Email",
helper: "Enter your recipient email addresses. Multiple addresses are supported.",
required: "This field is required",
},
plugin: {
selectTitle: "Certificate Apply Plugin",
jsAcme: "JS-ACME: Easy to use, powerful features [Recommended]",
legoAcme: "Lego-ACME: Based on Lego, supports a wide range of DNS providers, suitable for users familiar with Lego",
aliyunOrder: "Aliyun-Order: Get certificate from Aliyun certificate order",
},
pipelineForm: {
createTitle: "Create Certificate Pipeline",
moreParams: "More Parameters",
triggerCronTitle: "Scheduled Trigger",
triggerCronHelper:
"Click the button above to choose a daily execution time.\nIt is recommended to trigger once per day. The task will be skipped if the certificate has not expired and will not be executed repeatedly.",
notificationTitle: "Failure Notification",
notificationWhen: "Notification Timing",
notificationHelper: "Get real-time alerts when the task fails",
groupIdTitle: "Pipeline Group",
addToMonitorEnabled: "Add to Cert Monitor",
addToMonitorDomains: "Add to Monitor Domains",
webhookEnabled: "Webhook Enabled",
webhookEnabledHelper: "Support webhook trigger pipeline, please check webhook address in trigger source",
},
notificationDefault: "Use Default Notification",
checkStatus: {
success: "Success",
checking: "Checking",
error: "Error",
},
domainList: {
title: "Domain List",
helper: "Format: domain:port:name:remark, one per line. Port and name are optional.\nExamples:\nwww.baidu.com:443:Baidu:remarkText\nwww.taobao.com::Taobao\nwww.google.com",
required: "Please enter domains to import",
placeholder: "www.baidu.com:443:Baidu\nwww.taobao.com::Taobao\nwww.google.com\n",
},
accountInfo: "Account Information",
securitySettings: "Security & Settings",
confirmDisable2FA: "Are you sure you want to disable two-factor authentication login?",
disabledSuccess: "Disabled successfully",
saveSuccess: "Saved successfully",
twoFactorAuth: "2FA Two-Factor Authentication Login",
rebind: "Rebind",
twoFactorAuthHelper: "Enable or disable two-factor authentication login",
bindDevice: "Bind Device",
step1: "1. Install any authenticator app, for example:",
tooltipGoogleServiceError: "If you get a Google service not found error, you can install KK Google Assistant",
step2: "2. Scan the QR code to add the account",
step3: "3. Enter the verification code",
inputVerifyCode: "Please enter the verification code",
cancel: "Cancel",
authorizationManagement: "Authorization Management",
manageThirdPartyAuth: "Manage third-party system authorization information",
name: "Name",
pleaseEnterName: "Please enter the name",
nameHelper: "Fill in as you like, useful to distinguish when multiple authorizations of the same type exist",
level: "Level",
system: "System",
usera: "User",
nickName: "Nickname",
max50Chars: "Maximum 50 characters",
myInfo: "My Information",
save: "Save",
editSchedule: "Edit Schedule",
timerTrigger: "Timer Trigger",
schedule: "Schedule",
selectCron: "Please select a schedule Cron",
batchEditSchedule: "Batch Edit Schedule",
editTrigger: "Edit Trigger",
triggerName: "Trigger Name",
requiredField: "This field is required",
type: "Type",
enterName: "Please enter a name",
confirmDeleteTrigger: "Are you sure you want to delete this trigger?",
notificationType: "Notification Type",
selectNotificationType: "Please select a notification type",
notificationName: "Notification Name",
helperNotificationName: "Fill freely, helps to distinguish when multiple notifications of the same type exist",
isDefault: "Is Default",
yes: "Yes",
no: "No",
selectIsDefault: "Please select if default",
prompt: "Prompt",
confirmSetDefaultNotification: "Are you sure to set as default notification?",
test: "Test",
scope: "Scope",
scopeOpenApiOnly: "Open API Only",
scopeFullAccount: "Full Account Permissions",
required: "This field is required",
scopeHelper: "Open API only allows access to open APIs; full account permissions allow access to all APIs",
add: "Generate New Key",
gen: {
text: "API Test",
title: "x-certd-token",
okText: "Confirm",
contentPart1: "Test the x-certd-token below, you can use it within 3 minutes to test ",
openApi: "Open API",
contentPart2: " request testing",
},
pending_cname_setup: "Pending CNAME setup",
validating: "Validating",
validation_successful: "Validation successful",
validation_failed: "Validation failed",
validation_timed_out: "Validation timed out",
proxied_domain: "Proxied Domain",
host_record: "Host Record",
please_set_cname: "Please set CNAME",
cname_service: "CNAME Service",
default_public_cname: "Default public CNAME service, you can also ",
customize_cname: "Customize CNAME Service",
public_cname: "Public CNAME",
custom_cname: "Custom CNAME",
validate: "Validate",
validation_started: "Validation started, please wait patiently",
click_to_validate: "Click to Validate",
all: "All",
cname_feature_guide: "CNAME feature principle and usage guide",
batch_delete: "Batch Delete",
confirm_delete_count: "Are you sure to delete these {count} records in batch?",
delete_successful: "Delete successful",
please_select_records: "Please select records first",
edit_notification: "Edit Notification",
other_notification_method: "Other Notification Method",
trigger_time: "Trigger Time",
start_time: "At Start",
success_time: "On Success",
fail_to_success_time: "Fail to Success",
fail_time: "On Failure",
helper_suggest_fail_only: "It is recommended to select only 'On Failure' and 'Fail to Success'",
notification_config: "Notification Configuration",
please_select_notification: "Please select a notification method",
please_select_type: "Please select type",
please_select_trigger_time: "Please select notification trigger time",
please_select_notification_config: "Please select notification configuration",
confirm_delete_trigger: "Are you sure you want to delete this trigger?",
gift_package: "Gift Package",
package_name: "Package Name",
click_to_select: "Click to select",
please_select_package: "Please select a package",
package: "Package",
addon_package: "Addon Package",
domain_count: "Domain Count",
unit_count: "pcs",
field_required: "This field is required",
pipeline_count: "Pipeline Count",
unit_item: "items",
deploy_count: "Deploy Count",
unit_times: "times",
monitor_count: "Certificate Monitor Count",
duration: "Duration",
status: "Status",
active_time: "Activation Time",
expires_time: "Expiration Time",
is_present: "Is Present",
is_present_yes: "Yes",
is_present_no: "No",
basicInfo: "Basic Information",
titlea: "Title",
disabled: "Disabled",
ordera: "Order",
supportBuy: "Support Purchase",
intro: "Introduction",
packageContent: "Package Content",
maxDomainCount: "Max Domain Count",
maxPipelineCount: "Max Pipeline Count",
maxDeployCount: "Max Deploy Count",
maxMonitorCount: "Max Monitor Count",
price: "Price",
durationPrices: "Duration Prices",
packageName: "Package Name",
addon: "Addon",
typeHelper: "Suite: Only the most recently purchased one is active at a time\nAddon: Multiple can be purchased, effective immediately without affecting the suite\nThe quantities of suite and addon can be accumulated",
domainCount: "Domain Count",
pipelineCount: "Pipeline Count",
unitPipeline: "pipelines",
deployCount: "Deployment Count",
unitDeploy: "times",
monitorCount: "Certificate Monitor Count",
unitCount: "pcs",
durationPriceTitle: "Duration and Price",
selectDuration: "Select Duration",
supportPurchase: "Support Purchase",
cannotPurchase: "Cannot Purchase",
shelfStatus: "Shelf Status",
onShelf: "On Shelf",
offShelf: "Off Shelf",
orderHelper: "Smaller values appear first",
description: "Description",
createTime: "Creation Time",
updateTime: "Update Time",
mainDomain: "Main Domain",
edit: "Edit",
groupName: "Group Name",
enterGroupName: "Please enter group name",
subdomainHosting: "Subdomain Hosting",
subdomainHostingHint: "When your domain has subdomain hosting set, you need to create records here, otherwise certificate application will fail",
batchDeleteConfirm: "Are you sure to batch delete these {count} records?",
selectRecordFirst: "Please select records first",
subdomainHosted: "Hosted Subdomain",
subdomainHelpText: "If you don't understand what subdomain hosting is,Do not set it randomly, as it may result in the inability to apply for the certificate. please refer to the documentation ",
subdomainHelpSupportStart: "Supports * wildcard, indicating that all subdomains of the domain are hosted (free subdomains)",
subdomainManagement: "Subdomain Management",
isDisabled: "Is Disabled",
enabled: "Enabled",
uploadCustomCert: "Upload Custom Certificate",
sourcee: "Source",
sourcePipeline: "Cert Apply Pipeline",
sourceManualUpload: "Hosted Pipeline",
domains: "Domains",
enterDomain: "Please enter domain",
validDays: "Valid Days",
expires: " expires",
days: " days",
effectiveTime: "Effective Time",
expireTime: "Expiration Time",
certIssuer: "Certificate Issuer",
applyTime: "Application Time",
relatedPipeline: "Related Pipeline",
statusSuccess: "Success",
statusChecking: "Checking",
statusError: "Error",
actionImportBatch: "Batch Import",
actionSyncIp: "Sync IP",
modalTitleSyncIp: "Sync IP",
modalContentSyncIp: "Are you sure to sync IP?",
notificationSyncComplete: "Sync Complete",
actionCheckAll: "Check All",
modalTitleConfirm: "Confirm",
modalContentCheckAll: "Confirm to trigger checking all IP site's certificates?",
notificationCheckSubmitted: "Check task submitted",
notificationCheckDescription: "Please refresh later to see results",
tooltipCheckNow: "Check Now",
notificationCheckSubmittedPleaseRefresh: "Check task submitted, please refresh later",
columnId: "ID",
columnIp: "IP",
helperIpCname: "Supports entering CNAME domain name or source site domain name",
ruleIpRequired: "Please enter IP",
columnCertDomains: "Certificate Domains",
columnCertProvider: "Issuer",
columnCertStatus: "Certificate Status",
statusNormal: "Normal",
statusExpired: "Expired",
columnCertExpiresTime: "Certificate Expiration Time",
expired: "expired",
columnCheckStatus: "Check Status",
columnLastCheckTime: "Last Check Time",
columnSource: "Source",
sourceSync: "Sync",
sourceManual: "Manual",
sourceImport: "Import",
columnDisabled: "Enabled/Disabled",
columnRemark: "Remark",
pluginFile: "Plugin File",
selectPluginFile: "Select plugin file",
overrideSameName: "Override same name",
override: "Override",
noOverride: "No override",
overrideHelper: "If a plugin with the same name exists, override it directly",
importPlugin: "Import Plugin",
operationSuccess: "Operation successful",
customPlugin: "Custom Plugin",
import: "Import",
export: "Export",
pluginType: "Plugin Type",
auth: "Authorization",
dns: "DNS",
deployPlugin: "Deploy Plugin",
icon: "Icon",
pluginName: "Plugin Name",
pluginNameHelper: "Must be English letters or digits, camelCase with type prefix\nExample: AliyunDeployToCDN\nDo not modify name once plugin is used",
pluginNameRuleMsg: "Must be English letters or digits, camelCase with type prefix",
author: "Author",
authorHelper: "Used as prefix when uploading to plugin store, e.g., greper/pluginName",
authorRuleMsg: "Must be English letters or digits",
titleHelper: "Plugin name in Chinese",
descriptionHelper: "Description of the plugin",
builtIn: "Built-in",
custom: "Custom",
store: "Store",
version: "Version",
pluginDependencies: "Plugin Dependencies",
pluginDependenciesHelper: "Dependencies to install first in format: [author/]pluginName[:version]",
editableRunStrategy: "Editable Run Strategy",
editable: "Editable",
notEditable: "Not Editable",
runStrategy: "Run Strategy",
normalRun: "Normal Run",
skipOnSuccess: "Skip on success (Deploy task)",
defaultRunStrategyHelper: "Default run strategy",
enableDisable: "Enable/Disable",
clickToToggle: "Click to toggle enable/disable",
confirmToggle: "Are you sure to",
disable: "disable",
enable: "enable",
pluginGroup: "Plugin Group",
icpRegistrationNumber: "ICP Registration Number",
icpPlaceholder: "Guangdong ICP xxxxxxx Number",
publicSecurityRegistrationNumber: "Public Security Registration Number",
publicSecurityPlaceholder: "Beijing Public Security xxxxxxx Number",
enableAssistant: "Enable Assistant",
allowCrawlers: "Allow Crawlers",
httpProxy: "HTTP Proxy",
httpProxyPlaceholder: "http://192.168.1.2:18010/",
httpProxyHelper: "Configure when some websites are blocked",
httpsProxy: "HTTPS Proxy",
httpsProxyPlaceholder: "http://192.168.1.2:18010/",
saveThenTestTitle: "Save first, then click test",
testButton: "Test",
httpsProxyHelper: "Usually both proxies are the same, save first then test",
dualStackNetwork: "Dual Stack Network",
default: "Default",
ipv4Priority: "IPv4 Priority",
ipv6Priority: "IPv6 Priority",
dualStackNetworkHelper: "If IPv6 priority is selected, enable IPv6 in docker-compose.yaml",
enableCommonCnameService: "Enable Public CNAME Service",
commonCnameHelper: "Allow use of public CNAME service. If disabled and no <a href='#/sys/cname/provider'>custom CNAME service</a> is set, CNAME proxy certificate application will not work.",
enableCommonSelfServicePasswordRetrieval: "Enable self-service password recovery",
saveButton: "Save",
stopSuccess: "Stopped successfully",
google: "Google",
baidu: "Baidu",
success: "Success",
testFailed: "Test Failed",
testCompleted: "Test Completed",
manageOtherUserPipeline: "Manage other users' pipelines",
limitUserPipelineCount: "Limit user pipeline count",
limitUserPipelineCountHelper: "0 means no limit",
enableSelfRegistration: "Enable self-registration",
enableUserValidityPeriod: "Enable user validity period",
userValidityPeriodHelper: "Users can use normally within validity; pipelines disabled after expiry",
enableUsernameRegistration: "Enable username registration",
enableEmailRegistration: "Enable email registration",
proFeature: "Pro feature",
emailServerSetup: "Set up email server",
enableSmsLoginRegister: "Enable SMS login and registration",
commFeature: "Commercial feature",
smsProvider: "SMS provider",
aliyunSms: "Aliyun SMS",
tencentSms: "Tencent SMS",
yfySms: "YFY SMS",
smsTest: "SMS test",
testMobilePlaceholder: "Enter test mobile number",
saveThenTest: "Save first then test",
enterTestMobile: "Please enter test mobile number",
sendSuccess: "Sent successfully",
atLeastOneLoginRequired: "At least one of password login or SMS login must be enabled",
fieldRequired: "This field is required",
siteHide: "Site Hide",
enableSiteHide: "Enable Site Hide",
siteHideDescription: "You can disable site accessibility normally and enable it when needed to enhance site security",
helpDoc: "Help Document",
randomAddress: "Random Address",
siteHideUrlHelper: "After the site is hidden, you need to visit this URL to unlock to access normally",
fullUnlockUrl: "Full Unlock URL",
saveThisUrl: "Please save this URL carefully",
unlockPassword: "Unlock Password",
unlockPasswordHelper: "Password needed to unlock the hide; set on first time or reset when filled",
autoHideTime: "Auto Hide Time",
autoHideTimeHelper: "Minutes without requests before auto hiding",
hideOpenApi: "Hide Open API",
hideOpenApiHelper: "Whether to hide open APIs; whether to hide /api/v1 prefixed endpoints",
hideSiteImmediately: "Hide Site Immediately",
hideImmediately: "Hide Immediately",
confirmHideSiteTitle: "Are you sure to hide the site immediately?",
confirmHideSiteContent: "After hiding, the site will be inaccessible. Please operate cautiously.",
siteHiddenSuccess: "Site has been hidden",
emailServerSettings: "Email Server Settings",
setEmailSendingServer: "Set the email sending server",
useCustomEmailServer: "Use Custom Email Server",
smtpDomain: "SMTP Domain",
pleaseEnterSmtpDomain: "Please enter SMTP domain or IP",
smtpPort: "SMTP Port",
pleaseEnterSmtpPort: "Please enter SMTP port",
username: "Username",
pleaseEnterUsername: "Please enter username",
password: "Password",
pleaseEnterPassword: "Please enter password",
qqEmailAuthCodeHelper: "If using QQ email, get an authorization code in QQ email settings as the password",
senderEmailHelper: "You can use the format: Name<Email> to set the sender name, e.g.: autossl<certd{'@'}example.com>",
senderEmail: "Sender Email",
pleaseEnterSenderEmail: "Please enter sender email",
useSsl: "Use SSL",
sslPortNote: "SSL and non-SSL SMTP ports are different, please adjust port accordingly",
ignoreCertValidation: "Ignore Certificate Validation",
useOfficialEmailServer: "Use Official Email Server",
useOfficialEmailServerHelper: "Send emails directly using the official server to avoid complicated setup",
testReceiverEmail: "Test Receiver Email",
pleaseEnterTestReceiverEmail: "Please enter test receiver email",
saveBeforeTest: "Save before testing",
sendFailHelpDoc: "Failed to send??? ",
emailConfigHelpDoc: "Email configuration help document",
tryOfficialEmailServer: "You can also try using the official email server ↗↗↗↗↗↗↗↗",
pluginManagement: "Plugin Management",
pluginBetaWarning: "Custom plugins are in BETA and may have breaking changes in future",
pleaseSelectRecord: "Please select records first",
permissionManagement: "Permission Management",
adda: "Add",
rootNode: "Root Node",
permissionName: "Permission Name",
enterPermissionName: "Please enter permission name",
permissionCode: "Permission Code",
enterPermissionCode: "Please enter permission code",
max100Chars: "Maximum 100 characters",
examplePermissionCode: "e.g.: sys:user:view",
sortOrder: "Sort Order",
sortRequired: "Sort order is required",
parentNode: "Parent Node",
roleManagement: "Role Management",
assignPermissions: "Assign Permissions",
roleName: "Role Name",
enterRoleName: "Please enter role name",
unlockLogin: "Unlock Login",
notice: "Notice",
confirmUnlock: "Are you sure you want to unlock this user's login?",
unlockSuccess: "Unlock successful",
enterUsername: "Please enter username",
modifyPasswordIfFilled: "Fill in to change the password",
emaila: "Email",
mobile: "Mobile",
avatar: "Avatar",
validTime: "Valid Time",
remark: "Remark",
roles: "Roles",
cnameTitle: "CNAME Service Configuration",
cnameDescription:
"The domain name configured here serves as a proxy for verifying other domains. When other domains apply for certificates, they map to this domain via CNAME for ownership verification. The advantage is that any domain can apply for a certificate this way without providing an AccessSecret.",
cnameLinkText: "CNAME principle and usage instructions",
confirmTitle: "Confirm",
confirmDeleteBatch: "Are you sure you want to delete these {count} records?",
selectRecordsFirst: "Please select records first",
cnameDomain: "CNAME Domain",
cnameDomainPlaceholder: "cname.handsfree.work",
cnameDomainHelper:
"Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.",
cnameDomainPattern: "Domain name cannot contain *",
dnsProvider: "DNS Provider",
dnsProviderAuthorization: "DNS Provider Authorization",
setDefault: "Set Default",
confirmSetDefault: "Are you sure to set as default?",
setAsDefault: "Set as Default",
disabledLabel: "Disabled",
confirmToggleStatus: "Are you sure to {action}?",
template: {
title: "Pipeline Template",
edit: "Pipeline Template Edit",
importCreate: "Pipeline Batch Create",
// intro: "可根据模版批量创建流水线",
intro: "Batch create pipeline based on template",
createTemplate: "Create Template",
useTemplate: "Use This Template",
batchCreate: "Batch Create Pipeline",
singleCreate: "Create Single Pipeline",
templateName: "Template Name",
enterTemplateName: "Please enter template name",
copyPipelineConfig: "Copy this pipeline configuration as template source",
pipeline: "Pipeline",
},
addonType: "Type",
addonName: "Name",
addonNameHelper: "Fill freely, helps to distinguish when multiple same type exist",
addonTypeSelect: "Select type",
dates: {
years: "{count} years",
months: "{count} months",
},
sys: {
setting: {
baseSetting: "Base Settings",
registerSetting: "Register Settings",
safeSetting: "Safe Settings",
paymentSetting: "Payment Settings",
captchaSetting: "Captcha Setting",
pipelineSetting: "Pipeline Settings",
oauthSetting: "Login Settings",
networkSetting: "Network Settings",
adminModeSetting: "Admin Mode Settings",
adminModeHelper: "enterprise mode : allow to create and manage pipelines, roles, users, etc.\n saas mode : only allow to create and manage pipelines",
enterpriseMode: "Enterprise Mode",
saasMode: "SaaS Mode",
showRunStrategy: "Show RunStrategy",
showRunStrategyHelper: "Allow modify the run strategy of the task",
captchaEnabled: "Enable Login Captcha",
captchaHelper: "Whether to enable captcha verification for login",
captchaType: "Captcha Setting",
captchaTest: "Captcha Test",
// 保存后再点击测试,请务必测试通过了,再开启登录验证码
captchaTestHelper: "Save and click test, please make sure the test is passed before enabling login captcha",
pipelineValidTimeEnabled: "Enable Pipeline Valid Time",
pipelineValidTimeEnabledHelper: "Whether to enable the valid time of the pipeline",
certDomainAddToMonitorEnabled: "Add Domain to Certificate Monitor",
certDomainAddToMonitorEnabledHelper: "Whether to add the domain to the certificate monitor",
defaultCertRenewDays: "Default Certificate Renew Days",
defaultCertRenewDaysHelper: "Default certificate renewal days, helpful for table list progress bar display",
defaultCertRenewDaysRecommend: "Recommend 15",
pipelineMaxRunningCount: "Max Running Count",
pipelineMaxRunningCountHelper: "Max running count of the pipeline",
pipelineMaxRunningCountRecommend: "Recommend 5-15, default 10",
acmeWalkFromAuthoritative: "Check TXT Record from Authoritative NS",
acmeWalkFromAuthoritativeHelper: "Apply certificate when whether to check the TXT record from authoritative NS server first",
fixedCertExpireDays: "Fixed Cert Expire Days",
fixedCertExpireDaysHelper: "Fixed cert expiration days, helpful for table list progress bar display",
fixedCertExpireDaysRecommend: "Recommend 90",
enableOauth: "Enable OAuth2 Login",
oauthEnabledHelper: "Whether to enable OAuth2 login",
oauthProviders: "OAuth2 Login Providers",
oauthType: "OAuth2 Login Type",
oauthConfig: "OAuth2 Login Config",
oauthProviderSelectorPlaceholder: "Not Configured",
oauthCallback: "Callback URL",
oauthCallbackHelper: "Copy this URL to the callback address of the OAuth2 login provider",
oauthCallbackCopy: "Copy Callback URL",
oauthAutoRegister: "Auto Register User",
oauthAutoRegisterCheckedText: "Auto Register",
oauthAutoRegisterUnCheckedText: "User Select",
oauthAutoRegisterHelper: "Whether to auto register user when login",
oauthAutoRedirect: "Auto Redirect to OAuth2 Login",
oauthAutoRedirectHelper: "Whether to auto redirect to OAuth2 login when login (using the first enabled OAuth2 login type)",
oauthOnly: "OAuth2 Login Only",
oauthOnlyHelper: "Whether to only allow OAuth2 login, disable password login",
enablePasskey: "Enable Passkey Login",
passkeyHostnameNotSame: "Passkey hostname must be the same as the main domain",
passkeyEnabledHelper:
"1、Site must enable https \n2、Domain name must not change, otherwise the registered passkey will be invalid \n3、Domain name must be the same as the main domain, otherwise the registered passkey will be invalid",
email: {
templates: "Email Templates",
templateType: "Template Type",
templateProvider: "Template Config",
templateSetting: "Email Template Setting",
serverSetting: "Email Server Setting",
sendTest: "Send Test",
templateProviderSelectorPlaceholder: "Not Configured",
},
notice: "System Notice",
noticeHelper: "System notice, will be displayed on the login page",
noticePlaceholder: "System notice",
reverseProxy: "Reverse Proxy List",
reverseProxyHelper: "Reverse proxy for ACME address, used when applying for certificate",
reverseProxyPlaceholder: "http://le.px.handfree.work",
reverseProxyEmpty: "No reverse proxy list configured",
environmentVars: "Environment Variables",
environmentVarsHelper: "configure the runtime environment variables, one per line, format: KEY=VALUE",
bindUrl: "Bind URL",
},
},
modal: {
close: "Close",
viewCertificateTitle: "View Certificate",
},
domain: {
domainManager: "Domain Manager",
domainDescription: "used to auto apply for certificate", //管理域名的校验方式,用于申请证书时自动选择验证方式
domain: "Domain",
challengeType: "Challenge Type",
dnsProviderType: "DNS Provider Type",
dnsProviderAccess: "DNS Provider Access",
httpUploaderType: "HTTP Uploader Type",
httpUploaderAccess: "HTTP Uploader Access",
httpUploadRootDir: "HTTP Upload Root Dir",
disabled: "Disabled",
challengeSetting: "Challenge Setting",
gotoCnameTip: "Please go to CNAME Record Page",
fromType: "From Type",
expirationDate: "Expiration Date",
},
ent: {
projectName: "Project Name",
projectDescription: "Project Description",
projectDetailManager: "Project Detail",
projectDetailDescription: "Manage Project Members",
projectPermission: "Permission",
permission: {
read: "Read",
write: "Write",
admin: "Admin",
},
projectMemberStatus: "Member Status",
isSystem: "Is System Project",
isSystemHelper: "System-level projects allow running admin plugins",
},
project: {
noProjectJoined: "You haven't joined any projects yet",
applyToJoin: "Please apply to join a project to start using",
systemProjects: "System Project List",
createdAt: "Created At",
applyJoin: "Apply to Join",
noSystemProjects: "No system projects available",
fetchFailed: "Failed to fetch project list",
applySuccess: "Application successful, waiting for admin approval",
applyFailed: "Application failed, please try again later",
leave: "Leave Project",
leaveSuccess: "Leave project successful",
leaveFailed: "Leave project failed, please try again later",
applyJoinConfirm: "Are you sure you want to apply to join this project?",
leaveConfirm: "Are you sure you want to leave this project?",
viewDetail: "View Detail",
projectManage: "Project Manage",
},
addonSelector: {
select: "Select",
placeholder: "select please",
},
...common,
...navigation,
...dashboard,
...pipeline,
...history,
...monitor,
...certdomain,
...cname,
...access,
...open,
...mine,
...notification,
...addon,
...suite,
...project,
...syssettings,
...sysplugin,
...sysauthority,
...syscname,
...tutorial,
...cron,
};
@@ -0,0 +1,7 @@
export default {
authorizationManagement: "Authorization Management",
manageThirdPartyAuth: "Manage third-party system authorization information",
nameHelper: "Fill in as you like, useful to distinguish when multiple authorizations of the same type exist",
level: "Level",
system: "System",
};
@@ -0,0 +1,10 @@
export default {
addonType: "Type",
addonName: "Name",
addonNameHelper: "Fill freely, helps to distinguish when multiple same type exist",
addonTypeSelect: "Select type",
addonSelector: {
select: "Select",
placeholder: "select please",
},
};

Some files were not shown because too many files have changed in this diff Show More