mirror of
https://github.com/certd/certd.git
synced 2026-07-16 02:37:34 +08:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83495b3213 | |||
| 5108416904 | |||
| 5589da1822 | |||
| 21e5aed3f3 | |||
| 4a88f795e1 | |||
| 604fa5be63 | |||
| 7ed1be994f | |||
| 6cc74a1c0a | |||
| 167b303fae | |||
| b91c9e4ea6 | |||
| 584b7b6f45 | |||
| 2312c444ef | |||
| 0373d019f8 | |||
| fe09e75b80 | |||
| 432c9c6cc1 | |||
| 5fc1d55879 | |||
| 268cd6cc9c | |||
| 20ffe31638 | |||
| 02d30067ea | |||
| 0ce7808e08 | |||
| d609ee2b7a | |||
| 083df61fcc | |||
| a524988be6 | |||
| 8517a0b564 | |||
| ec69b8f11b | |||
| edda1b57f3 | |||
| 5a7766992d | |||
| 02dabe11db | |||
| 58024128d8 | |||
| e44bf9d773 | |||
| 1114223bda | |||
| 55022e5fa6 | |||
| ae732fb298 | |||
| a6ef6996c3 | |||
| 97cf5e127b | |||
| 690df7373c | |||
| 9cc01e5da1 | |||
| 0a068a2746 | |||
| 85a239ff8c | |||
| a545a28dfb | |||
| ac876a980c | |||
| 4b555e8a56 | |||
| 1a8d14dc44 | |||
| 83263a72d2 | |||
| 76f3ba8691 | |||
| 072edd7aff | |||
| c0be4d702c | |||
| c3d6db3f1e | |||
| e05ec53eb5 | |||
| 8d9dad9c82 | |||
| 0071bcb0e4 | |||
| dbdc1ccd1b | |||
| 2a606fdb1d | |||
| 3b86f30bcf |
@@ -400,6 +400,7 @@ export class OAuthDemoAccess extends BaseAccess {
|
||||
4. **错误处理**:API 调用失败时应抛出明确的错误信息。
|
||||
5. **测试方法**:实现 `onTestRequest` 方法,以便用户可以测试授权是否正常。
|
||||
6. **统一接口调用**:封装统一的 API 请求方法,避免重复编写错误处理逻辑。
|
||||
7. **旧版数据兼容**: 新增注解的插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
|
||||
|
||||
## 开发技巧
|
||||
|
||||
|
||||
@@ -454,3 +454,4 @@ new TencentDnsProvider();
|
||||
3. **记录结构**:定义适合对应云平台的记录数据结构,至少包含 id 字段用于删除记录。
|
||||
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`,参数文本化,不要传对象,否则会输出`[object Object]}`。
|
||||
5. **错误处理**:API 调用失败时应抛出明确的错误信息。
|
||||
6. **旧版数据兼容**: 新增插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
|
||||
|
||||
@@ -281,12 +281,12 @@ export class DemoTest extends AbstractTaskPlugin {
|
||||
return {
|
||||
value: item.siteName,
|
||||
label: item.siteName,
|
||||
domain: item.siteName,
|
||||
domain: item.siteName, //这里必须要包含domain 否则后面分组时候全部分配到未匹配中
|
||||
};
|
||||
});
|
||||
//将站点域名名称根据证书域名进行匹配分组,分成匹配的和不匹配的两组选项,返回给前端,供用户选择
|
||||
return {
|
||||
list: optionsUtils.buildGroupOptions(options, this.certDomains),
|
||||
list: optionsUtils.buildGroupOptions(options, this.certDomains), //分组后的列表
|
||||
total: siteRes.length,
|
||||
};
|
||||
}
|
||||
@@ -641,6 +641,7 @@ new AliyunOSSDeploy();
|
||||
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`,参数文本化,不要传对象,否则会输出`[object Object]}`。
|
||||
5. **错误处理**:执行过程中的错误应被捕获并记录。
|
||||
6. **授权获取**:使用 `this.getAccess(accessId)` 获取授权信息。
|
||||
7. **旧版数据兼容**: 新增@TaskInput注解的插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
|
||||
|
||||
## 部署逻辑注意事项
|
||||
|
||||
|
||||
@@ -210,3 +210,8 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
|
||||
- 后端业务数据、接口、实体、权限、迁移:改 `packages/ui/certd-server/src/modules` 与 `src/controller`。
|
||||
- 表单、列表、插件配置 UI:改 `packages/ui/certd-client/src/views/certd` 及对应 `src/api`。
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 旧版数据兼容
|
||||
|
||||
- 新增插件参数时,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
|
||||
@@ -3,6 +3,41 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
|
||||
* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3))
|
||||
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
|
||||
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
|
||||
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **volcengine-alb:** 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 ([0a068a2](https://github.com/certd/certd/commit/0a068a274673e9768954e9f7367c267d44f3b530))
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复火山视频点播源站选择不到自定义源站的bug ([0071bcb](https://github.com/certd/certd/commit/0071bcb0e4dd108c86d7ca01820a9f6e6960e440))
|
||||
* 修复企业模式下弹出邮箱绑定提醒的问题 ([8d9dad9](https://github.com/certd/certd/commit/8d9dad9c82f6f2fd3ab3040068946a33f37145b1))
|
||||
* 修复AsiaIsp CDN证书重复情况下部署失败的问题 ([c3d6db3](https://github.com/certd/certd/commit/c3d6db3f1ef2f1c897b7989521fe8809dffaded1))
|
||||
* 修复cname用阿里云校验时报找不到runtimeDepsService的错误 ([072edd7](https://github.com/certd/certd/commit/072edd7affee424ab3411f4d41d338f084d7cac6))
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -3,6 +3,42 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
|
||||
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
|
||||
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **volcengine-alb:** 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 ([0a068a2](https://github.com/certd/certd/commit/0a068a274673e9768954e9f7367c267d44f3b530))
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复火山视频点播源站选择不到自定义源站的bug ([0071bcb](https://github.com/certd/certd/commit/0071bcb0e4dd108c86d7ca01820a9f6e6960e440))
|
||||
* 修复企业模式下弹出邮箱绑定提醒的问题 ([8d9dad9](https://github.com/certd/certd/commit/8d9dad9c82f6f2fd3ab3040068946a33f37145b1))
|
||||
* 修复AsiaIsp CDN证书重复情况下部署失败的问题 ([c3d6db3](https://github.com/certd/certd/commit/c3d6db3f1ef2f1c897b7989521fe8809dffaded1))
|
||||
* 修复cname用阿里云校验时报找不到runtimeDepsService的错误 ([072edd7](https://github.com/certd/certd/commit/072edd7affee424ab3411f4d41d338f084d7cac6))
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复多域名无法使用passkey登录的bug ([d176f9c](https://github.com/certd/certd/commit/d176f9cc0ebd051a614bfac74d1616d1945fc9a3))
|
||||
* 修复企业模式下登录报projectId不能为空的问题 ([a65366b](https://github.com/certd/certd/commit/a65366bbe1aadea8baaffbdadab58a5b631d9417))
|
||||
* **login:** 修复输入法 composing 状态下回车触发提交的问题 ([b74db81](https://github.com/certd/certd/commit/b74db81304bbe68476bbec5ea4307a2264060e92))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **certd-server:** 使用 jks-go转换jks证书,大幅精简镜像大小 ([c78898e](https://github.com/certd/certd/commit/c78898e4c10dd1701467d2e42e3f72bd8f2a352f))
|
||||
* **pipeline:** 将默认历史保留条数从30调整为100 ([d3e4677](https://github.com/certd/certd/commit/d3e4677ea4fac8e7533749d7f4187e410489e536))
|
||||
|
||||
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
+1
-1
@@ -9,5 +9,5 @@
|
||||
}
|
||||
},
|
||||
"npmClient": "pnpm",
|
||||
"version": "1.42.1"
|
||||
"version": "1.42.5"
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"test:unit": "cross-env NODE_ENV=unittest pnpm -r --workspace-concurrency=1 run test:unit",
|
||||
"pub": "echo 1",
|
||||
"dev": "pnpm run -r --parallel compile ",
|
||||
"lint_all": "pnpm run -r --parallel lint ",
|
||||
"pub_all": "node ./scripts/pub-all.js",
|
||||
"release": "time /t >trigger/release.trigger && git add trigger/release.trigger && git commit -m \"build: release\" && git push",
|
||||
"publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.5](https://github.com/publishlab/node-acme-client/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.42.4](https://github.com/publishlab/node-acme-client/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.42.3](https://github.com/publishlab/node-acme-client/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.42.2](https://github.com/publishlab/node-acme-client/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.42.1](https://github.com/publishlab/node-acme-client/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Simple and unopinionated ACME client",
|
||||
"private": false,
|
||||
"author": "nmorsman",
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"type": "module",
|
||||
"module": "./dist/index.js",
|
||||
"main": "./dist/index.js",
|
||||
@@ -18,7 +18,7 @@
|
||||
"types"
|
||||
],
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.42.1",
|
||||
"@certd/basic": "^1.42.5",
|
||||
"@peculiar/x509": "^1.11.0",
|
||||
"asn1js": "^3.0.5",
|
||||
"axios": "^1.9.0",
|
||||
@@ -75,5 +75,5 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/publishlab/node-acme-client/issues"
|
||||
},
|
||||
"gitHead": "b46948c0ba67069ebcd2bb71b21b6c289d99f1b8"
|
||||
"gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
@@ -1 +1 @@
|
||||
00:18
|
||||
23:31
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/basic",
|
||||
"private": false,
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -54,5 +54,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "b46948c0ba67069ebcd2bb71b21b6c289d99f1b8"
|
||||
"gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/pipeline",
|
||||
"private": false,
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -21,8 +21,8 @@
|
||||
"lint": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.42.1",
|
||||
"@certd/plus-core": "^1.42.1",
|
||||
"@certd/basic": "^1.42.5",
|
||||
"@certd/plus-core": "^1.42.5",
|
||||
"dayjs": "^1.11.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
@@ -51,5 +51,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "b46948c0ba67069ebcd2bb71b21b6c289d99f1b8"
|
||||
"gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FormItemProps } from "../dt/index.js";
|
||||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
||||
import * as _ from "lodash-es";
|
||||
import { PluginRequestHandleReq } from "../plugin/index.js";
|
||||
import { IRuntimeDepsService, IServiceGetter } from "../service/index.js";
|
||||
import { IServiceGetter, getRuntimeDepsService } from "../service/index.js";
|
||||
|
||||
// export type AccessRequestHandleReqInput<T = any> = {
|
||||
// id?: number;
|
||||
@@ -48,23 +48,13 @@ export type AccessContext = {
|
||||
|
||||
export abstract class BaseAccess implements IAccess {
|
||||
ctx!: AccessContext;
|
||||
runtimeDepsService?: IRuntimeDepsService;
|
||||
|
||||
async importRuntime(specifier: string) {
|
||||
if (!this.runtimeDepsService) {
|
||||
throw new Error("runtimeDepsService 未初始化");
|
||||
}
|
||||
return await this.runtimeDepsService.importRuntime(specifier, this.ctx.logger);
|
||||
return await getRuntimeDepsService().importRuntime(specifier, this.ctx.logger);
|
||||
}
|
||||
|
||||
async setCtx(ctx: AccessContext) {
|
||||
this.ctx = ctx;
|
||||
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
|
||||
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
|
||||
}
|
||||
if (this.runtimeDepsService && this.ctx.define?.name) {
|
||||
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `access:${this.ctx.define.name}`, logger: this.ctx.logger });
|
||||
}
|
||||
}
|
||||
|
||||
async onRequest(req: AccessRequestHandleReq) {
|
||||
|
||||
@@ -47,7 +47,7 @@ export function AccessInput(input?: AccessInputDefine): PropertyDecorator {
|
||||
};
|
||||
}
|
||||
|
||||
export async function newAccess(type: string, input: any, accessService: IAccessService, ctx?: AccessContext) {
|
||||
export async function newAccess(type: string, input: any, accessService: IAccessService, ctx: AccessContext) {
|
||||
const register = accessRegistry.get(type);
|
||||
if (register == null) {
|
||||
throw new Error(`access ${type} not found`);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Registrable } from "../registry/index.js";
|
||||
import { FormItemProps, HistoryResult, Pipeline } from "../dt/index.js";
|
||||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
||||
import * as _ from "lodash-es";
|
||||
import { IEmailService, IRuntimeDepsService, IServiceGetter } from "../service/index.js";
|
||||
import { IEmailService, IServiceGetter, getRuntimeDepsService } from "../service/index.js";
|
||||
|
||||
export type NotificationBody = {
|
||||
userId?: number;
|
||||
@@ -89,16 +89,16 @@ export abstract class BaseNotification implements INotification {
|
||||
ctx!: NotificationContext;
|
||||
http!: HttpClient;
|
||||
logger!: ILogger;
|
||||
runtimeDepsService?: IRuntimeDepsService;
|
||||
|
||||
async importRuntime(specifier: string) {
|
||||
if (!this.runtimeDepsService) {
|
||||
return await import(specifier);
|
||||
}
|
||||
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
|
||||
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
|
||||
}
|
||||
|
||||
async doSend(body: NotificationBody) {
|
||||
if (body.content) {
|
||||
const content = body.content?.replace(/\n/g, " \n");
|
||||
body.content = content;
|
||||
}
|
||||
return await this.send(body);
|
||||
}
|
||||
abstract send(body: NotificationBody): Promise<void>;
|
||||
@@ -109,12 +109,6 @@ export abstract class BaseNotification implements INotification {
|
||||
this.ctx = ctx;
|
||||
this.http = ctx.http;
|
||||
this.logger = ctx.logger;
|
||||
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
|
||||
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
|
||||
}
|
||||
if (this.runtimeDepsService && this.ctx.define?.name) {
|
||||
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `notification:${this.ctx.define.name}`, logger: this.logger });
|
||||
}
|
||||
}
|
||||
setDefine = (define: NotificationDefine) => {
|
||||
this.define = define;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { INotificationService } from "../notification/index.js";
|
||||
import { Registrable } from "../registry/index.js";
|
||||
import { IPluginConfigService } from "../service/config.js";
|
||||
import { TaskEmitter } from "../service/emit.js";
|
||||
import { ICnameProxyService, IEmailService, IRuntimeDepsService, IServiceGetter, IUrlService } from "../service/index.js";
|
||||
import { ICnameProxyService, IEmailService, IServiceGetter, IUrlService, getRuntimeDepsService } from "../service/index.js";
|
||||
|
||||
export type PluginRequestHandleReq<T = any> = {
|
||||
typeName: string;
|
||||
@@ -76,7 +76,7 @@ export type ITaskPlugin = {
|
||||
execute(): Promise<void | string>;
|
||||
onRequest(req: PluginRequestHandleReq<any>): Promise<any>;
|
||||
setCtx(ctx: TaskInstanceContext): Promise<void>;
|
||||
importRuntime?(specifier: string): Promise<any>;
|
||||
importRuntime(specifier: string): Promise<any>;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
@@ -150,13 +150,9 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
|
||||
logger!: ILogger;
|
||||
http!: HttpClient;
|
||||
accessService!: IAccessService;
|
||||
runtimeDepsService!: IRuntimeDepsService;
|
||||
|
||||
async importRuntime(specifier: string) {
|
||||
if (!this.runtimeDepsService) {
|
||||
throw new Error("runtimeDepsService 未初始化");
|
||||
}
|
||||
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
|
||||
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
|
||||
}
|
||||
|
||||
clearLastStatus() {
|
||||
@@ -178,12 +174,6 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
|
||||
this.logger = ctx.logger;
|
||||
this.accessService = ctx.accessService;
|
||||
this.http = ctx.http;
|
||||
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
|
||||
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
|
||||
}
|
||||
if (this.runtimeDepsService && this.ctx.define?.name) {
|
||||
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `plugin:${this.ctx.define.name}`, logger: this.logger });
|
||||
}
|
||||
// 将证书加入secret
|
||||
// @ts-ignore
|
||||
if (this.cert && this.cert.crt && this.cert.key) {
|
||||
|
||||
+57
-222
@@ -1,38 +1,35 @@
|
||||
import assert from "assert";
|
||||
import assert from "assert";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { RuntimeDepsService, type RuntimeDependencyPluginDefine } from "./runtime-deps-service.js";
|
||||
import { accessRegistry, pluginRegistry } from "@certd/pipeline";
|
||||
import { addonRegistry } from "@certd/lib-server";
|
||||
import { RuntimeDepsService, NpmRegistryResolver, type RuntimeDependencyPluginDefine } from "./runtime.js";
|
||||
import { accessRegistry } from "../access/registry.js";
|
||||
import { pluginRegistry } from "../plugin/registry.js";
|
||||
|
||||
describe("RuntimeDepsService", () => {
|
||||
it("detects conflicting dependency ranges across plugins", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
const service = new RuntimeDepsService({}, null);
|
||||
const merged = service.collectDependencies([
|
||||
{ name: "a", dependPackages: { foo: "^1.0.0" } },
|
||||
{ name: "b", dependPackages: { foo: "^1.2.0" } },
|
||||
]);
|
||||
|
||||
assert.deepEqual(merged.dependencies, { foo: "^1.0.0" });
|
||||
assert.equal(merged.conflicts.length, 0);
|
||||
});
|
||||
|
||||
it("reports incompatible dependency ranges", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
const service = new RuntimeDepsService({}, null);
|
||||
const merged = service.collectDependencies([
|
||||
{ name: "a", dependPackages: { foo: "^1.0.0" } },
|
||||
{ name: "b", dependPackages: { foo: "^2.0.0" } },
|
||||
]);
|
||||
|
||||
assert.equal(merged.conflicts.length, 1);
|
||||
assert.equal(merged.conflicts[0].packageName, "foo");
|
||||
});
|
||||
|
||||
it("builds a runtime package manifest in the target directory", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "https://registry.npmmirror.com";
|
||||
@@ -50,18 +47,15 @@ describe("RuntimeDepsService", () => {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
const plugins: RuntimeDependencyPluginDefine[] = [{ name: "a", dependPackages: { foo: "^1.0.0" } }];
|
||||
const result = await service.ensureInstalled({ plugins });
|
||||
|
||||
assert.equal(result.registryUrl, "https://registry.npmmirror.com");
|
||||
assert.ok(fs.existsSync(path.join(rootDir, "package.json")));
|
||||
});
|
||||
|
||||
it("installs direct dependency maps without plugin metadata", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-direct-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
@@ -77,9 +71,7 @@ describe("RuntimeDepsService", () => {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
await service.ensureDependencies({ dependencies: { directPkg: "^1.0.0" } });
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { directPkg: "^1.0.0" });
|
||||
});
|
||||
@@ -91,27 +83,19 @@ describe("RuntimeDepsService", () => {
|
||||
fs.writeFileSync(path.join(rootDir, "package.json"), JSON.stringify({ name: "runtime-root", type: "module" }), "utf8");
|
||||
fs.writeFileSync(path.join(packageDir, "package.json"), JSON.stringify({ name: "runtime-only", type: "module", main: "index.js" }), "utf8");
|
||||
fs.writeFileSync(path.join(packageDir, "index.js"), "export const value = 42;\n", "utf8");
|
||||
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
service.commandRunner = {
|
||||
async run() {
|
||||
throw new Error("install should not run");
|
||||
},
|
||||
} as any;
|
||||
|
||||
const mod = await service.importRuntime("runtime-only");
|
||||
|
||||
assert.equal(mod.value, 42);
|
||||
});
|
||||
|
||||
it("installs configured lazy dependency when import target is missing", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.lazyDependencies = {
|
||||
"lazy-pkg": "^1.2.3",
|
||||
};
|
||||
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "lazy-pkg": "^1.2.3" } }, null);
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
@@ -130,9 +114,7 @@ describe("RuntimeDepsService", () => {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
const mod = await service.importRuntime("lazy-pkg/sub/entry.js");
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { "lazy-pkg": "^1.2.3" });
|
||||
assert.equal(mod.value, 7);
|
||||
@@ -140,11 +122,7 @@ describe("RuntimeDepsService", () => {
|
||||
|
||||
it("resolves scoped package names for lazy imports", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-scoped-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.lazyDependencies = {
|
||||
"@scope/lazy": "^2.0.0",
|
||||
};
|
||||
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "@scope/lazy": "^2.0.0" } }, null);
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
@@ -163,9 +141,7 @@ describe("RuntimeDepsService", () => {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
const mod = await service.importRuntime("@scope/lazy/dist/index.js");
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { "@scope/lazy": "^2.0.0" });
|
||||
assert.equal(mod.scoped, true);
|
||||
@@ -173,55 +149,20 @@ describe("RuntimeDepsService", () => {
|
||||
|
||||
it("reports missing lazy dependency configuration", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-missing-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.lazyDependencies = {};
|
||||
|
||||
const service = new RuntimeDepsService({ rootDir, lazyDependencies: {} }, null);
|
||||
await assert.rejects(() => service.importRuntime("missing-pkg/sub.js"), /未配置懒加载版本: missing-pkg/);
|
||||
});
|
||||
|
||||
it("falls back to project node_modules when lazy dependency is not configured", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.lazyDependencies = {};
|
||||
|
||||
const service = new RuntimeDepsService({ rootDir, lazyDependencies: {} }, null);
|
||||
const mod = await service.importRuntime("dayjs");
|
||||
|
||||
assert.equal(typeof mod.default, "function");
|
||||
});
|
||||
|
||||
it("falls back to project node_modules when lazy install fails", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-install-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.lazyDependencies = {
|
||||
dayjs: "^1.11.7",
|
||||
};
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
return { stdout: "", stderr: "install failed in test", code: 1 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
const mod = await service.importRuntime("dayjs");
|
||||
|
||||
assert.equal(typeof mod.default, "function");
|
||||
});
|
||||
|
||||
it("keeps previously installed dependencies when installing a later plugin", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-merge-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
@@ -237,22 +178,17 @@ describe("RuntimeDepsService", () => {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
await service.ensureInstalled({ plugins: [{ name: "a", pluginType: "deploy", dependPackages: { foo: "^1.0.0" } }] });
|
||||
await service.ensureInstalled({ plugins: [{ name: "b", pluginType: "deploy", dependPackages: { bar: "^2.0.0" } }] });
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, {
|
||||
foo: "^1.0.0",
|
||||
bar: "^2.0.0",
|
||||
});
|
||||
assert.deepEqual(manifest.dependencies, { foo: "^1.0.0", bar: "^2.0.0" });
|
||||
});
|
||||
|
||||
it("includes npm dependencies from dependent plugins", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
it("includes npm dependencies from dependent plugins", async () => {
|
||||
const service = new RuntimeDepsService({}, { accessRegistry, pluginRegistry });
|
||||
accessRegistry.register("runtimeDepsAccess", {
|
||||
define: { name: "runtimeDepsAccess", title: "access", dependPackages: { accessOnly: "^1.0.0" } } as any,
|
||||
target: async () => ({} as any),
|
||||
target: async () => ({}) as any,
|
||||
});
|
||||
try {
|
||||
const resolved = service.resolvePluginDependencies({
|
||||
@@ -262,11 +198,7 @@ describe("RuntimeDepsService", () => {
|
||||
dependPackages: { deployOnly: "^1.0.0" },
|
||||
});
|
||||
const merged = service.collectDependencies(resolved);
|
||||
|
||||
assert.deepEqual(merged.dependencies, {
|
||||
deployOnly: "^1.0.0",
|
||||
accessOnly: "^1.0.0",
|
||||
});
|
||||
assert.deepEqual(merged.dependencies, { deployOnly: "^1.0.0", accessOnly: "^1.0.0" });
|
||||
} finally {
|
||||
accessRegistry.unRegister("runtimeDepsAccess");
|
||||
}
|
||||
@@ -274,8 +206,7 @@ describe("RuntimeDepsService", () => {
|
||||
|
||||
it("installs dependencies by registered plugin key", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-key-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
const service = new RuntimeDepsService({ rootDir }, { pluginRegistry, accessRegistry });
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
@@ -293,11 +224,11 @@ describe("RuntimeDepsService", () => {
|
||||
} as any;
|
||||
pluginRegistry.register("runtimeDepsKey", {
|
||||
define: { name: "runtimeDepsKey", title: "key", dependPackages: { keyed: "^1.0.0" } } as any,
|
||||
target: async () => ({} as any),
|
||||
target: async () => ({}) as any,
|
||||
});
|
||||
try {
|
||||
service.setRegistries({ pluginRegistry, accessRegistry });
|
||||
await service.ensureRuntimeDependencies({ pluginKeys: "plugin:runtimeDepsKey" });
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { keyed: "^1.0.0" });
|
||||
} finally {
|
||||
@@ -305,58 +236,16 @@ describe("RuntimeDepsService", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("installs dependencies from multiple plugin keys including addon subtype keys", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-keys-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
accessRegistry.register("runtimeDepsArrayAccess", {
|
||||
define: { name: "runtimeDepsArrayAccess", title: "access", dependPackages: { accessPkg: "^1.0.0" } } as any,
|
||||
target: async () => ({} as any),
|
||||
});
|
||||
addonRegistry.register("captcha:runtimeDepsArrayAddon", {
|
||||
define: { addonType: "captcha", name: "runtimeDepsArrayAddon", title: "addon", dependPackages: { addonPkg: "^2.0.0" } } as any,
|
||||
target: async () => ({} as any),
|
||||
});
|
||||
try {
|
||||
await service.ensureRuntimeDependencies({ pluginKeys: ["access:runtimeDepsArrayAccess", "addon:captcha:runtimeDepsArrayAddon"] });
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, {
|
||||
accessPkg: "^1.0.0",
|
||||
addonPkg: "^2.0.0",
|
||||
});
|
||||
} finally {
|
||||
accessRegistry.unRegister("runtimeDepsArrayAccess");
|
||||
addonRegistry.unRegister("captcha:runtimeDepsArrayAddon");
|
||||
}
|
||||
});
|
||||
|
||||
it("reports missing dependent plugins", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
|
||||
const service = new RuntimeDepsService({}, { accessRegistry, pluginRegistry });
|
||||
assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { "access:access": "*" } }), /插件依赖缺失/);
|
||||
});
|
||||
|
||||
it("reports incompatible dependent plugin versions", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
const service = new RuntimeDepsService({}, { accessRegistry, pluginRegistry });
|
||||
accessRegistry.register("runtimeDepsVersionedAccess", {
|
||||
define: { name: "runtimeDepsVersionedAccess", title: "access", version: "1.4.0", dependPackages: { accessOnly: "^1.0.0" } } as any,
|
||||
target: async () => ({} as any),
|
||||
target: async () => ({}) as any,
|
||||
});
|
||||
try {
|
||||
assert.throws(
|
||||
@@ -374,73 +263,10 @@ describe("RuntimeDepsService", () => {
|
||||
});
|
||||
|
||||
it("reports bare dependent plugin names as invalid format", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
|
||||
const service = new RuntimeDepsService({}, null);
|
||||
assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { runtimeDepsBareName: "*" } }), /插件依赖格式错误/);
|
||||
});
|
||||
|
||||
it("records runtime install environment state", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-state-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
assert.equal(args[0], "install");
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
await service.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] });
|
||||
|
||||
const state = JSON.parse(fs.readFileSync(path.join(rootDir, "install-state.json"), "utf8"));
|
||||
assert.equal(state.nodeVersion, process.version);
|
||||
assert.equal(state.pnpmVersion, "9.1.0");
|
||||
assert.equal(state.lastError, undefined);
|
||||
});
|
||||
|
||||
it("serializes installs with a file lock", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lock-"));
|
||||
const serviceA = new RuntimeDepsService();
|
||||
const serviceB = new RuntimeDepsService();
|
||||
for (const service of [serviceA, serviceB]) {
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
let installCount = 0;
|
||||
const commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
assert.equal(args[0], "install");
|
||||
installCount++;
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
};
|
||||
serviceA.commandRunner = commandRunner as any;
|
||||
serviceB.commandRunner = commandRunner as any;
|
||||
|
||||
await Promise.all([serviceA.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] }), serviceB.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] })]);
|
||||
|
||||
assert.equal(installCount, 1);
|
||||
});
|
||||
|
||||
it("does not pass node debugger options to pnpm child process", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-env-"));
|
||||
const oldNodeOptions = process.env.NODE_OPTIONS;
|
||||
@@ -448,8 +274,7 @@ describe("RuntimeDepsService", () => {
|
||||
process.env.NODE_OPTIONS = "--inspect=127.0.0.1:9229 --max-old-space-size=4096";
|
||||
process.env.VSCODE_INSPECTOR_OPTIONS = '{"inspectorIpc":"test"}';
|
||||
try {
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
@@ -467,7 +292,6 @@ describe("RuntimeDepsService", () => {
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
await service.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] });
|
||||
} finally {
|
||||
if (oldNodeOptions == null) {
|
||||
@@ -483,27 +307,38 @@ describe("RuntimeDepsService", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.skip("clears runtime dependency directory", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-"));
|
||||
const runtimeRootDir = path.join(rootDir, ".runtime-deps");
|
||||
fs.mkdirSync(path.join(runtimeRootDir, "node_modules", "foo"), { recursive: true });
|
||||
fs.writeFileSync(path.join(runtimeRootDir, "package.json"), "{}", "utf8");
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = runtimeRootDir;
|
||||
service.installTimeoutMs = 1000;
|
||||
|
||||
await service.clearRuntimeDeps();
|
||||
|
||||
assert.equal(fs.existsSync(runtimeRootDir), true);
|
||||
const remainingEntries = fs.readdirSync(runtimeRootDir).filter(e => e !== ".install.lock");
|
||||
assert.equal(remainingEntries.length, 0);
|
||||
});
|
||||
|
||||
it("rejects clearing unexpected runtime dependency path", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-invalid-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
await assert.rejects(() => service.clearRuntimeDeps(), /动态依赖目录配置异常/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("NpmRegistryResolver", () => {
|
||||
it("chooses the fastest successful registry in auto mode", async () => {
|
||||
const resolver = new NpmRegistryResolver({
|
||||
mode: "auto",
|
||||
candidates: ["https://slow.example.com", "https://fast.example.com"],
|
||||
probeTimeoutMs: 100,
|
||||
cacheTtlMs: 1000,
|
||||
});
|
||||
resolver.probe = async (registryUrl: string) => ({
|
||||
registryUrl,
|
||||
ok: true,
|
||||
elapsedMs: registryUrl.includes("fast") ? 10 : 50,
|
||||
});
|
||||
const result = await resolver.resolve();
|
||||
assert.equal(result, "https://fast.example.com");
|
||||
});
|
||||
|
||||
it("uses fixed registry without probing", async () => {
|
||||
const resolver = new NpmRegistryResolver({
|
||||
mode: "fixed",
|
||||
fixedUrl: "https://registry.example.com",
|
||||
probeTimeoutMs: 100,
|
||||
cacheTtlMs: 1000,
|
||||
});
|
||||
const result = await resolver.resolve();
|
||||
assert.equal(result, "https://registry.example.com");
|
||||
});
|
||||
});
|
||||
@@ -1,27 +1,773 @@
|
||||
/**
|
||||
* 运行时动态导入函数类型
|
||||
*/
|
||||
export type ImportRuntime = (specifier: string, logger?: ILogger) => Promise<any>;
|
||||
|
||||
/**
|
||||
* 日志接口
|
||||
*/
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { spawn } from "child_process";
|
||||
import crypto from "crypto";
|
||||
import { createRequire } from "module";
|
||||
import { pathToFileURL } from "url";
|
||||
import { logger as defaultLogger } from "@certd/basic";
|
||||
import type { Registry } from "../registry/registry.js";
|
||||
export type ILogger = {
|
||||
info: (message: string) => void;
|
||||
warn?: (message: string) => void;
|
||||
error?: (message: string, ...args: any[]) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 运行时依赖服务参数
|
||||
*/
|
||||
export type ImportRuntime = (specifier: string, logger?: ILogger) => Promise<any>;
|
||||
|
||||
export type EnsureRuntimeDepsOptions = {
|
||||
pluginKeys: string | string[];
|
||||
logger?: ILogger;
|
||||
};
|
||||
|
||||
/**
|
||||
* 运行时依赖服务接口
|
||||
*/
|
||||
export interface IRuntimeDepsService {
|
||||
ensureRuntimeDependencies(options: EnsureRuntimeDepsOptions): Promise<any>;
|
||||
importRuntime: ImportRuntime;
|
||||
}
|
||||
|
||||
export type RuntimeDependencyPluginDefine = {
|
||||
name: string;
|
||||
key?: string;
|
||||
title?: string;
|
||||
version?: string;
|
||||
pluginType?: string;
|
||||
addonType?: string;
|
||||
dependPlugins?: Record<string, string>;
|
||||
dependPackages?: Record<string, string>;
|
||||
};
|
||||
|
||||
type RegisteredDefineLike = RuntimeDependencyPluginDefine & {
|
||||
key?: string;
|
||||
pluginType?: string;
|
||||
addonType?: string;
|
||||
dependPlugins?: Record<string, string>;
|
||||
dependPackages?: Record<string, string>;
|
||||
};
|
||||
|
||||
type DependencyConflict = {
|
||||
packageName: string;
|
||||
ranges: Array<{ pluginName: string; range: string }>;
|
||||
};
|
||||
|
||||
type CollectDependenciesResult = {
|
||||
dependencies: Record<string, string>;
|
||||
conflicts: DependencyConflict[];
|
||||
};
|
||||
|
||||
type InstallResult = {
|
||||
registryUrl: string;
|
||||
packageJsonPath: string;
|
||||
};
|
||||
|
||||
type RuntimeImportResolveResult = {
|
||||
resolved: string;
|
||||
packageName: string;
|
||||
};
|
||||
|
||||
type CommandRunnerResult = {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number;
|
||||
};
|
||||
|
||||
type CommandRunner = {
|
||||
run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult>;
|
||||
};
|
||||
|
||||
export type NpmRegistryResolverConfig = {
|
||||
mode?: "auto" | "fixed" | "system";
|
||||
fixedUrl?: string;
|
||||
candidates?: string[];
|
||||
probeTimeoutMs?: number;
|
||||
cacheTtlMs?: number;
|
||||
};
|
||||
|
||||
export type RegistryProbeResult = {
|
||||
registryUrl: string;
|
||||
ok: boolean;
|
||||
elapsedMs: number;
|
||||
};
|
||||
|
||||
export class NpmRegistryResolver {
|
||||
config: NpmRegistryResolverConfig;
|
||||
private cache?: { registryUrl: string; expiresAt: number };
|
||||
|
||||
constructor(config?: NpmRegistryResolverConfig) {
|
||||
this.config = config || {};
|
||||
}
|
||||
|
||||
async resolve(): Promise<string> {
|
||||
const config = this.config;
|
||||
if (config?.mode === "fixed" && config.fixedUrl) {
|
||||
return config.fixedUrl;
|
||||
}
|
||||
if (config?.mode === "system") {
|
||||
return "";
|
||||
}
|
||||
const cached = this.cache;
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.registryUrl;
|
||||
}
|
||||
const candidates = (config?.candidates || []).filter(Boolean);
|
||||
if (candidates.length === 0) {
|
||||
return "";
|
||||
}
|
||||
const probes = await Promise.allSettled(candidates.map(registryUrl => this.probe(registryUrl)));
|
||||
const okList = probes.map(item => (item.status === "fulfilled" ? item.value : null)).filter((item): item is RegistryProbeResult => !!item && item.ok);
|
||||
if (okList.length > 0) {
|
||||
okList.sort((a, b) => a.elapsedMs - b.elapsedMs);
|
||||
const best = okList[0].registryUrl;
|
||||
this.cache = { registryUrl: best, expiresAt: Date.now() + (config?.cacheTtlMs || 6 * 60 * 60 * 1000) };
|
||||
return best;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async probe(registryUrl: string): Promise<RegistryProbeResult> {
|
||||
const timeoutMs = this.config?.probeTimeoutMs || 3000;
|
||||
const started = Date.now();
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const res = await fetch(`${registryUrl.replace(/\/$/, "")}/-/ping`, { signal: controller.signal });
|
||||
return { registryUrl, ok: res.ok, elapsedMs: Date.now() - started };
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
} catch {
|
||||
return { registryUrl, ok: false, elapsedMs: Date.now() - started };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type RuntimeDepsConfig = {
|
||||
rootDir?: string;
|
||||
autoInstall?: boolean;
|
||||
enabled?: boolean;
|
||||
installTimeoutMs?: number;
|
||||
pnpmCommand?: string;
|
||||
lazyDependencies?: Record<string, string>;
|
||||
registry?: NpmRegistryResolverConfig;
|
||||
};
|
||||
|
||||
function normalizeRange(range: string) {
|
||||
return range.trim().replace(/^\^/, "").replace(/^~?/, "");
|
||||
}
|
||||
|
||||
function areRangesCompatible(a: string, b: string) {
|
||||
if (!a || !b) {
|
||||
return true;
|
||||
}
|
||||
if (a === "*" || b === "*") {
|
||||
return true;
|
||||
}
|
||||
const left = normalizeRange(a).split(".");
|
||||
const right = normalizeRange(b).split(".");
|
||||
return left[0] === right[0];
|
||||
}
|
||||
|
||||
const PROCESS_LOCKS = new Map<string, Promise<unknown>>();
|
||||
|
||||
class DefaultCommandRunner implements CommandRunner {
|
||||
async run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult> {
|
||||
return await new Promise<CommandRunnerResult>(resolve => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let settled = false;
|
||||
const child = spawn(command, args, { cwd: options.cwd, env: options.env, windowsHide: true, shell: process.platform === "win32" });
|
||||
const timer = setTimeout(() => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
child.kill("SIGTERM");
|
||||
resolve({ stdout, stderr: stderr || `command timeout after ${options.timeoutMs}ms`, code: 1 });
|
||||
}, options.timeoutMs);
|
||||
child.stdout?.on("data", chunk => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr?.on("data", chunk => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("error", error => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
resolve({ stdout, stderr: error.message, code: 1 });
|
||||
});
|
||||
child.on("close", code => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
resolve({ stdout, stderr, code: code || 0 });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class RuntimeDepsService {
|
||||
runtimeDepsRootDir: string;
|
||||
autoInstall: boolean;
|
||||
enabled: boolean;
|
||||
installTimeoutMs: number;
|
||||
pnpmCommand: string;
|
||||
lazyDependencies: Record<string, string>;
|
||||
registryResolver!: NpmRegistryResolver;
|
||||
commandRunner: CommandRunner = new DefaultCommandRunner();
|
||||
pluginLazyDependencies: Record<string, string> = {};
|
||||
private installPromises = new Map<string, Promise<InstallResult>>();
|
||||
private registriesMap: Record<string, { registry: Registry<any>; pluginType: string; addonType?: string }> | null = null;
|
||||
|
||||
constructor(config: RuntimeDepsConfig, registries: any) {
|
||||
this.runtimeDepsRootDir = config?.rootDir ?? "./data/.runtime-deps";
|
||||
this.autoInstall = config?.autoInstall ?? true;
|
||||
this.enabled = config?.enabled ?? true;
|
||||
this.installTimeoutMs = config?.installTimeoutMs ?? 120000;
|
||||
this.pnpmCommand = config?.pnpmCommand ?? "";
|
||||
this.lazyDependencies = config?.lazyDependencies ?? {};
|
||||
this.registryResolver = new NpmRegistryResolver(config?.registry);
|
||||
if (registries) {
|
||||
this.setRegistries(registries);
|
||||
}
|
||||
}
|
||||
|
||||
setRegistries(registries: { pluginRegistry?: Registry<any>; accessRegistry?: Registry<any>; notificationRegistry?: Registry<any>; dnsProviderRegistry?: Registry<any>; addonRegistry?: Registry<any> }) {
|
||||
const map: Record<string, { registry: Registry<any>; pluginType: string; addonType?: string }> = {};
|
||||
if (registries.pluginRegistry) {
|
||||
map["plugin"] = { registry: registries.pluginRegistry, pluginType: "plugin" };
|
||||
}
|
||||
if (registries.accessRegistry) {
|
||||
map["access"] = { registry: registries.accessRegistry, pluginType: "access" };
|
||||
}
|
||||
if (registries.notificationRegistry) {
|
||||
map["notification"] = { registry: registries.notificationRegistry, pluginType: "notification" };
|
||||
}
|
||||
if (registries.dnsProviderRegistry) {
|
||||
map["dnsProvider"] = { registry: registries.dnsProviderRegistry, pluginType: "dnsProvider" };
|
||||
}
|
||||
if (registries.addonRegistry) {
|
||||
map["addon"] = { registry: registries.addonRegistry, pluginType: "addon", addonType: "" };
|
||||
}
|
||||
this.registriesMap = map;
|
||||
}
|
||||
|
||||
collectDependencies(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
|
||||
const merged: Record<string, string> = {};
|
||||
const seen: Record<string, Array<{ pluginName: string; range: string }>> = {};
|
||||
for (const plugin of plugins) {
|
||||
const deps = plugin.dependPackages || {};
|
||||
for (const [packageName, range] of Object.entries(deps)) {
|
||||
seen[packageName] ||= [];
|
||||
seen[packageName].push({ pluginName: plugin.name, range });
|
||||
}
|
||||
}
|
||||
const conflicts: DependencyConflict[] = [];
|
||||
for (const [packageName, ranges] of Object.entries(seen)) {
|
||||
const first = ranges[0]?.range;
|
||||
if (!first) {
|
||||
continue;
|
||||
}
|
||||
const conflict = ranges.some(item => !areRangesCompatible(first, item.range));
|
||||
if (conflict) {
|
||||
conflicts.push({ packageName, ranges });
|
||||
continue;
|
||||
}
|
||||
merged[packageName] = first;
|
||||
}
|
||||
return { dependencies: merged, conflicts };
|
||||
}
|
||||
|
||||
async ensureInstalled(options: { plugins: RuntimeDependencyPluginDefine[]; logger?: ILogger }): Promise<InstallResult> {
|
||||
const { plugins, logger: log } = options;
|
||||
const { dependencies, conflicts } = this.resolveDependenciesFromPlugins(plugins);
|
||||
if (conflicts.length > 0) {
|
||||
const conflict = conflicts[0];
|
||||
throw new Error(`动态依赖版本冲突: ${conflict.packageName} => ${conflict.ranges.map(item => `${item.pluginName}:${item.range}`).join(", ")}`);
|
||||
}
|
||||
return await this.ensureDependencies({ dependencies, logger: log });
|
||||
}
|
||||
|
||||
async ensureDependencies(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
|
||||
const { dependencies, logger: log } = options;
|
||||
if (!this.enabled) {
|
||||
return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") };
|
||||
}
|
||||
if (!this.autoInstall) {
|
||||
return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") };
|
||||
}
|
||||
const dependenciesHash = this.createDependenciesHash(dependencies);
|
||||
let installPromise = this.installPromises.get(dependenciesHash);
|
||||
if (installPromise) {
|
||||
const nodeModulesPath = path.join(this.getRuntimeDepsRootDir(), "node_modules");
|
||||
if (!fs.existsSync(nodeModulesPath)) {
|
||||
this.installPromises.delete(dependenciesHash);
|
||||
installPromise = undefined;
|
||||
}
|
||||
}
|
||||
if (!installPromise) {
|
||||
installPromise = this.doEnsureInstalled({ dependencies, logger: log }).catch(error => {
|
||||
this.installPromises.delete(dependenciesHash);
|
||||
throw error;
|
||||
});
|
||||
this.installPromises.set(dependenciesHash, installPromise);
|
||||
}
|
||||
return await installPromise;
|
||||
}
|
||||
|
||||
resolveDependenciesFromPlugins(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
|
||||
const expandedPlugins = plugins.flatMap(plugin => this.resolvePluginDependencies(plugin));
|
||||
return this.collectDependencies(expandedPlugins);
|
||||
}
|
||||
|
||||
async ensureRuntimeDependencies(options: { pluginKeys: string | string[]; logger?: ILogger }): Promise<InstallResult> {
|
||||
const { pluginKeys, logger: log } = options;
|
||||
const keys = Array.isArray(pluginKeys) ? pluginKeys : [pluginKeys];
|
||||
const pluginDefines = keys.map(pluginKey => this.getDefineByPluginKey(pluginKey));
|
||||
if (pluginDefines.every(pluginDefine => !pluginDefine.dependPackages && !pluginDefine.dependPlugins)) {
|
||||
return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") };
|
||||
}
|
||||
const expandedPluginDefines = pluginDefines.flatMap(pluginDefine => this.resolvePluginDependencies(pluginDefine));
|
||||
return await this.ensureInstalled({ plugins: expandedPluginDefines, logger: log });
|
||||
}
|
||||
|
||||
async importRuntime(specifier: string, logger: ILogger = defaultLogger) {
|
||||
if (this.isNativeImportSpecifier(specifier)) {
|
||||
return await import(specifier);
|
||||
}
|
||||
const resolved = await this.resolveImportSpecifier(specifier, logger);
|
||||
return await import(pathToFileURL(resolved).href);
|
||||
}
|
||||
|
||||
private async resolveImportSpecifier(specifier: string, logger: ILogger = defaultLogger) {
|
||||
try {
|
||||
return this.resolveRuntimeSpecifier(specifier).resolved;
|
||||
} catch (runtimeError: any) {
|
||||
if (!this.isModuleNotFoundError(runtimeError)) {
|
||||
throw runtimeError;
|
||||
}
|
||||
return await this.resolveMissingRuntimeSpecifier(specifier, runtimeError, logger);
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveMissingRuntimeSpecifier(specifier: string, runtimeError: any, logger?: ILogger) {
|
||||
const packageName = this.parsePackageName(specifier);
|
||||
const mergedDeps = this.getMergedLazyDependencies();
|
||||
const lazyRange = mergedDeps[packageName];
|
||||
if (!lazyRange) {
|
||||
try {
|
||||
return this.resolveProjectSpecifier(specifier, runtimeError).resolved;
|
||||
} catch {
|
||||
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.ensureLazyDependency(packageName, logger);
|
||||
return this.resolveRuntimeSpecifier(specifier).resolved;
|
||||
} catch (lazyError: any) {
|
||||
return this.resolveProjectSpecifier(specifier, lazyError).resolved;
|
||||
}
|
||||
}
|
||||
|
||||
private isNativeImportSpecifier(specifier: string) {
|
||||
return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:");
|
||||
}
|
||||
|
||||
private resolveRuntimeSpecifier(specifier: string): RuntimeImportResolveResult {
|
||||
const packageName = this.parsePackageName(specifier);
|
||||
const packageJsonPath = path.join(this.getRuntimeDepsRootDir(), "package.json");
|
||||
const require = createRequire(packageJsonPath);
|
||||
const resolved = require.resolve(specifier);
|
||||
return { packageName, resolved };
|
||||
}
|
||||
|
||||
private resolveProjectSpecifier(specifier: string, cause?: any): RuntimeImportResolveResult {
|
||||
try {
|
||||
const packageName = this.parsePackageName(specifier);
|
||||
const packageJsonPath = path.resolve("package.json");
|
||||
const require = createRequire(packageJsonPath);
|
||||
const resolved = require.resolve(specifier);
|
||||
return { packageName, resolved };
|
||||
} catch (projectError: any) {
|
||||
if (cause) {
|
||||
projectError.cause = cause;
|
||||
}
|
||||
throw projectError;
|
||||
}
|
||||
}
|
||||
|
||||
private parsePackageName(specifier: string) {
|
||||
if (!specifier || specifier.trim() !== specifier) {
|
||||
throw new Error(`动态依赖导入路径无效: ${specifier}`);
|
||||
}
|
||||
const parts = specifier.split("/");
|
||||
if (specifier.startsWith("@")) {
|
||||
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
||||
throw new Error(`动态依赖导入路径无效: ${specifier}`);
|
||||
}
|
||||
return `${parts[0]}/${parts[1]}`;
|
||||
}
|
||||
if (!parts[0]) {
|
||||
throw new Error(`动态依赖导入路径无效: ${specifier}`);
|
||||
}
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
private async ensureLazyDependency(packageName: string, logger?: ILogger) {
|
||||
const range = this.lazyDependencies?.[packageName];
|
||||
if (!range) {
|
||||
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
|
||||
}
|
||||
await this.ensureDependencies({ dependencies: { [packageName]: range }, logger });
|
||||
}
|
||||
|
||||
private isModuleNotFoundError(error: any) {
|
||||
return error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND";
|
||||
}
|
||||
|
||||
resolvePluginDependencies(current: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine[] {
|
||||
const resolved: RuntimeDependencyPluginDefine[] = [];
|
||||
const visited = new Set<string>();
|
||||
const visit = (item: RuntimeDependencyPluginDefine) => {
|
||||
const key = this.buildPluginDependencyKey(item);
|
||||
if (visited.has(key)) {
|
||||
return;
|
||||
}
|
||||
visited.add(key);
|
||||
resolved.push(item);
|
||||
for (const [dependencyName, expectedRange] of Object.entries(item.dependPlugins || {})) {
|
||||
const dependency = this.getDefineByPluginKey(dependencyName, item);
|
||||
if (!isPluginVersionCompatible(dependency, expectedRange)) {
|
||||
throw new Error(`插件依赖版本冲突: ${item.name} 依赖 ${dependencyName}@${expectedRange},当前版本为 ${dependency.version || "未声明"}`);
|
||||
}
|
||||
visit(dependency);
|
||||
}
|
||||
};
|
||||
visit(current);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private buildPluginDependencyKey(plugin: RuntimeDependencyPluginDefine) {
|
||||
if (plugin.pluginType === "addon" && plugin.addonType) {
|
||||
return `addon:${plugin.addonType}:${plugin.name}`;
|
||||
}
|
||||
const pluginType = plugin.pluginType === "deploy" ? "plugin" : plugin.pluginType || "unknown";
|
||||
return `${pluginType}:${plugin.name}`;
|
||||
}
|
||||
|
||||
private getDefineByPluginKey(pluginKey: string, owner?: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine {
|
||||
const parts = pluginKey.split(":");
|
||||
let pluginType: string, name: string, subtype: string | undefined;
|
||||
if (parts.length === 2) {
|
||||
[pluginType, name] = parts;
|
||||
} else if (parts.length === 3) {
|
||||
[pluginType, subtype, name] = parts;
|
||||
} else {
|
||||
const ownerName = owner?.name || pluginKey;
|
||||
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey}`);
|
||||
}
|
||||
if (!this.registriesMap) {
|
||||
throw new Error("注册表未设置,请先调用 setRegistries");
|
||||
}
|
||||
const target = this.registriesMap[pluginType];
|
||||
if (!target) {
|
||||
const ownerName = owner?.name || pluginKey;
|
||||
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},未知插件类型 ${pluginType}`);
|
||||
}
|
||||
// addon 类型的 key 需要包含 subtype
|
||||
const registryKey = pluginType === "addon" && subtype ? `${subtype}:${name}` : name;
|
||||
const define = target.registry.getDefine(registryKey) as RegisteredDefineLike;
|
||||
if (!define) {
|
||||
throw new Error(`插件依赖缺失: ${owner?.name || pluginKey} 依赖 ${pluginKey},但该插件未注册或已禁用`);
|
||||
}
|
||||
return { ...define, key: pluginKey, pluginType: target.pluginType, addonType: target.addonType };
|
||||
}
|
||||
|
||||
private async doEnsureInstalled(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
|
||||
let { dependencies } = options;
|
||||
const log = options.logger || defaultLogger;
|
||||
return await this.withInstallLock(async () => {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
const packageJsonPath = path.join(rootDir, "package.json");
|
||||
const lockPath = path.join(rootDir, "pnpm-lock.yaml");
|
||||
log.info(`第三方依赖安装: ${JSON.stringify(dependencies)}`);
|
||||
dependencies = this.mergeInstalledDependencies(this.readManifestDependencies(packageJsonPath), dependencies);
|
||||
const dependenciesHash = this.createDependenciesHash(dependencies);
|
||||
const statePath = path.join(rootDir, "install-state.json");
|
||||
const currentState = this.readInstallState(statePath);
|
||||
if (currentState?.dependenciesHash === dependenciesHash && fs.existsSync(path.join(rootDir, "node_modules"))) {
|
||||
log.info("第三方依赖已安装");
|
||||
return { registryUrl: currentState.registryUrl || "", packageJsonPath };
|
||||
}
|
||||
const manifest = { name: "certd-runtime-deps", private: true, type: "module", dependencies };
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(manifest, null, 2), "utf8");
|
||||
const registryUrl = await this.registryResolver.resolve();
|
||||
const env = this.buildChildEnv(registryUrl);
|
||||
const command = this.getPnpmCommand();
|
||||
const pnpmVersion = await this.getPnpmVersion(command, env);
|
||||
const args = ["install", "--prod", "--ignore-scripts", "--ignore-workspace", "--no-frozen-lockfile", "--reporter=append-only"];
|
||||
if (registryUrl) {
|
||||
args.push(`--registry=${registryUrl}`);
|
||||
}
|
||||
log.info(`开始安装第三方依赖: ${Object.keys(dependencies).join(", ")}`);
|
||||
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env });
|
||||
if (result.code !== 0) {
|
||||
const message = result.stderr || result.stdout || "unknown error";
|
||||
this.writeInstallState(statePath, {
|
||||
...currentState,
|
||||
installedAt: currentState?.installedAt,
|
||||
failedAt: new Date().toISOString(),
|
||||
registryUrl,
|
||||
dependenciesHash,
|
||||
nodeVersion: process.version,
|
||||
pnpmVersion,
|
||||
lockFileExists: fs.existsSync(lockPath),
|
||||
lastError: message,
|
||||
});
|
||||
throw new Error(`动态依赖安装失败: ${message}`);
|
||||
}
|
||||
this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) });
|
||||
log.info("第三方依赖安装完成");
|
||||
return { registryUrl, packageJsonPath };
|
||||
});
|
||||
}
|
||||
|
||||
private async withInstallLock<T>(run: () => Promise<T>): Promise<T> {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
fs.mkdirSync(rootDir, { recursive: true });
|
||||
const lockFile = path.join(rootDir, ".install.lock");
|
||||
const previous = PROCESS_LOCKS.get(lockFile);
|
||||
if (previous) {
|
||||
await previous.catch(() => undefined);
|
||||
}
|
||||
let releaseProcessLock!: () => void;
|
||||
const current = new Promise<void>(resolve => {
|
||||
releaseProcessLock = resolve;
|
||||
});
|
||||
PROCESS_LOCKS.set(lockFile, current);
|
||||
let fd: number | undefined;
|
||||
try {
|
||||
fd = await this.acquireFileLock(lockFile);
|
||||
return await run();
|
||||
} finally {
|
||||
if (fd != null) {
|
||||
fs.closeSync(fd);
|
||||
try {
|
||||
fs.rmSync(lockFile, { force: true });
|
||||
} catch {
|
||||
try {
|
||||
fs.rmSync(lockFile, { force: true });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
releaseProcessLock();
|
||||
if (PROCESS_LOCKS.get(lockFile) === current) {
|
||||
PROCESS_LOCKS.delete(lockFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async acquireFileLock(lockFile: string) {
|
||||
const deadline = Date.now() + this.installTimeoutMs;
|
||||
while (true) {
|
||||
try {
|
||||
const fd = fs.openSync(lockFile, "wx");
|
||||
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), "utf8");
|
||||
return fd;
|
||||
} catch (error: any) {
|
||||
if (error?.code !== "EEXIST") {
|
||||
throw error;
|
||||
}
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(`动态依赖安装锁等待超时: ${lockFile}`);
|
||||
}
|
||||
await this.waitForExternalLock(lockFile, deadline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForExternalLock(lockFile: string, deadline: number) {
|
||||
while (fs.existsSync(lockFile)) {
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(`动态依赖安装锁等待超时: ${lockFile}`);
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
}
|
||||
}
|
||||
|
||||
async clearRuntimeDeps() {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
const normalizedRootDir = path.normalize(rootDir);
|
||||
if (!normalizedRootDir.endsWith(path.normalize(".runtime-deps"))) {
|
||||
throw new Error(`动态依赖目录配置异常,拒绝清理: ${rootDir}`);
|
||||
}
|
||||
await this.withInstallLock(async () => {
|
||||
if (fs.existsSync(rootDir)) {
|
||||
const entries = fs.readdirSync(rootDir);
|
||||
for (const entry of entries) {
|
||||
if (entry === ".install.lock") {
|
||||
continue;
|
||||
}
|
||||
fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
this.installPromises.clear();
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
getMergedLazyDependencies(): Record<string, string> {
|
||||
return { ...this.lazyDependencies, ...this.pluginLazyDependencies };
|
||||
}
|
||||
|
||||
collectPluginDeps(logger?: ILogger) {
|
||||
if (!this.registriesMap) {
|
||||
return;
|
||||
}
|
||||
const deps: Record<string, string> = {};
|
||||
for (const { registry } of Object.values(this.registriesMap)) {
|
||||
const defineList = registry.getDefineList();
|
||||
for (const define of defineList) {
|
||||
const dependPackages = (define as any).dependPackages as Record<string, string> | undefined;
|
||||
if (!dependPackages) {
|
||||
continue;
|
||||
}
|
||||
for (const [pkgName, range] of Object.entries(dependPackages)) {
|
||||
const existing = deps[pkgName];
|
||||
if (existing && !areRangesCompatible(existing, range)) {
|
||||
(logger || defaultLogger).warn?.(`懒加载依赖版本冲突: ${pkgName} => ${existing} vs ${range},保留已有版本`);
|
||||
continue;
|
||||
}
|
||||
deps[pkgName] = range;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.pluginLazyDependencies = deps;
|
||||
(logger || defaultLogger).info(`从插件注册表收集到 ${Object.keys(deps).length} 个懒加载依赖`);
|
||||
}
|
||||
|
||||
refreshPluginDeps(logger?: ILogger) {
|
||||
this.collectPluginDeps(logger);
|
||||
}
|
||||
|
||||
private readInstallState(statePath: string): any {
|
||||
if (!fs.existsSync(statePath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(statePath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private writeInstallState(statePath: string, state: any) {
|
||||
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8");
|
||||
}
|
||||
|
||||
private readManifestDependencies(packageJsonPath: string): Record<string, string> {
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
return manifest.dependencies || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
private mergeInstalledDependencies(installed: Record<string, string>, requested: Record<string, string>) {
|
||||
const dependencies = { ...installed };
|
||||
for (const [packageName, range] of Object.entries(requested)) {
|
||||
const installedRange = dependencies[packageName];
|
||||
if (installedRange && !areRangesCompatible(installedRange, range)) {
|
||||
throw new Error(`动态依赖版本冲突: ${packageName} => installed:${installedRange}, requested:${range}`);
|
||||
}
|
||||
dependencies[packageName] = installedRange || range;
|
||||
}
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
private async getPnpmVersion(command: string, env: NodeJS.ProcessEnv) {
|
||||
const result = await this.commandRunner.run(command, ["--version"], { cwd: this.getRuntimeDepsRootDir(), timeoutMs: Math.min(this.installTimeoutMs, 10000), env });
|
||||
if (result.code !== 0) {
|
||||
return "";
|
||||
}
|
||||
return (result.stdout || result.stderr || "").trim();
|
||||
}
|
||||
|
||||
private getPnpmCommand() {
|
||||
return this.pnpmCommand || "pnpm";
|
||||
}
|
||||
|
||||
private buildChildEnv(registryUrl: string) {
|
||||
const env = { ...process.env };
|
||||
for (const key of ["NODE_OPTIONS", "VSCODE_INSPECTOR_OPTIONS", "NODE_INSPECTOR_PORT", "NODE_DEBUG"]) {
|
||||
if (!env[key]) {
|
||||
continue;
|
||||
}
|
||||
if (key === "NODE_OPTIONS") {
|
||||
env[key] = this.stripDebugNodeOptions(env[key] as string);
|
||||
} else {
|
||||
delete env[key];
|
||||
}
|
||||
}
|
||||
if (registryUrl) {
|
||||
env.npm_config_registry = registryUrl;
|
||||
env.pnpm_config_registry = registryUrl;
|
||||
}
|
||||
env.CI = env.CI || "true";
|
||||
env.npm_config_confirm_modules_purge = "false";
|
||||
env.pnpm_config_confirm_modules_purge = "false";
|
||||
return env;
|
||||
}
|
||||
|
||||
private stripDebugNodeOptions(value: string) {
|
||||
return value
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.filter(item => !/^--inspect(-brk|-port)?(=|$)/.test(item))
|
||||
.filter(item => !/^--debug(=|$)/.test(item))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
getRuntimeDepsRootDir() {
|
||||
return path.resolve(this.runtimeDepsRootDir);
|
||||
}
|
||||
|
||||
private createDependenciesHash(dependencies: Record<string, string>) {
|
||||
return crypto.createHash("sha256").update(JSON.stringify(dependencies)).digest("hex");
|
||||
}
|
||||
}
|
||||
|
||||
function isPluginVersionCompatible(plugin: RuntimeDependencyPluginDefine, expectedRange: string) {
|
||||
if (!expectedRange || expectedRange === "*") {
|
||||
return true;
|
||||
}
|
||||
if (!plugin.version) {
|
||||
return false;
|
||||
}
|
||||
return areRangesCompatible(expectedRange, plugin.version);
|
||||
}
|
||||
|
||||
let runtimeDepsServiceInstance: RuntimeDepsService | null = null;
|
||||
|
||||
export function initRuntimeDepsService(config: RuntimeDepsConfig, registries: any): RuntimeDepsService {
|
||||
runtimeDepsServiceInstance = new RuntimeDepsService(config, registries);
|
||||
return runtimeDepsServiceInstance;
|
||||
}
|
||||
|
||||
export function getRuntimeDepsService(): RuntimeDepsService {
|
||||
if (!runtimeDepsServiceInstance) {
|
||||
throw new Error("RuntimeDepsService 未初始化");
|
||||
}
|
||||
return runtimeDepsServiceInstance!;
|
||||
}
|
||||
|
||||
export async function importRuntime(specifier: string, logger: ILogger = defaultLogger): Promise<any> {
|
||||
return getRuntimeDepsService().importRuntime(specifier, logger);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-huawei",
|
||||
"private": false,
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"main": "./dist/bundle.js",
|
||||
"module": "./dist/bundle.js",
|
||||
"types": "./dist/d/index.d.ts",
|
||||
@@ -26,9 +26,10 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
"esmock": "^2.7.5",
|
||||
"prettier": "3.3.3",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"gitHead": "b46948c0ba67069ebcd2bb71b21b6c289d99f1b8"
|
||||
"gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-iframe",
|
||||
"private": false,
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -37,5 +37,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "b46948c0ba67069ebcd2bb71b21b6c289d99f1b8"
|
||||
"gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/jdcloud",
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"description": "jdcloud openApi sdk",
|
||||
"main": "./dist/bundle.js",
|
||||
"module": "./dist/bundle.js",
|
||||
@@ -35,6 +35,7 @@
|
||||
"chai": "^5.1.0",
|
||||
"config": "^1.30.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
"esmock": "^2.7.5",
|
||||
"js-yaml": "^3.11.0",
|
||||
"mocha": "^10.6.0",
|
||||
@@ -62,5 +63,5 @@
|
||||
"fetch"
|
||||
]
|
||||
},
|
||||
"gitHead": "b46948c0ba67069ebcd2bb71b21b6c289d99f1b8"
|
||||
"gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-k8s",
|
||||
"private": false,
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -21,7 +21,7 @@
|
||||
"lint": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.42.1",
|
||||
"@certd/basic": "^1.42.5",
|
||||
"@kubernetes/client-node": "0.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -38,5 +38,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "b46948c0ba67069ebcd2bb71b21b6c289d99f1b8"
|
||||
"gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
|
||||
}
|
||||
|
||||
@@ -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.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-server
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-server
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/lib-server",
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"description": "midway with flyway, sql upgrade way ",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
@@ -29,11 +29,11 @@
|
||||
],
|
||||
"license": "AGPL",
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^1.42.1",
|
||||
"@certd/basic": "^1.42.1",
|
||||
"@certd/pipeline": "^1.42.1",
|
||||
"@certd/plugin-lib": "^1.42.1",
|
||||
"@certd/plus-core": "^1.42.1",
|
||||
"@certd/acme-client": "^1.42.5",
|
||||
"@certd/basic": "^1.42.5",
|
||||
"@certd/pipeline": "^1.42.5",
|
||||
"@certd/plugin-lib": "^1.42.5",
|
||||
"@certd/plus-core": "^1.42.5",
|
||||
"@midwayjs/cache": "3.14.0",
|
||||
"@midwayjs/core": "3.20.11",
|
||||
"@midwayjs/i18n": "3.20.13",
|
||||
@@ -69,5 +69,5 @@
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "b46948c0ba67069ebcd2bb71b21b6c289d99f1b8"
|
||||
"gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ export abstract class BaseService<T> {
|
||||
return item != null && item != "";
|
||||
});
|
||||
}
|
||||
async batchDelete(ids: number[], userId: number, projectId?: number) {
|
||||
async batchDelete(ids: number[], userId: number, projectId?: number): Promise<number> {
|
||||
ids = this.filterIds(ids);
|
||||
if (userId != null) {
|
||||
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
|
||||
@@ -295,6 +295,7 @@ export abstract class BaseService<T> {
|
||||
}
|
||||
|
||||
await this.delete(ids);
|
||||
return ids.length;
|
||||
}
|
||||
|
||||
async findOne(options: FindOneOptions<T>) {
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
import { IAccessService, IRuntimeDepsService } from "@certd/pipeline";
|
||||
|
||||
export type AccessRuntimeDepsService = IRuntimeDepsService;
|
||||
import { IAccessService } from "@certd/pipeline";
|
||||
|
||||
export class AccessGetter implements IAccessService {
|
||||
userId: number;
|
||||
projectId?: number;
|
||||
runtimeDepsService?: AccessRuntimeDepsService;
|
||||
getter: <T>(id: any, userId?: number, projectId?: number, ignorePermission?: boolean, runtimeDepsService?: AccessRuntimeDepsService) => Promise<T>;
|
||||
constructor(
|
||||
userId: number,
|
||||
projectId: number,
|
||||
getter: (id: any, userId: number, projectId?: number, ignorePermission?: boolean, runtimeDepsService?: AccessRuntimeDepsService) => Promise<any>,
|
||||
runtimeDepsService?: AccessRuntimeDepsService
|
||||
) {
|
||||
getter: <T>(id: any, userId?: number, projectId?: number, ignorePermission?: boolean) => Promise<T>;
|
||||
constructor(userId: number, projectId: number, getter: (id: any, userId: number, projectId?: number, ignorePermission?: boolean) => Promise<any>) {
|
||||
this.userId = userId;
|
||||
this.projectId = projectId;
|
||||
this.getter = getter;
|
||||
this.runtimeDepsService = runtimeDepsService;
|
||||
}
|
||||
|
||||
async getById<T = any>(id: any) {
|
||||
return await this.getter<T>(id, this.userId, this.projectId, false, this.runtimeDepsService);
|
||||
return await this.getter<T>(id, this.userId, this.projectId, false);
|
||||
}
|
||||
|
||||
async getCommonById<T = any>(id: any) {
|
||||
return await this.getter<T>(id, 0, null, false, this.runtimeDepsService);
|
||||
return await this.getter<T>(id, 0, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { ApplicationContext, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import type { IMidwayContainer } from "@midwayjs/core";
|
||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||
import { In, Repository } from "typeorm";
|
||||
import { AccessGetter, BaseService, PageReq, PermissionException, ValidateException } from "../../../index.js";
|
||||
import type { AccessRuntimeDepsService } from "./access-getter.js";
|
||||
import { AccessEntity } from "../entity/access.js";
|
||||
import { AccessDefine, accessRegistry, newAccess } from "@certd/pipeline";
|
||||
import { EncryptService } from "./encrypt-service.js";
|
||||
@@ -20,6 +20,9 @@ export class AccessService extends BaseService<AccessEntity> {
|
||||
@Inject()
|
||||
encryptService: EncryptService;
|
||||
|
||||
@ApplicationContext()
|
||||
applicationContext: IMidwayContainer;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
getRepository() {
|
||||
@@ -161,7 +164,7 @@ export class AccessService extends BaseService<AccessEntity> {
|
||||
};
|
||||
}
|
||||
|
||||
async getAccessById(id: any, checkUserId: boolean, userId?: number, projectId?: number, runtimeDepsService?: AccessRuntimeDepsService): Promise<any> {
|
||||
async getAccessById(id: any, checkUserId: boolean, userId?: number, projectId?: number): Promise<any> {
|
||||
const entity = await this.info(id);
|
||||
if (entity == null) {
|
||||
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${id}`);
|
||||
@@ -184,20 +187,23 @@ export class AccessService extends BaseService<AccessEntity> {
|
||||
id: entity.id,
|
||||
...setting,
|
||||
};
|
||||
const taskServiceBuilder: any = await this.applicationContext.getAsync("taskServiceBuilder");
|
||||
const serviceGetter = taskServiceBuilder.create({ userId: userId || 0, projectId });
|
||||
const getAccessById = this.getById.bind(this);
|
||||
const accessGetter = new AccessGetter(userId, projectId, getAccessById, runtimeDepsService);
|
||||
const accessGetter = new AccessGetter(userId, projectId, getAccessById);
|
||||
const accessContext = {
|
||||
logger,
|
||||
http,
|
||||
utils,
|
||||
accessService: accessGetter,
|
||||
serviceGetter,
|
||||
} as any;
|
||||
const access = await newAccess(entity.type, input, accessGetter, accessContext);
|
||||
return access;
|
||||
}
|
||||
|
||||
async getById(id: any, userId: number, projectId?: number, _ignorePermission?: boolean, runtimeDepsService?: AccessRuntimeDepsService): Promise<any> {
|
||||
return await this.getAccessById(id, true, userId, projectId, runtimeDepsService);
|
||||
async getById(id: any, userId: number, projectId?: number, _ignorePermission?: boolean): Promise<any> {
|
||||
return await this.getAccessById(id, true, userId, projectId);
|
||||
}
|
||||
|
||||
decryptAccessEntity(entity: AccessEntity): any {
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
||||
import {upperFirst} from "lodash-es";
|
||||
import {
|
||||
accessRegistry,
|
||||
FormItemProps,
|
||||
IAccessService,
|
||||
IRuntimeDepsService,
|
||||
IServiceGetter,
|
||||
PluginRequestHandleReq,
|
||||
Registrable
|
||||
} from "@certd/pipeline";
|
||||
|
||||
import { upperFirst } from "lodash-es";
|
||||
import { accessRegistry, FormItemProps, IAccessService, IServiceGetter, PluginRequestHandleReq, Registrable, getRuntimeDepsService } from "@certd/pipeline";
|
||||
|
||||
export type AddonRequestHandleReqInput<T = any> = {
|
||||
id?: number;
|
||||
@@ -19,7 +10,7 @@ export type AddonRequestHandleReqInput<T = any> = {
|
||||
|
||||
export type AddonRequestHandleReq<T = any> = {
|
||||
addonType: string;
|
||||
} &PluginRequestHandleReq<AddonRequestHandleReqInput<T>>;
|
||||
} & PluginRequestHandleReq<AddonRequestHandleReqInput<T>>;
|
||||
|
||||
export type AddonInputDefine = FormItemProps & {
|
||||
title: string;
|
||||
@@ -48,8 +39,6 @@ export type AddonInstanceConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
export interface IAddon {
|
||||
ctx: AddonContext;
|
||||
[key: string]: any;
|
||||
@@ -67,13 +56,9 @@ export abstract class BaseAddon implements IAddon {
|
||||
ctx!: AddonContext;
|
||||
http!: HttpClient;
|
||||
logger!: ILogger;
|
||||
runtimeDepsService?: IRuntimeDepsService;
|
||||
|
||||
async importRuntime(specifier: string) {
|
||||
if (!this.runtimeDepsService) {
|
||||
return await import(specifier);
|
||||
}
|
||||
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
|
||||
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
|
||||
}
|
||||
|
||||
title!: string;
|
||||
@@ -85,7 +70,7 @@ export abstract class BaseAddon implements IAddon {
|
||||
if (accessId == null) {
|
||||
throw new Error("您还没有配置授权");
|
||||
}
|
||||
const accessService = await this.ctx.serviceGetter.get<IAccessService>("accessService")
|
||||
const accessService = await this.ctx.serviceGetter.get<IAccessService>("accessService");
|
||||
let res: any = null;
|
||||
if (isCommon) {
|
||||
res = await accessService.getCommonById(accessId);
|
||||
@@ -118,18 +103,12 @@ export abstract class BaseAddon implements IAddon {
|
||||
this.ctx = ctx;
|
||||
this.http = ctx.http;
|
||||
this.logger = ctx.logger;
|
||||
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
|
||||
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
|
||||
}
|
||||
if (this.runtimeDepsService && this.define?.addonType && this.define?.name) {
|
||||
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `addon:${this.define.addonType}:${this.define.name}`, logger: this.logger });
|
||||
}
|
||||
}
|
||||
setDefine = (define:AddonDefine) => {
|
||||
setDefine = (define: AddonDefine) => {
|
||||
this.define = define;
|
||||
};
|
||||
|
||||
async onRequest(req:AddonRequestHandleReq) {
|
||||
async onRequest(req: AddonRequestHandleReq) {
|
||||
if (!req.action) {
|
||||
throw new Error("action is required");
|
||||
}
|
||||
@@ -147,10 +126,8 @@ export abstract class BaseAddon implements IAddon {
|
||||
}
|
||||
throw new Error(`action ${req.action} not found`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export interface IAddonGetter {
|
||||
getById<T = any>(id: any): Promise<T>;
|
||||
getCommonById<T = any>(id: any): Promise<T>;
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3))
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/midway-flyway-js",
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"description": "midway with flyway, sql upgrade way ",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
@@ -52,5 +52,5 @@
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "b46948c0ba67069ebcd2bb71b21b6c289d99f1b8"
|
||||
"gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { QueryRunner, Table } from 'typeorm';
|
||||
import { FlywayHistory } from './entity.js';
|
||||
import * as crypto from 'crypto';
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { QueryRunner, Table } from "typeorm";
|
||||
import { FlywayHistory } from "./entity.js";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
/**
|
||||
* 脚本文件信息
|
||||
@@ -32,10 +32,10 @@ const DefaultLogger = {
|
||||
},
|
||||
};
|
||||
|
||||
let customLogger:any = null;
|
||||
export function setFlywayLogger (logger: any) {
|
||||
let customLogger: any = null;
|
||||
export function setFlywayLogger(logger: any) {
|
||||
customLogger = logger;
|
||||
};
|
||||
}
|
||||
|
||||
export class Flyway {
|
||||
scriptDir;
|
||||
@@ -45,8 +45,8 @@ export class Flyway {
|
||||
connection;
|
||||
logger;
|
||||
constructor(opts: any) {
|
||||
this.scriptDir = opts.scriptDir ?? 'db/migration';
|
||||
this.flywayTableName = opts.flywayTableName ?? 'flyway_history';
|
||||
this.scriptDir = opts.scriptDir ?? "db/migration";
|
||||
this.flywayTableName = opts.flywayTableName ?? "flyway_history";
|
||||
this.baseline = opts.baseline ?? false;
|
||||
this.allowHashNotMatch = opts.allowHashNotMatch ?? false;
|
||||
this.logger = customLogger || opts.logger || DefaultLogger;
|
||||
@@ -54,9 +54,9 @@ export class Flyway {
|
||||
}
|
||||
|
||||
async run(ignores?: (RegExp | string)[]) {
|
||||
this.logger.info('[ midfly ] start-------------');
|
||||
this.logger.info("[ midfly ] start-------------");
|
||||
if (!fs.existsSync(this.scriptDir)) {
|
||||
this.logger.info('[ midfly ] scriptDir<' + this.scriptDir + '> not found');
|
||||
this.logger.info("[ midfly ] scriptDir<" + this.scriptDir + "> not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export class Flyway {
|
||||
continue;
|
||||
}
|
||||
if (!file.isBaseline) {
|
||||
this.logger.info('need exec script file: ', file.script);
|
||||
this.logger.info("need exec script file: ", file.script);
|
||||
//执行sql文件
|
||||
if (/\.sql$/.test(file.script)) {
|
||||
await this.execSql(filepath, queryRunner);
|
||||
@@ -87,7 +87,7 @@ export class Flyway {
|
||||
// await this.execJsOrTs(filepath, t);
|
||||
// }
|
||||
} else {
|
||||
this.logger.info('baseline script file: ', file.script);
|
||||
this.logger.info("baseline script file: ", file.script);
|
||||
}
|
||||
await this.storeSqlExecLog(file.script, filepath, true, queryRunner);
|
||||
await queryRunner.commitTransaction();
|
||||
@@ -95,10 +95,15 @@ export class Flyway {
|
||||
this.logger.error(err);
|
||||
await this.storeSqlExecLog(file.script, filepath, false, queryRunner);
|
||||
await queryRunner.rollbackTransaction();
|
||||
|
||||
if (err.code === "SQLITE_IOERR_WRITE") {
|
||||
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)", file.script);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
this.logger.info('[ midfly ] end-------------');
|
||||
this.logger.info("[ midfly ] end-------------");
|
||||
}
|
||||
|
||||
private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) {
|
||||
@@ -160,17 +165,17 @@ export class Flyway {
|
||||
name: this.flywayTableName,
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
name: "id",
|
||||
type: this.connection.driver.normalizeType({
|
||||
type: this.connection.driver.mappedDataTypes.migrationId,
|
||||
}),
|
||||
isGenerated: true,
|
||||
generationStrategy: 'increment',
|
||||
generationStrategy: "increment",
|
||||
isPrimary: true,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: 'timestamp',
|
||||
name: "timestamp",
|
||||
type: this.connection.driver.normalizeType({
|
||||
type: this.connection.driver.mappedDataTypes.migrationTimestamp,
|
||||
}),
|
||||
@@ -178,23 +183,23 @@ export class Flyway {
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
name: "name",
|
||||
type: this.connection.driver.normalizeType({
|
||||
type: this.connection.driver.mappedDataTypes.migrationName,
|
||||
}),
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: 'hash',
|
||||
name: "hash",
|
||||
type: this.connection.driver.normalizeType({
|
||||
type: this.connection.driver.mappedDataTypes.migrationName,
|
||||
}),
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: 'success',
|
||||
name: "success",
|
||||
type: this.connection.driver.normalizeType({
|
||||
type: 'boolean',
|
||||
type: "boolean",
|
||||
}),
|
||||
isNullable: true,
|
||||
},
|
||||
@@ -210,7 +215,7 @@ export class Flyway {
|
||||
}
|
||||
let ret = false;
|
||||
for (const ignore of ignores) {
|
||||
if (typeof ignore === 'string' && file === ignore) {
|
||||
if (typeof ignore === "string" && file === ignore) {
|
||||
ret = true;
|
||||
break;
|
||||
}
|
||||
@@ -233,20 +238,20 @@ export class Flyway {
|
||||
if (history.hash !== hash && this.allowHashNotMatch === false) {
|
||||
throw new Error(file + `hash conflict ,old: ${history.hash} != new: ${hash}`);
|
||||
}
|
||||
this.logger.info('[ midfly ] script<' + file + '> already executed');
|
||||
this.logger.info("[ midfly ] script<" + file + "> already executed");
|
||||
return true;
|
||||
}
|
||||
this.logger.info('[ midfly ] script<' + file + '> not yet execute');
|
||||
this.logger.info("[ midfly ] script<" + file + "> not yet execute");
|
||||
return false;
|
||||
}
|
||||
|
||||
private async getFileHash(filepath: string) {
|
||||
const content = fs.readFileSync(filepath).toString();
|
||||
return crypto.createHash('md5').update(content.toString()).digest('hex');
|
||||
return crypto.createHash("md5").update(content.toString()).digest("hex");
|
||||
}
|
||||
|
||||
private async execSql(filepath: string, queryRunner: QueryRunner) {
|
||||
this.logger.info('[ midfly ] exec ', filepath);
|
||||
this.logger.info("[ midfly ] exec ", filepath);
|
||||
const content = fs.readFileSync(filepath).toString().trim();
|
||||
const arr = this.splitSql2Array(content);
|
||||
for (const s of arr) {
|
||||
@@ -255,11 +260,11 @@ export class Flyway {
|
||||
}
|
||||
|
||||
private async execOnePart(sql: string, queryRunner: QueryRunner) {
|
||||
this.logger.debug('exec sql index: ', sql);
|
||||
this.logger.debug("exec sql index: ", sql);
|
||||
try {
|
||||
await queryRunner.query(sql);
|
||||
} catch (err: any) {
|
||||
this.logger.error('exec sql error : ', err.message, err);
|
||||
this.logger.error("exec sql error : ", err.message, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -275,11 +280,11 @@ export class Flyway {
|
||||
|
||||
const temp = String(str).trim();
|
||||
|
||||
if (temp === 'null') {
|
||||
if (temp === "null") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const semicolon = ';';
|
||||
const semicolon = ";";
|
||||
const deepChars = ['"', "'"];
|
||||
const splits = [];
|
||||
|
||||
@@ -289,7 +294,7 @@ export class Flyway {
|
||||
|
||||
if (deepChars.indexOf(charAt) >= 0) {
|
||||
//如果是深度char
|
||||
if (i !== 0 && temp.charAt(i - 1) === '\\') {
|
||||
if (i !== 0 && temp.charAt(i - 1) === "\\") {
|
||||
//如果前一个是转义字符,忽略它
|
||||
} else {
|
||||
//说明需要进出深度了
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/plugin-cert",
|
||||
"private": false,
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -20,7 +20,7 @@
|
||||
"lint": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/plugin-lib": "^1.42.1"
|
||||
"@certd/plugin-lib": "^1.42.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.12",
|
||||
@@ -38,5 +38,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "b46948c0ba67069ebcd2bb71b21b6c289d99f1b8"
|
||||
"gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
|
||||
}
|
||||
|
||||
@@ -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.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-lib
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-lib
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/plugin-lib",
|
||||
"private": false,
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -17,9 +17,9 @@
|
||||
"lint": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^1.42.1",
|
||||
"@certd/basic": "^1.42.1",
|
||||
"@certd/pipeline": "^1.42.1",
|
||||
"@certd/acme-client": "^1.42.5",
|
||||
"@certd/basic": "^1.42.5",
|
||||
"@certd/pipeline": "^1.42.5",
|
||||
"dayjs": "^1.11.7",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
@@ -45,5 +45,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "b46948c0ba67069ebcd2bb71b21b6c289d99f1b8"
|
||||
"gitHead": "268cd6cc9cb4f1f3d5d5d77859a82f18f0cb6db7"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, ILogger } from "@certd/basic";
|
||||
import { IAccessService, IRuntimeDepsService, PageRes, PageSearch } from "@certd/pipeline";
|
||||
import { IAccessService, PageRes, PageSearch, getRuntimeDepsService } from "@certd/pipeline";
|
||||
import punycode from "punycode.js";
|
||||
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DnsResolveRecord, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
|
||||
import { dnsProviderRegistry } from "./registry.js";
|
||||
@@ -7,13 +7,9 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
|
||||
ctx!: DnsProviderContext;
|
||||
http!: HttpClient;
|
||||
logger!: ILogger;
|
||||
runtimeDepsService?: IRuntimeDepsService;
|
||||
|
||||
async importRuntime(specifier: string) {
|
||||
if (!this.runtimeDepsService) {
|
||||
throw new Error("runtimeDepsService 未初始化");
|
||||
}
|
||||
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
|
||||
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
|
||||
}
|
||||
|
||||
usePunyCode(): boolean {
|
||||
@@ -42,12 +38,6 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
|
||||
this.ctx = ctx;
|
||||
this.logger = ctx.logger;
|
||||
this.http = ctx.http;
|
||||
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
|
||||
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
|
||||
}
|
||||
if (this.runtimeDepsService && this.ctx.define?.name) {
|
||||
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `dnsProvider:${this.ctx.define.name}`, logger: this.logger });
|
||||
}
|
||||
}
|
||||
|
||||
async parseDomain(fullDomain: string) {
|
||||
|
||||
@@ -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.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/ui-client
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/ui-client
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复企业模式下弹出邮箱绑定提醒的问题 ([8d9dad9](https://github.com/certd/certd/commit/8d9dad9c82f6f2fd3ab3040068946a33f37145b1))
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/ui-client",
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --open",
|
||||
@@ -59,7 +59,6 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"core-js": "^3.36.0",
|
||||
"cos-js-sdk-v5": "^1.7.0",
|
||||
"cron-parser": "^4.9.0",
|
||||
"cropperjs": "^1.6.1",
|
||||
"cssnano": "^7.0.6",
|
||||
@@ -105,8 +104,8 @@
|
||||
"zod-defaults": "^0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/lib-iframe": "^1.42.1",
|
||||
"@certd/pipeline": "^1.42.1",
|
||||
"@certd/lib-iframe": "^1.42.5",
|
||||
"@certd/pipeline": "^1.42.5",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@types/chai": "^4.3.12",
|
||||
|
||||
@@ -15,11 +15,14 @@ import { Modal, notification } from "ant-design-vue";
|
||||
import { useI18n } from "/src/locales";
|
||||
import { request } from "/@/api/service";
|
||||
import { useFormDialog } from "/@/use/use-dialog";
|
||||
import { useSettingStore } from "/@/store/settings/index.jsx";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { openFormDialog } = useFormDialog();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
const changePasswordButtonRef = ref();
|
||||
const emailFormWrapperRef = ref<any>();
|
||||
|
||||
@@ -34,6 +37,9 @@ const validateEmailConfirm = async (_rule: any, value: string) => {
|
||||
};
|
||||
|
||||
async function checkAndSetupAccount() {
|
||||
if (settingStore.isEnterprise) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const userInfo = userStore.getUserInfo as any;
|
||||
if (!userInfo.needInitAccount) {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"ignorePatterns": ["dist"],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/ban-ts-ignore": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
// 允许any
|
||||
"@typescript-eslint/no-unsafe-anyassignment": "off"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,40 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
|
||||
* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
|
||||
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
|
||||
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **volcengine-alb:** 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 ([0a068a2](https://github.com/certd/certd/commit/0a068a274673e9768954e9f7367c267d44f3b530))
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复火山视频点播源站选择不到自定义源站的bug ([0071bcb](https://github.com/certd/certd/commit/0071bcb0e4dd108c86d7ca01820a9f6e6960e440))
|
||||
* 修复企业模式下弹出邮箱绑定提醒的问题 ([8d9dad9](https://github.com/certd/certd/commit/8d9dad9c82f6f2fd3ab3040068946a33f37145b1))
|
||||
* 修复AsiaIsp CDN证书重复情况下部署失败的问题 ([c3d6db3](https://github.com/certd/certd/commit/c3d6db3f1ef2f1c897b7989521fe8809dffaded1))
|
||||
* 修复cname用阿里云校验时报找不到runtimeDepsService的错误 ([072edd7](https://github.com/certd/certd/commit/072edd7affee424ab3411f4d41d338f084d7cac6))
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -96,24 +96,11 @@ input:
|
||||
label: 点播加速域名
|
||||
- value: image
|
||||
label: 封面加速域名
|
||||
- value: third
|
||||
label: 自定义源站
|
||||
value: play
|
||||
required: true
|
||||
order: 0
|
||||
sourceStationType:
|
||||
title: 源站类型
|
||||
helper: 选择源站类型
|
||||
component:
|
||||
name: a-select
|
||||
vModel: value
|
||||
options:
|
||||
- value: 1
|
||||
label: 点播源站
|
||||
- value: 2
|
||||
label: 自定义源站
|
||||
value: 1
|
||||
helper: 注意:封面加速域名不支持自定义源站
|
||||
required: false
|
||||
order: 0
|
||||
domainList:
|
||||
title: 域名
|
||||
component:
|
||||
@@ -132,7 +119,6 @@ input:
|
||||
- accessId
|
||||
- spaceName
|
||||
- domainType
|
||||
- sourceStationType
|
||||
required: true
|
||||
mergeScript: |2-
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/ui-server",
|
||||
"version": "1.42.1",
|
||||
"version": "1.42.5",
|
||||
"description": "fast-server base midway",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -25,6 +25,7 @@
|
||||
"format": "prettier --write src",
|
||||
"lint": "mwts fix",
|
||||
"ci": "pnpm run cov",
|
||||
"lint2": "cross-env NODE_ENV=production mwtsc -p tsconfig.build.json",
|
||||
"build-only": "cross-env NODE_ENV=production mwtsc -p tsconfig.build.json --cleanOutDir --skipLibCheck",
|
||||
"build": "pnpm run build-only && pnpm run export-metadata",
|
||||
"export-metadata": "node export-plugin-yaml.js",
|
||||
@@ -41,20 +42,20 @@
|
||||
"lint1": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^1.42.1",
|
||||
"@certd/basic": "^1.42.1",
|
||||
"@certd/commercial-core": "^1.42.1",
|
||||
"@certd/acme-client": "^1.42.5",
|
||||
"@certd/basic": "^1.42.5",
|
||||
"@certd/commercial-core": "^1.42.5",
|
||||
"@certd/cv4pve-api-javascript": "^8.4.2",
|
||||
"@certd/jdcloud": "^1.42.1",
|
||||
"@certd/lib-huawei": "^1.42.1",
|
||||
"@certd/lib-k8s": "^1.42.1",
|
||||
"@certd/lib-server": "^1.42.1",
|
||||
"@certd/midway-flyway-js": "^1.42.1",
|
||||
"@certd/pipeline": "^1.42.1",
|
||||
"@certd/plugin-cert": "^1.42.1",
|
||||
"@certd/plugin-lib": "^1.42.1",
|
||||
"@certd/plugin-plus": "^1.42.1",
|
||||
"@certd/plus-core": "^1.42.1",
|
||||
"@certd/jdcloud": "^1.42.5",
|
||||
"@certd/lib-huawei": "^1.42.5",
|
||||
"@certd/lib-k8s": "^1.42.5",
|
||||
"@certd/lib-server": "^1.42.5",
|
||||
"@certd/midway-flyway-js": "^1.42.5",
|
||||
"@certd/pipeline": "^1.42.5",
|
||||
"@certd/plugin-cert": "^1.42.5",
|
||||
"@certd/plugin-lib": "^1.42.5",
|
||||
"@certd/plugin-plus": "^1.42.5",
|
||||
"@certd/plus-core": "^1.42.5",
|
||||
"@koa/cors": "^5.0.0",
|
||||
"@midwayjs/bootstrap": "3.20.11",
|
||||
"@midwayjs/cache": "3.14.0",
|
||||
@@ -71,7 +72,6 @@
|
||||
"@peculiar/x509": "^1.11.0",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@simplewebauthn/server": "^13.2.3",
|
||||
"alipay-sdk": "^4.13.0",
|
||||
"axios": "^1.9.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^11.1.2",
|
||||
@@ -96,8 +96,6 @@
|
||||
"nanoid": "^5.0.7",
|
||||
"node-forge": "^1.3.1",
|
||||
"nodemailer": "^6.9.16",
|
||||
"openid-client": "^6.8.1",
|
||||
"otplib": "^12.0.1",
|
||||
"pg": "^8.12.0",
|
||||
"psl": "^1.15.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
@@ -113,7 +111,6 @@
|
||||
"svg-captcha": "^1.4.0",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^10.0.0",
|
||||
"wechatpay-node-v3": "^2.2.1",
|
||||
"whoiser": "2.0.0-beta.10",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
@@ -129,6 +126,7 @@
|
||||
"@types/nodemailer": "^6.4.8",
|
||||
"c8": "^10.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^7.32.0",
|
||||
"esmock": "^2.7.5",
|
||||
"mocha": "^10.6.0",
|
||||
"mwts": "^1.3.0",
|
||||
@@ -170,7 +168,11 @@
|
||||
"@google-cloud/publicca": "^1.3.0",
|
||||
"basic-ftp": "^5.0.5",
|
||||
"esdk-obs-nodejs": "^3.25.6",
|
||||
"qiniu": "^7.12.0"
|
||||
"qiniu": "^7.12.0",
|
||||
"alipay-sdk": "^4.13.0",
|
||||
"wechatpay-node-v3": "^2.2.1",
|
||||
"openid-client": "^6.8.1",
|
||||
"otplib": "^12.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
||||
@@ -132,7 +132,5 @@ export class MainConfiguration {
|
||||
logger.info(text);
|
||||
});
|
||||
logger.info("当前环境:", this.app.getEnv()); // prod
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getRuntimeDepsService } from "@certd/pipeline";
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query, RequestIP } from "@midwayjs/core";
|
||||
import { addonRegistry, AddonService, CrudController, SysPrivateSettings, SysPublicSettings, SysSafeSetting, SysSettingsEntity, SysSettingsService } from "@certd/lib-server";
|
||||
import { cloneDeep, merge } from "lodash-es";
|
||||
@@ -7,7 +8,6 @@ import { getEmailSettings } from "../../../modules/sys/settings/fix.js";
|
||||
import { http, logger, utils } from "@certd/basic";
|
||||
import { CodeService } from "../../../modules/basic/service/code-service.js";
|
||||
import { SmsServiceFactory } from "../../../modules/basic/sms/factory.js";
|
||||
import { RuntimeDepsService } from "../../../modules/runtime-deps/runtime-deps-service.js";
|
||||
|
||||
/**
|
||||
*/
|
||||
@@ -24,9 +24,6 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
|
||||
codeService: CodeService;
|
||||
@Inject()
|
||||
addonService: AddonService;
|
||||
@Inject()
|
||||
runtimeDepsService: RuntimeDepsService;
|
||||
|
||||
getService() {
|
||||
return this.service;
|
||||
}
|
||||
@@ -222,7 +219,7 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
|
||||
|
||||
@Post("/clearRuntimeDeps", { description: "sys:settings:edit" })
|
||||
async clearRuntimeDeps() {
|
||||
await this.runtimeDepsService.clearRuntimeDeps();
|
||||
await getRuntimeDepsService().clearRuntimeDeps();
|
||||
return this.ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccessGetter, AccessService, BaseController, Constants, SysSettingsService } from "@certd/lib-server";
|
||||
import { AccessGetter, AccessService, BaseController, Constants, isEnterprise, SysSettingsService } from "@certd/lib-server";
|
||||
import { ALL, Body, Controller, Inject, Post, Provide } from "@midwayjs/core";
|
||||
import { PasskeyService } from "../../../modules/login/service/passkey-service.js";
|
||||
import { RoleService } from "../../../modules/sys/authority/service/role-service.js";
|
||||
@@ -9,6 +9,7 @@ import { http, logger, utils } from "@certd/basic";
|
||||
import { ApiTags } from "@midwayjs/swagger";
|
||||
import { CodeService } from "../../../modules/basic/service/code-service.js";
|
||||
import { EmailService } from "../../../modules/basic/service/email-service.js";
|
||||
import { TaskServiceBuilder } from "../../../modules/pipeline/service/getter/task-service-getter.js";
|
||||
|
||||
/**
|
||||
*/
|
||||
@@ -40,6 +41,9 @@ export class MineController extends BaseController {
|
||||
@Inject()
|
||||
emailService: EmailService;
|
||||
|
||||
@Inject()
|
||||
taskServiceBuilder: TaskServiceBuilder;
|
||||
|
||||
@Post("/info", { description: Constants.per.authOnly, summary: "查询用户信息" })
|
||||
public async info() {
|
||||
const userId = this.getUserId();
|
||||
@@ -54,14 +58,18 @@ export class MineController extends BaseController {
|
||||
delete user.password;
|
||||
//@ts-ignore
|
||||
user.needInitPassword = needInitPassword;
|
||||
|
||||
const existingAccess = await this.accessService.findOne({
|
||||
where: { type: "acmeAccount", subtype: "letsencrypt", userId },
|
||||
});
|
||||
if (!existingAccess) {
|
||||
//@ts-ignore
|
||||
user.needInitAccount = true;
|
||||
//@ts-ignore
|
||||
user.needInitAccount = false;
|
||||
if (!isEnterprise()) {
|
||||
const existingAccess = await this.accessService.findOne({
|
||||
where: { type: "acmeAccount", subtype: "letsencrypt", userId },
|
||||
});
|
||||
if (!existingAccess) {
|
||||
//@ts-ignore
|
||||
user.needInitAccount = true;
|
||||
}
|
||||
}
|
||||
|
||||
return this.ok(user);
|
||||
}
|
||||
|
||||
@@ -146,8 +154,7 @@ export class MineController extends BaseController {
|
||||
|
||||
@Post("/accountInit", { description: Constants.per.authOnly, summary: "初始化Let's Encrypt ACME账号和邮件通知" })
|
||||
public async accountInit(@Body("email") email?: string) {
|
||||
const { projectId, userId } = await this.getProjectUserIdWrite();
|
||||
|
||||
const userId = this.getUserId();
|
||||
let userEmail = email;
|
||||
let user: any = null;
|
||||
if (!userEmail) {
|
||||
@@ -169,15 +176,17 @@ export class MineController extends BaseController {
|
||||
|
||||
await this.emailService.add(userId, userEmail);
|
||||
|
||||
await this.notificationService.getOrCreateDefault(userEmail, userId, projectId);
|
||||
await this.notificationService.getOrCreateDefault(userEmail, userId);
|
||||
|
||||
const getAccessById = this.accessService.getById.bind(this.accessService);
|
||||
const accessGetter = new AccessGetter(userId, projectId, getAccessById);
|
||||
const accessGetter = new AccessGetter(userId, undefined, getAccessById);
|
||||
const serviceGetter = this.taskServiceBuilder.create({ userId });
|
||||
const accessContext = {
|
||||
http,
|
||||
logger,
|
||||
utils,
|
||||
accessService: accessGetter,
|
||||
serviceGetter,
|
||||
define: undefined,
|
||||
} as any;
|
||||
const access = await newAccess("acmeAccount", { caType: "letsencrypt", email: userEmail }, accessGetter, accessContext);
|
||||
@@ -187,7 +196,7 @@ export class MineController extends BaseController {
|
||||
type: "acmeAccount",
|
||||
name: "Let's Encrypt",
|
||||
userId,
|
||||
projectId,
|
||||
projectId: undefined,
|
||||
setting: JSON.stringify({
|
||||
caType: "letsencrypt",
|
||||
email: userEmail,
|
||||
|
||||
@@ -8,7 +8,6 @@ import { TaskServiceBuilder } from "../../../modules/pipeline/service/getter/tas
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { ApiTags } from "@midwayjs/swagger";
|
||||
import { AuthService } from "../../../modules/sys/authority/service/auth-service.js";
|
||||
import { RuntimeDepsService } from "../../../modules/runtime-deps/runtime-deps-service.js";
|
||||
|
||||
@Provide()
|
||||
@Controller("/api/pi/handle")
|
||||
@@ -29,9 +28,6 @@ export class HandleController extends BaseController {
|
||||
@Inject()
|
||||
notificationService: NotificationService;
|
||||
|
||||
@Inject()
|
||||
runtimeDepsService: RuntimeDepsService;
|
||||
|
||||
@Post("/access", { description: Constants.per.authOnly, summary: "处理授权请求" })
|
||||
async accessRequest(@Body(ALL) body: AccessRequestHandleReq) {
|
||||
let { projectId, userId } = await this.getProjectUserIdRead();
|
||||
@@ -64,12 +60,14 @@ export class HandleController extends BaseController {
|
||||
}
|
||||
}
|
||||
const getAccessById = this.accessService.getById.bind(this.accessService);
|
||||
const accessGetter = new AccessGetter(userId, projectId, getAccessById, this.runtimeDepsService);
|
||||
const accessGetter = new AccessGetter(userId, projectId, getAccessById);
|
||||
const serviceGetter = this.taskServiceBuilder.create({ userId, projectId });
|
||||
const accessContext = {
|
||||
http,
|
||||
logger,
|
||||
utils,
|
||||
accessService: accessGetter,
|
||||
serviceGetter,
|
||||
define: undefined,
|
||||
} as any;
|
||||
const access = await newAccess(body.typeName, inputAccess, accessGetter, accessContext);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { AutoCron } from "./auto-cron.js";
|
||||
import { AutoInitSite } from "./auto-init-site.js";
|
||||
import { AutoLoadPlugins } from "./auto-load-plugins.js";
|
||||
import { AutoCron } from "./auto-cron.js";
|
||||
import { AutoMitterRegister } from "./auto-mitter-register.js";
|
||||
import { AutoPipelineEmitterRegister } from "./auto-pipeline-emitter-register.js";
|
||||
import { AutoFix } from "./fix/auto-fix.js";
|
||||
import { AutoPrint } from "./auto-print.js";
|
||||
import { AutoFix } from "./fix/auto-fix.js";
|
||||
|
||||
@Autoload()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { logger } from "@certd/basic";
|
||||
import { PluginService } from "../plugin/service/plugin-service.js";
|
||||
import { registerPaymentProviders } from "../suite/payments/index.js";
|
||||
import { getRuntimeDepsService, initRuntimeDepsService } from "@certd/pipeline";
|
||||
import { pluginRegistry, accessRegistry, notificationRegistry } from "@certd/pipeline";
|
||||
import { dnsProviderRegistry } from "@certd/plugin-lib";
|
||||
import { addonRegistry } from "@certd/lib-server";
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
@@ -9,6 +13,9 @@ export class AutoLoadPlugins {
|
||||
@Inject()
|
||||
pluginService: PluginService;
|
||||
|
||||
@Config("runtimeDeps")
|
||||
runtimeDepsConfig: any;
|
||||
|
||||
async init() {
|
||||
logger.info(`加载插件开始,加载模式:${process.env.certd_plugin_loadmode}`);
|
||||
if (process.env.certd_plugin_loadmode === "metadata") {
|
||||
@@ -30,5 +37,17 @@ export class AutoLoadPlugins {
|
||||
|
||||
await registerPaymentProviders();
|
||||
logger.info(`加载插件完成,加载模式:${process.env.certd_plugin_loadmode}`);
|
||||
|
||||
//初始化第三方依赖服务
|
||||
initRuntimeDepsService(this.runtimeDepsConfig, {
|
||||
pluginRegistry,
|
||||
accessRegistry,
|
||||
notificationRegistry,
|
||||
dnsProviderRegistry,
|
||||
addonRegistry,
|
||||
});
|
||||
// 收集插件 dependPackages 并安装
|
||||
const service = getRuntimeDepsService();
|
||||
service.refreshPluginDeps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cache, isDev, randomNumber, simpleNanoId } from "@certd/basic";
|
||||
import { cache, isDev, randomNumber, simpleNanoId } from "@certd/basic";
|
||||
import { AccessService, AccessSysGetter, CodeErrorException, SysSettingsService } from "@certd/lib-server";
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { ISmsService } from "../sms/api.js";
|
||||
@@ -6,7 +6,6 @@ import { SmsServiceFactory } from "../sms/factory.js";
|
||||
import { CaptchaService } from "./captcha-service.js";
|
||||
import { EmailService } from "./email-service.js";
|
||||
import { CaptchaRequest } from "../../../plugins/plugin-captcha/api.js";
|
||||
import { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
|
||||
|
||||
// {data: '<svg.../svg>', text: 'abcd'}
|
||||
/**
|
||||
@@ -25,9 +24,6 @@ export class CodeService {
|
||||
@Inject()
|
||||
captchaService: CaptchaService;
|
||||
|
||||
@Inject()
|
||||
runtimeDepsService: RuntimeDepsService;
|
||||
|
||||
async checkCaptcha(body: any, req: CaptchaRequest) {
|
||||
return await this.captchaService.doValidate({ form: body, req });
|
||||
}
|
||||
@@ -60,7 +56,6 @@ export class CodeService {
|
||||
await sender.setCtx({
|
||||
accessService: accessGetter,
|
||||
config: smsConfig,
|
||||
runtimeDepsService: this.runtimeDepsService,
|
||||
});
|
||||
const smsCode = randomNumber(verificationCodeLength);
|
||||
await sender.sendSmsCode({
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { FormItemProps, IAccessService } from "@certd/pipeline";
|
||||
import type { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
|
||||
import { FormItemProps, IAccessService } from "@certd/pipeline";
|
||||
|
||||
export interface ISmsService {
|
||||
sendSmsCode(opts: { mobile: string; code: string; phoneCode: string }): Promise<void>;
|
||||
setCtx(ctx: { accessService: IAccessService; config: { [key: string]: any }; runtimeDepsService?: RuntimeDepsService }): Promise<void>;
|
||||
setCtx(ctx: { accessService: IAccessService; config: { [key: string]: any } }): Promise<void>;
|
||||
}
|
||||
|
||||
export type PluginInputs<T = any> = {
|
||||
@@ -13,5 +12,4 @@ export type PluginInputs<T = any> = {
|
||||
export type SmsPluginCtx<T = any> = {
|
||||
accessService: IAccessService;
|
||||
config: T;
|
||||
runtimeDepsService?: RuntimeDepsService;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TencentAccess } from "../../../plugins/plugin-lib/tencent/access.js";
|
||||
import { ISmsService, PluginInputs, SmsPluginCtx } from "./api.js";
|
||||
import { TencentAccess } from "../../../plugins/plugin-lib/tencent/access.js";
|
||||
import { importRuntime } from "@certd/pipeline";
|
||||
import { ISmsService, PluginInputs } from "./api.js";
|
||||
|
||||
export type TencentSmsConfig = {
|
||||
accessId: string;
|
||||
@@ -66,26 +67,16 @@ export class TencentSmsService implements ISmsService {
|
||||
};
|
||||
}
|
||||
|
||||
ctx: SmsPluginCtx<TencentSmsConfig>;
|
||||
ctx: { accessService: any; config: TencentSmsConfig };
|
||||
|
||||
async setCtx(ctx: any) {
|
||||
this.ctx = ctx;
|
||||
if (this.ctx.runtimeDepsService) {
|
||||
await this.ctx.runtimeDepsService.ensureDependencies({
|
||||
dependencies: {
|
||||
"tencentcloud-sdk-nodejs": "^4.1.112",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
if (!this.ctx.runtimeDepsService) {
|
||||
throw new Error("动态依赖服务未初始化,无法加载腾讯云短信SDK");
|
||||
}
|
||||
const sdk = await this.ctx.runtimeDepsService.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js");
|
||||
const sdk = await importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js");
|
||||
const client = sdk.v20210111.Client;
|
||||
const access = await this.ctx.accessService.getById<TencentAccess>(this.ctx.config.accessId);
|
||||
const access: TencentAccess = await this.ctx.accessService.getById(this.ctx.config.accessId);
|
||||
|
||||
// const region = this.region;
|
||||
const clientConfig = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { importRuntime } from "@certd/pipeline";
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { UserSettingsService } from "./user-settings-service.js";
|
||||
import { UserTwoFactorSetting } from "./models.js";
|
||||
@@ -13,13 +14,12 @@ export class TwoFactorService {
|
||||
userSettingsService: UserSettingsService;
|
||||
@Inject()
|
||||
userService: UserService;
|
||||
|
||||
async getAuthenticatorQrCode(userId: any) {
|
||||
const setting = await this.getSetting(userId);
|
||||
|
||||
const authenticatorSetting = setting.authenticator;
|
||||
if (!authenticatorSetting.secret) {
|
||||
const { authenticator } = await import("otplib");
|
||||
const { authenticator } = await importRuntime("otplib");
|
||||
|
||||
authenticatorSetting.secret = authenticator.generateSecret();
|
||||
await this.userSettingsService.saveSetting(userId, null, setting);
|
||||
@@ -38,7 +38,7 @@ export class TwoFactorService {
|
||||
|
||||
async saveAuthenticator(req: { userId: any; verifyCode: any }) {
|
||||
const userId = req.userId;
|
||||
const { authenticator } = await import("otplib");
|
||||
const { authenticator } = await importRuntime("otplib");
|
||||
const setting = await this.getSetting(userId);
|
||||
|
||||
const authenticatorSetting = setting.authenticator;
|
||||
@@ -77,7 +77,7 @@ export class TwoFactorService {
|
||||
}
|
||||
|
||||
async verifyAuthenticatorCode(userId: any, verifyCode: string) {
|
||||
const { authenticator } = await import("otplib");
|
||||
const { authenticator } = await importRuntime("otplib");
|
||||
const setting = await this.getSetting(userId);
|
||||
if (!setting.authenticator.enabled) {
|
||||
throw new Error("authenticator 未开启");
|
||||
|
||||
@@ -805,11 +805,12 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
});
|
||||
}
|
||||
|
||||
async batchDelete(ids: number[], userId: number, projectId?: number): Promise<void> {
|
||||
async batchDelete(ids: number[], userId: number, projectId?: number): Promise<number> {
|
||||
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
|
||||
await this.repository.delete({
|
||||
id: In(ids),
|
||||
...userProjectQuery,
|
||||
});
|
||||
return ids.length;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-10
@@ -1,4 +1,4 @@
|
||||
import { IServiceGetter } from "@certd/pipeline";
|
||||
import { IServiceGetter } from "@certd/pipeline";
|
||||
import { ApplicationContext, IMidwayContainer, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { AccessGetter, AccessService } from "@certd/lib-server";
|
||||
import { CnameProxyService } from "./cname-proxy-service.js";
|
||||
@@ -13,7 +13,6 @@ import { CertInfoGetter } from "./cert-info-getter.js";
|
||||
import { CertInfoService } from "../../../monitor/index.js";
|
||||
import { ICertInfoGetter } from "@certd/plugin-lib";
|
||||
import { CnameProviderService } from "../../../cname/service/cname-provider-service.js";
|
||||
import { RuntimeDepsService } from "../../../runtime-deps/runtime-deps-service.js";
|
||||
|
||||
const serviceNames = ["ocrService"];
|
||||
export class TaskServiceGetter implements IServiceGetter {
|
||||
@@ -39,8 +38,6 @@ export class TaskServiceGetter implements IServiceGetter {
|
||||
return (await this.getDomainVerifierGetter()) as T;
|
||||
} else if (serviceName === "certInfoGetter") {
|
||||
return (await this.getCertInfoGetter()) as T;
|
||||
} else if (serviceName === "runtimeDepsService") {
|
||||
return (await this.getRuntimeDepsService()) as T;
|
||||
} else {
|
||||
if (!serviceNames.includes(serviceName)) {
|
||||
throw new Error(`${serviceName} not in whitelist`);
|
||||
@@ -66,9 +63,8 @@ export class TaskServiceGetter implements IServiceGetter {
|
||||
|
||||
async getAccessService(): Promise<AccessGetter> {
|
||||
const accessService: AccessService = await this.appCtx.getAsync("accessService");
|
||||
const runtimeDepsService = await this.getRuntimeDepsService();
|
||||
const getAccessById = accessService.getById.bind(accessService);
|
||||
return new AccessGetter(this.userId, this.projectId, getAccessById, runtimeDepsService);
|
||||
return new AccessGetter(this.userId, this.projectId, getAccessById);
|
||||
}
|
||||
|
||||
async getCnameProxyService(): Promise<CnameProxyService> {
|
||||
@@ -85,10 +81,6 @@ export class TaskServiceGetter implements IServiceGetter {
|
||||
const domainService: DomainService = await this.appCtx.getAsync("domainService");
|
||||
return new DomainVerifierGetter(this.userId, this.projectId, domainService);
|
||||
}
|
||||
|
||||
async getRuntimeDepsService(): Promise<RuntimeDepsService> {
|
||||
return await this.appCtx.getAsync("runtimeDepsService");
|
||||
}
|
||||
}
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
|
||||
@@ -962,7 +962,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
return result;
|
||||
}
|
||||
|
||||
async batchDelete(ids: number[], userId?: number, projectId?: number) {
|
||||
async batchDelete(ids: number[], userId?: number, projectId?: number): Promise<number> {
|
||||
if (!isPlus()) {
|
||||
throw new NeedVIPException("此功能需要升级Certd专业版");
|
||||
}
|
||||
@@ -974,7 +974,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
await this.checkUserId(id, projectId, "projectId");
|
||||
}
|
||||
await this.delete(id);
|
||||
ids.push(id);
|
||||
}
|
||||
return ids.length;
|
||||
}
|
||||
|
||||
async batchUpdateGroup(ids: number[], groupId: number, userId: any, projectId?: number) {
|
||||
|
||||
@@ -87,7 +87,7 @@ export class TemplateService extends BaseService<TemplateEntity> {
|
||||
};
|
||||
}
|
||||
|
||||
async batchDelete(ids: number[], userId: number, projectId?: number) {
|
||||
async batchDelete(ids: number[], userId: number, projectId?: number): Promise<number> {
|
||||
const where: any = {
|
||||
id: In(ids),
|
||||
};
|
||||
@@ -102,6 +102,7 @@ export class TemplateService extends BaseService<TemplateEntity> {
|
||||
const pipelineIds = list.map(item => item.pipelineId);
|
||||
await this.delete(ids);
|
||||
await this.pipelineService.batchDelete(pipelineIds, userId, projectId);
|
||||
return ids.length;
|
||||
}
|
||||
|
||||
async createPipelineByTemplate(body: PipelineEntity) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { addonRegistry, BaseService, PageReq } from "@certd/lib-server";
|
||||
import { PluginEntity } from "../entity/plugin.js";
|
||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||
@@ -6,14 +6,13 @@ import { IsNull, Not, Repository } from "typeorm";
|
||||
import { isComm } from "@certd/plus-core";
|
||||
import { BuiltInPluginService } from "../../pipeline/service/builtin-plugin-service.js";
|
||||
import { merge } from "lodash-es";
|
||||
import { accessRegistry, notificationRegistry, pluginRegistry } from "@certd/pipeline";
|
||||
import { dnsProviderRegistry } from "@certd/plugin-cert";
|
||||
import { logger } from "@certd/basic";
|
||||
import yaml from "js-yaml";
|
||||
import { getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin } from "./default-plugin.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
|
||||
import { importRuntime as importRuntimeDirect, getRuntimeDepsService, pluginRegistry, accessRegistry, notificationRegistry } from "@certd/pipeline";
|
||||
|
||||
export type PluginImportReq = {
|
||||
content: string;
|
||||
@@ -49,9 +48,6 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
@Inject()
|
||||
builtInPluginService: BuiltInPluginService;
|
||||
|
||||
@Inject()
|
||||
runtimeDepsService: RuntimeDepsService;
|
||||
|
||||
//@ts-ignore
|
||||
getRepository() {
|
||||
return this.repository;
|
||||
@@ -270,6 +266,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
return;
|
||||
}
|
||||
await this.registerPlugin(item);
|
||||
await this.refreshPluginDeps();
|
||||
}
|
||||
|
||||
async unRegisterById(id: any) {
|
||||
@@ -297,6 +294,12 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
} else {
|
||||
logger.warn(`不支持的插件类型:${item.pluginType}`);
|
||||
}
|
||||
await this.refreshPluginDeps();
|
||||
}
|
||||
|
||||
async refreshPluginDeps() {
|
||||
const service = getRuntimeDepsService();
|
||||
service.refreshPluginDeps();
|
||||
}
|
||||
|
||||
async update(param: any) {
|
||||
@@ -332,7 +335,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
if (!isBareModuleSpecifier(modulePath)) {
|
||||
return await importLocalModule(modulePath);
|
||||
}
|
||||
return await this.runtimeDepsService.importRuntime(modulePath, logger);
|
||||
return await importRuntimeDirect(modulePath, logger);
|
||||
}
|
||||
|
||||
private async getPluginClassFromFile(item: any) {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import assert from "assert";
|
||||
import { NpmRegistryResolver } from "./npm-registry-resolver.js";
|
||||
|
||||
describe("NpmRegistryResolver", () => {
|
||||
it("chooses the fastest successful registry in auto mode", async () => {
|
||||
const resolver = new NpmRegistryResolver();
|
||||
resolver.config = {
|
||||
mode: "auto",
|
||||
fixedUrl: "",
|
||||
candidates: ["https://slow.example.com", "https://fast.example.com"],
|
||||
probeTimeoutMs: 100,
|
||||
cacheTtlMs: 1000,
|
||||
};
|
||||
resolver.probe = async registryUrl => {
|
||||
return {
|
||||
registryUrl,
|
||||
ok: true,
|
||||
elapsedMs: registryUrl.includes("fast") ? 10 : 50,
|
||||
};
|
||||
};
|
||||
|
||||
const result = await resolver.resolve();
|
||||
|
||||
assert.equal(result, "https://fast.example.com");
|
||||
});
|
||||
|
||||
it("uses fixed registry without probing", async () => {
|
||||
const resolver = new NpmRegistryResolver();
|
||||
resolver.config = {
|
||||
mode: "fixed",
|
||||
fixedUrl: "https://registry.example.com",
|
||||
candidates: [],
|
||||
probeTimeoutMs: 100,
|
||||
cacheTtlMs: 1000,
|
||||
};
|
||||
|
||||
const result = await resolver.resolve();
|
||||
|
||||
assert.equal(result, "https://registry.example.com");
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import { Config, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
|
||||
export type NpmRegistryResolverConfig = {
|
||||
mode: "auto" | "fixed" | "system";
|
||||
fixedUrl: string;
|
||||
candidates: string[];
|
||||
probeTimeoutMs: number;
|
||||
cacheTtlMs: number;
|
||||
};
|
||||
|
||||
export type RegistryProbeResult = {
|
||||
registryUrl: string;
|
||||
ok: boolean;
|
||||
elapsedMs: number;
|
||||
};
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class NpmRegistryResolver {
|
||||
@Config("runtimeDeps.registry")
|
||||
config!: NpmRegistryResolverConfig;
|
||||
|
||||
private cache?: { registryUrl: string; expiresAt: number };
|
||||
|
||||
async resolve(): Promise<string> {
|
||||
const config = this.config;
|
||||
if (config?.mode === "fixed" && config.fixedUrl) {
|
||||
return config.fixedUrl;
|
||||
}
|
||||
if (config?.mode === "system") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const cached = this.cache;
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.registryUrl;
|
||||
}
|
||||
|
||||
const candidates = (config?.candidates || []).filter(Boolean);
|
||||
const probes = await Promise.allSettled(candidates.map(registryUrl => this.probe(registryUrl)));
|
||||
const okList = probes.map(item => (item.status === "fulfilled" ? item.value : null)).filter((item): item is RegistryProbeResult => !!item && item.ok);
|
||||
|
||||
if (okList.length > 0) {
|
||||
okList.sort((a, b) => a.elapsedMs - b.elapsedMs);
|
||||
const best = okList[0].registryUrl;
|
||||
this.cache = {
|
||||
registryUrl: best,
|
||||
expiresAt: Date.now() + (config?.cacheTtlMs || 0),
|
||||
};
|
||||
return best;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async probe(registryUrl: string): Promise<RegistryProbeResult> {
|
||||
const timeoutMs = this.config?.probeTimeoutMs || 3000;
|
||||
const started = Date.now();
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const res = await fetch(`${registryUrl.replace(/\/$/, "")}/-/ping`, { signal: controller.signal });
|
||||
return {
|
||||
registryUrl,
|
||||
ok: res.ok,
|
||||
elapsedMs: Date.now() - started,
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
registryUrl,
|
||||
ok: false,
|
||||
elapsedMs: Date.now() - started,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,677 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { spawn } from "child_process";
|
||||
import crypto from "crypto";
|
||||
import { Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { createRequire } from "module";
|
||||
import { pathToFileURL } from "url";
|
||||
import { NpmRegistryResolver } from "./npm-registry-resolver.js";
|
||||
import { Registry, accessRegistry, notificationRegistry, pluginRegistry } from "@certd/pipeline";
|
||||
import { dnsProviderRegistry } from "@certd/plugin-lib";
|
||||
import { addonRegistry } from "@certd/lib-server";
|
||||
import { logger, ILogger } from "@certd/basic";
|
||||
|
||||
export type RuntimeDependencyPluginDefine = {
|
||||
name: string;
|
||||
key?: string;
|
||||
title?: string;
|
||||
version?: string;
|
||||
pluginType?: string;
|
||||
addonType?: string;
|
||||
dependPlugins?: Record<string, string>;
|
||||
dependPackages?: Record<string, string>;
|
||||
};
|
||||
|
||||
type RegisteredDefineLike = RuntimeDependencyPluginDefine & {
|
||||
key?: string;
|
||||
pluginType?: string;
|
||||
addonType?: string;
|
||||
dependPlugins?: Record<string, string>;
|
||||
dependPackages?: Record<string, string>;
|
||||
};
|
||||
|
||||
function normalizeRange(range: string) {
|
||||
return range.trim().replace(/^\^/, "").replace(/^~?/, "");
|
||||
}
|
||||
|
||||
function areRangesCompatible(a: string, b: string) {
|
||||
if (!a || !b) {
|
||||
return true;
|
||||
}
|
||||
if (a === "*" || b === "*") {
|
||||
return true;
|
||||
}
|
||||
const left = normalizeRange(a).split(".");
|
||||
const right = normalizeRange(b).split(".");
|
||||
return left[0] === right[0];
|
||||
}
|
||||
|
||||
type DependencyConflict = {
|
||||
packageName: string;
|
||||
ranges: Array<{ pluginName: string; range: string }>;
|
||||
};
|
||||
|
||||
type CollectDependenciesResult = {
|
||||
dependencies: Record<string, string>;
|
||||
conflicts: DependencyConflict[];
|
||||
};
|
||||
|
||||
type InstallResult = {
|
||||
registryUrl: string;
|
||||
packageJsonPath: string;
|
||||
};
|
||||
|
||||
type RuntimeImportResolveResult = {
|
||||
resolved: string;
|
||||
packageName: string;
|
||||
};
|
||||
|
||||
type CommandRunnerResult = {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number;
|
||||
};
|
||||
|
||||
type CommandRunner = {
|
||||
// @ts-ignore
|
||||
run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult>;
|
||||
};
|
||||
|
||||
const PROCESS_LOCKS = new Map<string, Promise<unknown>>();
|
||||
|
||||
class DefaultCommandRunner implements CommandRunner {
|
||||
// @ts-ignore
|
||||
async run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult> {
|
||||
return await new Promise<CommandRunnerResult>(resolve => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let settled = false;
|
||||
const child = spawn(command, args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
windowsHide: true,
|
||||
// @ts-ignore
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
child.kill("SIGTERM");
|
||||
resolve({ stdout, stderr: stderr || `command timeout after ${options.timeoutMs}ms`, code: 1 });
|
||||
}, options.timeoutMs);
|
||||
|
||||
child.stdout?.on("data", chunk => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr?.on("data", chunk => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("error", error => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
resolve({ stdout, stderr: error.message, code: 1 });
|
||||
});
|
||||
child.on("close", code => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
resolve({ stdout, stderr, code: code || 0 });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class RuntimeDepsService {
|
||||
@Config("runtimeDeps.rootDir")
|
||||
runtimeDepsRootDir = "./data/.runtime-deps";
|
||||
|
||||
@Config("runtimeDeps.autoInstall")
|
||||
autoInstall = true;
|
||||
|
||||
@Config("runtimeDeps.enabled")
|
||||
enabled = true;
|
||||
|
||||
@Config("runtimeDeps.installTimeoutMs")
|
||||
installTimeoutMs = 120000;
|
||||
|
||||
@Config("runtimeDeps.pnpmCommand")
|
||||
pnpmCommand = "";
|
||||
|
||||
@Config("runtimeDeps.lazyDependencies")
|
||||
lazyDependencies: Record<string, string> = {};
|
||||
|
||||
@Inject()
|
||||
registryResolver!: NpmRegistryResolver;
|
||||
|
||||
commandRunner: CommandRunner = new DefaultCommandRunner();
|
||||
|
||||
private installPromises = new Map<string, Promise<InstallResult>>();
|
||||
|
||||
collectDependencies(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
|
||||
const merged: Record<string, string> = {};
|
||||
const seen: Record<string, Array<{ pluginName: string; range: string }>> = {};
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const deps = plugin.dependPackages || {};
|
||||
for (const [packageName, range] of Object.entries(deps)) {
|
||||
seen[packageName] ||= [];
|
||||
seen[packageName].push({ pluginName: plugin.name, range });
|
||||
}
|
||||
}
|
||||
|
||||
const conflicts: DependencyConflict[] = [];
|
||||
for (const [packageName, ranges] of Object.entries(seen)) {
|
||||
const first = ranges[0]?.range;
|
||||
if (!first) {
|
||||
continue;
|
||||
}
|
||||
const conflict = ranges.some(item => !areRangesCompatible(first, item.range));
|
||||
if (conflict) {
|
||||
conflicts.push({ packageName, ranges });
|
||||
continue;
|
||||
}
|
||||
merged[packageName] = first;
|
||||
}
|
||||
|
||||
return { dependencies: merged, conflicts };
|
||||
}
|
||||
|
||||
async ensureInstalled(options: { plugins: RuntimeDependencyPluginDefine[]; logger?: ILogger }): Promise<InstallResult> {
|
||||
const { plugins, logger: log } = options;
|
||||
const { dependencies, conflicts } = this.resolveDependenciesFromPlugins(plugins);
|
||||
if (conflicts.length > 0) {
|
||||
const conflict = conflicts[0];
|
||||
throw new Error(`动态依赖版本冲突: ${conflict.packageName} => ${conflict.ranges.map(item => `${item.pluginName}:${item.range}`).join(", ")}`);
|
||||
}
|
||||
return await this.ensureDependencies({ dependencies, logger: log });
|
||||
}
|
||||
|
||||
async ensureDependencies(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
|
||||
const { dependencies, logger: log } = options;
|
||||
if (!this.enabled) {
|
||||
return {
|
||||
registryUrl: "",
|
||||
packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"),
|
||||
};
|
||||
}
|
||||
if (!this.autoInstall) {
|
||||
return {
|
||||
registryUrl: "",
|
||||
packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"),
|
||||
};
|
||||
}
|
||||
const dependenciesHash = this.createDependenciesHash(dependencies);
|
||||
let installPromise = this.installPromises.get(dependenciesHash);
|
||||
if (installPromise) {
|
||||
const nodeModulesPath = path.join(this.getRuntimeDepsRootDir(), "node_modules");
|
||||
if (!fs.existsSync(nodeModulesPath)) {
|
||||
this.installPromises.delete(dependenciesHash);
|
||||
installPromise = undefined;
|
||||
}
|
||||
}
|
||||
if (!installPromise) {
|
||||
installPromise = this.doEnsureInstalled({ dependencies, logger: log }).catch(error => {
|
||||
this.installPromises.delete(dependenciesHash);
|
||||
throw error;
|
||||
});
|
||||
this.installPromises.set(dependenciesHash, installPromise);
|
||||
}
|
||||
return await installPromise;
|
||||
}
|
||||
|
||||
resolveDependenciesFromPlugins(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
|
||||
const expandedPlugins = plugins.flatMap(plugin => this.resolvePluginDependencies(plugin));
|
||||
return this.collectDependencies(expandedPlugins);
|
||||
}
|
||||
|
||||
async ensureRuntimeDependencies(options: { pluginKeys: string | string[]; logger?: ILogger }): Promise<InstallResult> {
|
||||
const { pluginKeys, logger: log } = options;
|
||||
const keys = Array.isArray(pluginKeys) ? pluginKeys : [pluginKeys];
|
||||
const pluginDefines = keys.map(pluginKey => this.getDefineByPluginKey(pluginKey));
|
||||
if (pluginDefines.every(pluginDefine => !pluginDefine.dependPackages && !pluginDefine.dependPlugins)) {
|
||||
return {
|
||||
registryUrl: "",
|
||||
packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"),
|
||||
};
|
||||
}
|
||||
const expandedPluginDefines = pluginDefines.flatMap(pluginDefine => this.resolvePluginDependencies(pluginDefine));
|
||||
return await this.ensureInstalled({ plugins: expandedPluginDefines, logger: log });
|
||||
}
|
||||
|
||||
private async doEnsureInstalled(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
|
||||
let { dependencies } = options;
|
||||
const log = options.logger || logger;
|
||||
return await this.withInstallLock(async () => {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
const packageJsonPath = path.join(rootDir, "package.json");
|
||||
const lockPath = path.join(rootDir, "pnpm-lock.yaml");
|
||||
log.info(`第三方依赖安装: ${JSON.stringify(dependencies)}`);
|
||||
dependencies = this.mergeInstalledDependencies(this.readManifestDependencies(packageJsonPath), dependencies);
|
||||
const dependenciesHash = this.createDependenciesHash(dependencies);
|
||||
const statePath = path.join(rootDir, "install-state.json");
|
||||
const currentState = this.readInstallState(statePath);
|
||||
if (currentState?.dependenciesHash === dependenciesHash && fs.existsSync(path.join(rootDir, "node_modules"))) {
|
||||
log.info("第三方依赖已安装");
|
||||
return { registryUrl: currentState.registryUrl || "", packageJsonPath };
|
||||
}
|
||||
const manifest = {
|
||||
name: "certd-runtime-deps",
|
||||
private: true,
|
||||
type: "module",
|
||||
dependencies,
|
||||
};
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(manifest, null, 2), "utf8");
|
||||
|
||||
const registryUrl = await this.registryResolver.resolve();
|
||||
const env = this.buildChildEnv(registryUrl);
|
||||
const command = this.getPnpmCommand();
|
||||
const pnpmVersion = await this.getPnpmVersion(command, env);
|
||||
const args = ["install", "--prod", "--ignore-scripts", "--ignore-workspace", "--no-frozen-lockfile", "--reporter=append-only"];
|
||||
if (registryUrl) {
|
||||
args.push(`--registry=${registryUrl}`);
|
||||
}
|
||||
|
||||
log.info(`开始安装第三方依赖: ${Object.keys(dependencies).join(", ")}`);
|
||||
const result = await this.commandRunner.run(command, args, {
|
||||
cwd: rootDir,
|
||||
timeoutMs: this.installTimeoutMs,
|
||||
env,
|
||||
});
|
||||
if (result.code !== 0) {
|
||||
const message = result.stderr || result.stdout || "unknown error";
|
||||
this.writeInstallState(statePath, {
|
||||
...currentState,
|
||||
installedAt: currentState?.installedAt,
|
||||
failedAt: new Date().toISOString(),
|
||||
registryUrl,
|
||||
dependenciesHash,
|
||||
// @ts-ignore
|
||||
nodeVersion: process.version,
|
||||
pnpmVersion,
|
||||
lockFileExists: fs.existsSync(lockPath),
|
||||
lastError: message,
|
||||
});
|
||||
throw new Error(`动态依赖安装失败: ${message}`);
|
||||
}
|
||||
this.writeInstallState(statePath, {
|
||||
installedAt: new Date().toISOString(),
|
||||
registryUrl,
|
||||
dependenciesHash,
|
||||
// @ts-ignore
|
||||
nodeVersion: process.version,
|
||||
pnpmVersion,
|
||||
lockFileExists: fs.existsSync(lockPath),
|
||||
});
|
||||
log.info("第三方依赖安装完成");
|
||||
return { registryUrl, packageJsonPath };
|
||||
});
|
||||
}
|
||||
|
||||
async importRuntime(specifier: string, logger?: ILogger) {
|
||||
if (this.isNativeImportSpecifier(specifier)) {
|
||||
return await import(specifier);
|
||||
}
|
||||
|
||||
const resolved = await this.resolveImportSpecifier(specifier, logger);
|
||||
return await import(pathToFileURL(resolved).href);
|
||||
}
|
||||
|
||||
private async resolveImportSpecifier(specifier: string, logger?: ILogger) {
|
||||
try {
|
||||
return this.resolveRuntimeSpecifier(specifier).resolved;
|
||||
} catch (runtimeError: any) {
|
||||
if (!this.isModuleNotFoundError(runtimeError)) {
|
||||
throw runtimeError;
|
||||
}
|
||||
return await this.resolveMissingRuntimeSpecifier(specifier, runtimeError, logger);
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveMissingRuntimeSpecifier(specifier: string, runtimeError: any, logger?: ILogger) {
|
||||
const packageName = this.parsePackageName(specifier);
|
||||
const lazyRange = this.lazyDependencies?.[packageName];
|
||||
if (!lazyRange) {
|
||||
try {
|
||||
return this.resolveProjectSpecifier(specifier, runtimeError).resolved;
|
||||
} catch {
|
||||
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.ensureLazyDependency(packageName, logger);
|
||||
return this.resolveRuntimeSpecifier(specifier).resolved;
|
||||
} catch (lazyError: any) {
|
||||
return this.resolveProjectSpecifier(specifier, lazyError).resolved;
|
||||
}
|
||||
}
|
||||
|
||||
private isNativeImportSpecifier(specifier: string) {
|
||||
return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:");
|
||||
}
|
||||
|
||||
private resolveRuntimeSpecifier(specifier: string): RuntimeImportResolveResult {
|
||||
const packageName = this.parsePackageName(specifier);
|
||||
const packageJsonPath = path.join(this.getRuntimeDepsRootDir(), "package.json");
|
||||
const require = createRequire(packageJsonPath);
|
||||
const resolved = require.resolve(specifier);
|
||||
return { packageName, resolved };
|
||||
}
|
||||
|
||||
private resolveProjectSpecifier(specifier: string, cause?: any): RuntimeImportResolveResult {
|
||||
try {
|
||||
const packageName = this.parsePackageName(specifier);
|
||||
const packageJsonPath = path.resolve("package.json");
|
||||
const require = createRequire(packageJsonPath);
|
||||
const resolved = require.resolve(specifier);
|
||||
return { packageName, resolved };
|
||||
} catch (projectError: any) {
|
||||
if (cause) {
|
||||
projectError.cause = cause;
|
||||
}
|
||||
throw projectError;
|
||||
}
|
||||
}
|
||||
|
||||
private parsePackageName(specifier: string) {
|
||||
if (!specifier || specifier.trim() !== specifier) {
|
||||
throw new Error(`动态依赖导入路径无效: ${specifier}`);
|
||||
}
|
||||
const parts = specifier.split("/");
|
||||
if (specifier.startsWith("@")) {
|
||||
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
||||
throw new Error(`动态依赖导入路径无效: ${specifier}`);
|
||||
}
|
||||
return `${parts[0]}/${parts[1]}`;
|
||||
}
|
||||
if (!parts[0]) {
|
||||
throw new Error(`动态依赖导入路径无效: ${specifier}`);
|
||||
}
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
private async ensureLazyDependency(packageName: string, logger?: ILogger) {
|
||||
const range = this.lazyDependencies?.[packageName];
|
||||
if (!range) {
|
||||
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
|
||||
}
|
||||
const dependencies = {
|
||||
[packageName]: range,
|
||||
};
|
||||
await this.ensureDependencies({ dependencies, logger });
|
||||
}
|
||||
|
||||
private isModuleNotFoundError(error: any) {
|
||||
return error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND";
|
||||
}
|
||||
|
||||
resolvePluginDependencies(current: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine[] {
|
||||
const resolved: RuntimeDependencyPluginDefine[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
const visit = (item: RuntimeDependencyPluginDefine) => {
|
||||
const key = this.buildPluginDependencyKey(item);
|
||||
if (visited.has(key)) {
|
||||
return;
|
||||
}
|
||||
visited.add(key);
|
||||
resolved.push(item);
|
||||
for (const [dependencyName, expectedRange] of Object.entries(item.dependPlugins || {})) {
|
||||
const dependency = this.getDefineByPluginKey(dependencyName, item);
|
||||
if (!isPluginVersionCompatible(dependency, expectedRange)) {
|
||||
throw new Error(`插件依赖版本冲突: ${item.name} 依赖 ${dependencyName}@${expectedRange},当前版本为 ${dependency.version || "未声明"}`);
|
||||
}
|
||||
visit(dependency);
|
||||
}
|
||||
};
|
||||
|
||||
visit(current);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private buildPluginDependencyKey(plugin: RuntimeDependencyPluginDefine) {
|
||||
if (plugin.pluginType === "addon" && plugin.addonType) {
|
||||
return `addon:${plugin.addonType}:${plugin.name}`;
|
||||
}
|
||||
const pluginType = plugin.pluginType === "deploy" ? "plugin" : plugin.pluginType || "unknown";
|
||||
return `${pluginType}:${plugin.name}`;
|
||||
}
|
||||
|
||||
private getDefineByPluginKey(pluginKey: string, owner?: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine {
|
||||
const parts = pluginKey.split(":");
|
||||
const [pluginType, subtype, rawName] = parts;
|
||||
let name = rawName;
|
||||
if (parts.length === 2) {
|
||||
name = subtype;
|
||||
} else if (parts.length === 3) {
|
||||
//无修改
|
||||
} else {
|
||||
const ownerName = owner?.name || pluginKey;
|
||||
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},请使用 plugin:name、access:name、notification:name、dnsProvider:name 或 addon:subtype:name 格式`);
|
||||
}
|
||||
const registryMap: Record<string, { registry: Registry<any>; key: string; pluginType: string; addonType?: string }> = {
|
||||
plugin: { registry: pluginRegistry, key: name, pluginType: "plugin" },
|
||||
access: { registry: accessRegistry, key: name, pluginType: "access" },
|
||||
notification: { registry: notificationRegistry, key: name, pluginType: "notification" },
|
||||
dnsProvider: { registry: dnsProviderRegistry, key: name, pluginType: "dnsProvider" },
|
||||
addon: { registry: addonRegistry, key: `${subtype}:${name}`, pluginType: "addon", addonType: subtype },
|
||||
};
|
||||
const target = registryMap[pluginType];
|
||||
if (!target) {
|
||||
const ownerName = owner?.name || pluginKey;
|
||||
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},未知插件类型 ${pluginType}`);
|
||||
}
|
||||
const define = target.registry.getDefine(target.key) as RegisteredDefineLike;
|
||||
if (!define) {
|
||||
throw new Error(`插件依赖缺失: ${owner?.name || pluginKey} 依赖 ${pluginKey},但该插件未注册或已禁用`);
|
||||
}
|
||||
return { ...define, key: pluginKey, pluginType: target.pluginType, addonType: target.addonType };
|
||||
}
|
||||
|
||||
private async withInstallLock<T>(run: () => Promise<T>): Promise<T> {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
fs.mkdirSync(rootDir, { recursive: true });
|
||||
const lockFile = path.join(rootDir, ".install.lock");
|
||||
const previous = PROCESS_LOCKS.get(lockFile);
|
||||
if (previous) {
|
||||
await previous.catch(() => undefined);
|
||||
}
|
||||
let releaseProcessLock!: () => void;
|
||||
const current = new Promise<void>(resolve => {
|
||||
releaseProcessLock = resolve;
|
||||
});
|
||||
PROCESS_LOCKS.set(lockFile, current);
|
||||
let fd: number | undefined;
|
||||
try {
|
||||
fd = await this.acquireFileLock(lockFile);
|
||||
return await run();
|
||||
} finally {
|
||||
if (fd != null) {
|
||||
fs.closeSync(fd);
|
||||
try {
|
||||
fs.rmSync(lockFile, { force: true });
|
||||
} catch {
|
||||
try {
|
||||
fs.rmSync(lockFile, { force: true });
|
||||
} catch {
|
||||
// Windows 下 closeSync 后文件句柄可能未立即释放,忽略清理失败
|
||||
}
|
||||
}
|
||||
}
|
||||
releaseProcessLock();
|
||||
if (PROCESS_LOCKS.get(lockFile) === current) {
|
||||
PROCESS_LOCKS.delete(lockFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async acquireFileLock(lockFile: string) {
|
||||
const deadline = Date.now() + this.installTimeoutMs;
|
||||
while (true) {
|
||||
try {
|
||||
const fd = fs.openSync(lockFile, "wx");
|
||||
// @ts-ignore
|
||||
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), "utf8");
|
||||
return fd;
|
||||
} catch (error: any) {
|
||||
if (error?.code !== "EEXIST") {
|
||||
throw error;
|
||||
}
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(`动态依赖安装锁等待超时: ${lockFile}`);
|
||||
}
|
||||
await this.waitForExternalLock(lockFile, deadline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForExternalLock(lockFile: string, deadline: number) {
|
||||
while (fs.existsSync(lockFile)) {
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(`动态依赖安装锁等待超时: ${lockFile}`);
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
}
|
||||
}
|
||||
|
||||
async clearRuntimeDeps() {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
const normalizedRootDir = path.normalize(rootDir);
|
||||
if (!normalizedRootDir.endsWith(path.normalize(".runtime-deps"))) {
|
||||
throw new Error(`动态依赖目录配置异常,拒绝清理: ${rootDir}`);
|
||||
}
|
||||
await this.withInstallLock(async () => {
|
||||
if (fs.existsSync(rootDir)) {
|
||||
const entries = fs.readdirSync(rootDir);
|
||||
for (const entry of entries) {
|
||||
if (entry === ".install.lock") {
|
||||
continue;
|
||||
}
|
||||
const entryPath = path.join(rootDir, entry);
|
||||
fs.rmSync(entryPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
this.installPromises.clear();
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private readInstallState(statePath: string): any {
|
||||
if (!fs.existsSync(statePath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(statePath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private writeInstallState(statePath: string, state: any) {
|
||||
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8");
|
||||
}
|
||||
|
||||
private readManifestDependencies(packageJsonPath: string): Record<string, string> {
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
return manifest.dependencies || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
private mergeInstalledDependencies(installed: Record<string, string>, requested: Record<string, string>) {
|
||||
const dependencies = { ...installed };
|
||||
for (const [packageName, range] of Object.entries(requested)) {
|
||||
const installedRange = dependencies[packageName];
|
||||
if (installedRange && !areRangesCompatible(installedRange, range)) {
|
||||
throw new Error(`动态依赖版本冲突: ${packageName} => installed:${installedRange}, requested:${range}`);
|
||||
}
|
||||
dependencies[packageName] = installedRange || range;
|
||||
}
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
private async getPnpmVersion(command: string, env: NodeJS.ProcessEnv) {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
const result = await this.commandRunner.run(command, ["--version"], {
|
||||
cwd: rootDir,
|
||||
timeoutMs: Math.min(this.installTimeoutMs, 10000),
|
||||
env,
|
||||
});
|
||||
if (result.code !== 0) {
|
||||
return "";
|
||||
}
|
||||
return (result.stdout || result.stderr || "").trim();
|
||||
}
|
||||
|
||||
private getPnpmCommand() {
|
||||
if (this.pnpmCommand) {
|
||||
return this.pnpmCommand;
|
||||
}
|
||||
return "pnpm";
|
||||
}
|
||||
|
||||
private buildChildEnv(registryUrl: string) {
|
||||
// @ts-ignore
|
||||
const env = { ...process.env };
|
||||
for (const key of ["NODE_OPTIONS", "VSCODE_INSPECTOR_OPTIONS", "NODE_INSPECTOR_PORT", "NODE_DEBUG"]) {
|
||||
if (!env[key]) {
|
||||
continue;
|
||||
}
|
||||
if (key === "NODE_OPTIONS") {
|
||||
env[key] = this.stripDebugNodeOptions(env[key] as string);
|
||||
} else {
|
||||
delete env[key];
|
||||
}
|
||||
}
|
||||
if (registryUrl) {
|
||||
env.npm_config_registry = registryUrl;
|
||||
env.pnpm_config_registry = registryUrl;
|
||||
}
|
||||
env.CI = env.CI || "true";
|
||||
env.npm_config_confirm_modules_purge = "false";
|
||||
env.pnpm_config_confirm_modules_purge = "false";
|
||||
return env;
|
||||
}
|
||||
|
||||
private stripDebugNodeOptions(value: string) {
|
||||
return value
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.filter(item => !/^--inspect(-brk|-port)?(=|$)/.test(item))
|
||||
.filter(item => !/^--debug(=|$)/.test(item))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
private getRuntimeDepsRootDir() {
|
||||
return path.resolve(this.runtimeDepsRootDir);
|
||||
}
|
||||
|
||||
private createDependenciesHash(dependencies: Record<string, string>) {
|
||||
return crypto.createHash("sha256").update(JSON.stringify(dependencies)).digest("hex");
|
||||
}
|
||||
}
|
||||
|
||||
function isPluginVersionCompatible(plugin: RuntimeDependencyPluginDefine, expectedRange: string) {
|
||||
if (!expectedRange || expectedRange === "*") {
|
||||
return true;
|
||||
}
|
||||
if (!plugin.version) {
|
||||
return false;
|
||||
}
|
||||
return areRangesCompatible(expectedRange, plugin.version);
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export class PaymentAlipay implements IPaymentProvider {
|
||||
}
|
||||
|
||||
private async createAlipaySdk() {
|
||||
const AlipaySdk = await import("alipay-sdk");
|
||||
const AlipaySdk = await this.access.importRuntime("alipay-sdk");
|
||||
|
||||
const alipaySdk = new AlipaySdk.AlipaySdk({
|
||||
appId: this.access.appId,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IPaymentProvider, TradeEntity, UpdateTrade, UpdateTradeInfo } from "@certd/commercial-core";
|
||||
import WxPay from "wechatpay-node-v3";
|
||||
import dayjs from "dayjs";
|
||||
import { logger } from "@certd/basic"; // 支持使用require
|
||||
import { logger } from "@certd/basic";
|
||||
import { WxpayAccess } from "../../../plugins/plugin-plus/wxpay/access.js";
|
||||
export class PaymentWxpay implements IPaymentProvider {
|
||||
access: WxpayAccess;
|
||||
@@ -26,7 +25,7 @@ export class PaymentWxpay implements IPaymentProvider {
|
||||
* }
|
||||
*/
|
||||
|
||||
const pay = this.createSdk();
|
||||
const pay = await this.createSdk();
|
||||
|
||||
const result: any = await pay.query({ out_trade_no: tradeNo });
|
||||
logger.info(`微信支付查询订单返回:${JSON.stringify(result)}`);
|
||||
@@ -58,7 +57,7 @@ export class PaymentWxpay implements IPaymentProvider {
|
||||
async createOrder(trade: TradeEntity, opts: { bindUrl: string; clientIp: string }) {
|
||||
const notify_url = `${opts.bindUrl}/api/payment/notify/wxpay`;
|
||||
|
||||
const pay = this.createSdk();
|
||||
const pay = await this.createSdk();
|
||||
|
||||
const params = {
|
||||
description: trade.title,
|
||||
@@ -83,7 +82,9 @@ export class PaymentWxpay implements IPaymentProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private createSdk() {
|
||||
private async createSdk() {
|
||||
const WxPayLib = await this.access.importRuntime("wechatpay-node-v3");
|
||||
const WxPay = WxPayLib.default;
|
||||
const pay = new WxPay({
|
||||
appid: this.access.appId,
|
||||
mchid: this.access.mchid,
|
||||
@@ -94,7 +95,7 @@ export class PaymentWxpay implements IPaymentProvider {
|
||||
}
|
||||
|
||||
async onNotify(notifyData: any, updateTrade: UpdateTrade) {
|
||||
const pay = this.createSdk();
|
||||
const pay = await this.createSdk();
|
||||
const { ciphertext, associated_data, nonce } = notifyData.resource;
|
||||
logger.info(`微信支付notify:${JSON.stringify(notifyData)}`);
|
||||
const key = this.access.key;
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, DnsResolveRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
|
||||
import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, DnsResolveRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
|
||||
import { AliyunAccess } from "../../plugin-lib/aliyun/access/aliyun-access.js";
|
||||
import { AliyunClient } from "../../plugin-lib/aliyun/index.js";
|
||||
import { Pager, PageRes, PageSearch } from "@certd/pipeline";
|
||||
@@ -16,7 +16,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
|
||||
async onInstance() {
|
||||
const access: AliyunAccess = this.ctx.access as AliyunAccess;
|
||||
|
||||
this.client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||
this.client = new AliyunClient({ logger: this.logger });
|
||||
await this.client.init({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { utils } from "@certd/basic";
|
||||
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
@@ -241,7 +241,7 @@ export class DeployCertToAliyunAckPlugin extends AbstractTaskPlugin {
|
||||
}
|
||||
|
||||
async getClient(aliyunProvider: any, regionId: string) {
|
||||
const client = new AliyunClient({ logger: this.logger, useROAClient: true, importRuntime: aliyunProvider.importRuntime.bind(aliyunProvider) });
|
||||
const client = new AliyunClient({ logger: this.logger, useROAClient: true });
|
||||
await client.init({
|
||||
accessKeyId: aliyunProvider.accessKeyId,
|
||||
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js";
|
||||
@@ -128,7 +128,7 @@ export class AliyunDeployCertToALB extends AbstractTaskPlugin {
|
||||
async onInstance() {}
|
||||
|
||||
async getLBClient(access: AliyunAccess, region: string) {
|
||||
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||
const client = new AliyunClient({ logger: this.logger });
|
||||
|
||||
const version = "2020-06-16";
|
||||
await client.init({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { optionsUtils } from "@certd/basic";
|
||||
import { optionsUtils } from "@certd/basic";
|
||||
import { AbstractTaskPlugin, CertTargetItem, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertReader } from "@certd/plugin-cert";
|
||||
import { CertInfo, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
@@ -209,7 +209,7 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
|
||||
}
|
||||
|
||||
async getClient(access: AliyunAccess) {
|
||||
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||
const client = new AliyunClient({ logger: this.logger });
|
||||
await client.init({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AbstractTaskPlugin, CertTargetItem, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline";
|
||||
import { AbstractTaskPlugin, CertTargetItem, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import dayjs from "dayjs";
|
||||
import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js";
|
||||
@@ -143,7 +143,7 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin {
|
||||
}
|
||||
|
||||
async getClient(access: AliyunAccess) {
|
||||
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||
const client = new AliyunClient({ logger: this.logger });
|
||||
await client.init({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
|
||||
@@ -137,8 +137,8 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
|
||||
|
||||
const client = await this.getClient(access);
|
||||
|
||||
const $Util = await access.importRuntime("@alicloud/tea-util");
|
||||
const $OpenApi = await access.importRuntime("@alicloud/openapi-client");
|
||||
const $Util = await this.importRuntime("@alicloud/tea-util");
|
||||
const $OpenApi = await this.importRuntime("@alicloud/openapi-client");
|
||||
|
||||
let privateKey = this.cert.key;
|
||||
try {
|
||||
@@ -204,7 +204,7 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
|
||||
}
|
||||
|
||||
async getClient(access: AliyunAccess) {
|
||||
const $OpenApi = await access.importRuntime("@alicloud/openapi-client");
|
||||
const $OpenApi = await this.importRuntime("@alicloud/openapi-client");
|
||||
const config = new $OpenApi.Config({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
@@ -221,8 +221,8 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
|
||||
const access = await this.getAccess<AliyunAccess>(this.accessId);
|
||||
const client = await this.getClient(access);
|
||||
|
||||
const $OpenApi = await access.importRuntime("@alicloud/openapi-client");
|
||||
const $Util = await access.importRuntime("@alicloud/tea-util");
|
||||
const $OpenApi = await this.importRuntime("@alicloud/openapi-client");
|
||||
const $Util = await this.importRuntime("@alicloud/tea-util");
|
||||
const params = new $OpenApi.Params({
|
||||
// 接口名称
|
||||
action: "ListCustomDomains",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { CertApplyPluginNames } from "@certd/plugin-cert";
|
||||
@@ -117,7 +117,7 @@ export class AliyunDeployCertToNLB extends AbstractTaskPlugin {
|
||||
async onInstance() {}
|
||||
|
||||
async getLBClient(access: AliyunAccess, region: string) {
|
||||
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||
const client = new AliyunClient({ logger: this.logger });
|
||||
|
||||
const version = "2022-04-30";
|
||||
await client.init({
|
||||
|
||||
@@ -196,7 +196,7 @@ export class DeployCertToAliyunOSS extends AbstractTaskPlugin {
|
||||
|
||||
async getClient(access: AliyunAccess) {
|
||||
// @ts-ignore
|
||||
const OSS = await access.importRuntime("ali-oss");
|
||||
const OSS = await this.importRuntime("ali-oss");
|
||||
return new OSS.default({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertInfo } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { CertApplyPluginNames } from "@certd/plugin-cert";
|
||||
@@ -130,7 +130,7 @@ export class AliyunDeployCertToSLB extends AbstractTaskPlugin {
|
||||
async onInstance() {}
|
||||
|
||||
async getLBClient(access: AliyunAccess, region: string) {
|
||||
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||
const client = new AliyunClient({ logger: this.logger });
|
||||
const version = "2014-05-15";
|
||||
await client.init({
|
||||
accessKeyId: access.accessKeyId,
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js";
|
||||
@@ -103,7 +103,7 @@ export class AliyunDeployCertToWafCloud extends AbstractTaskPlugin {
|
||||
async onInstance() {}
|
||||
|
||||
async getWafClient(access: AliyunAccess) {
|
||||
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||
const client = new AliyunClient({ logger: this.logger });
|
||||
await client.init({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js";
|
||||
@@ -115,7 +115,7 @@ export class AliyunDeployCertToWaf extends AbstractTaskPlugin {
|
||||
async onInstance() {}
|
||||
|
||||
async getWafClient(access: AliyunAccess) {
|
||||
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||
const client = new AliyunClient({ logger: this.logger });
|
||||
await client.init({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
|
||||
@@ -114,14 +114,20 @@ export class AsiaIspClient {
|
||||
|
||||
if (response.code !== "0") {
|
||||
this.logger.error(`接口请求失败: code=${response.code}, msg=${response.msg}`);
|
||||
throw new Error(response.msg || "接口请求失败");
|
||||
const e = new Error(response.msg || "接口请求失败");
|
||||
// @ts-ignore
|
||||
e.errorCode = response.code;
|
||||
throw e;
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
if (error.message && !error.message.includes("接口请求失败")) {
|
||||
this.logger.error(`接口请求异常: ${error.message}`);
|
||||
throw new Error(`接口请求异常: ${error.message}`);
|
||||
const response = error.response;
|
||||
if (response && response.data) {
|
||||
const e = new Error(response.data.msg || error.message || "接口请求失败");
|
||||
// @ts-ignore
|
||||
e.errorCode = response.data.code;
|
||||
throw e;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -189,7 +195,7 @@ export class AsiaIspClient {
|
||||
return certId;
|
||||
} catch (e: any) {
|
||||
const msg = e.message || "";
|
||||
const isExists = msg.includes("Certificate already exists") || e.code === "80003" || msg.includes("Certificate note name already exists") || e.code === "80010";
|
||||
const isExists = msg.includes("Certificate already exists") || e.errorCode === "80003" || msg.includes("Certificate note name already exists") || e.errorCode === "80010";
|
||||
//返回数据: {"code":"80010","msg":"Certificate note name already exists","data":null}
|
||||
if (!isExists) {
|
||||
throw e;
|
||||
@@ -199,7 +205,7 @@ export class AsiaIspClient {
|
||||
const list = await this.getCertList();
|
||||
const found = list.find((item: any) => item.name === certName);
|
||||
if (!found) {
|
||||
throw new Error(`证书已存在但无法查询到: ${certName}`);
|
||||
throw new Error(`证书已存在但无法查询到: 请重新申请一份证书,或者将已有证书名称修改为:${certName}`);
|
||||
}
|
||||
const certId = Number(found.certId);
|
||||
this.logger.info(`复用已有证书,证书ID: ${certId}`);
|
||||
@@ -224,15 +230,23 @@ export class AsiaIspClient {
|
||||
* PUT /openapi/v3/stat?action=domainModify
|
||||
*/
|
||||
async deployCertToDomain(req: { domain: string; certId: number; protocol: string }): Promise<void> {
|
||||
await this.doRequest({
|
||||
method: "PUT",
|
||||
action: "domainModify",
|
||||
data: {
|
||||
domain: req.domain,
|
||||
certId: `${req.certId}`,
|
||||
protocol: req.protocol || "https",
|
||||
},
|
||||
});
|
||||
try {
|
||||
await this.doRequest({
|
||||
method: "PUT",
|
||||
action: "domainModify",
|
||||
data: {
|
||||
domain: req.domain,
|
||||
certId: `${req.certId}`,
|
||||
protocol: req.protocol || "https",
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e.errorCode === "50024") {
|
||||
this.logger.info(`域名 ${req.domain} 已绑定该证书 ${req.certId},无需重复绑定`);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
this.logger.info(`部署证书到域名成功: ${req.domain}, certId=${req.certId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,107 @@
|
||||
// 导入所需的 SDK 模块
|
||||
import { AwsCNAccess } from "../access.js";
|
||||
import { CertInfo } from "@certd/plugin-cert";
|
||||
import { ILogger } from "@certd/basic";
|
||||
|
||||
type AwsIAMClientOptions = { access: AwsCNAccess; region: string };
|
||||
type AwsIAMClientOptions = { access: AwsCNAccess; region: string; logger?: ILogger };
|
||||
|
||||
// IAM ListServerCertificates 返回的证书元信息(仅保留本插件用到的字段)
|
||||
export type ServerCertificateMetadata = {
|
||||
ServerCertificateName?: string;
|
||||
ServerCertificateId?: string;
|
||||
Expiration?: Date | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 拆分完整 PEM,得到叶子证书和证书链。
|
||||
* 使用 lookbehind 分割,保留每段结尾的 -----END CERTIFICATE-----,
|
||||
* 避免证书链丢失结尾标记而变成非法 PEM(AWS 会报 MalformedCertificate)。
|
||||
*/
|
||||
export function splitCertAndChain(crt: string): { cert: string; chain: string } {
|
||||
const pemBlocks = crt.split(/(?<=-----END CERTIFICATE-----)/);
|
||||
const cert = pemBlocks[0].trim();
|
||||
const chain = pemBlocks.slice(1).join("").trim();
|
||||
return { cert, chain };
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 IAM 证书元信息列表中,挑出"本次被替换掉"的旧证书名称。
|
||||
* distribution 已改用新证书,旧证书不再被其引用,因此无论是否过期都应清理,
|
||||
* 否则提前续期/手动重部署产生的旧证书会在 IAM 中不断堆积。
|
||||
* 过滤规则:
|
||||
* - ServerCertificateId 必须命中 targetCertIds(即本次部署前 CloudFront 引用的旧证书)
|
||||
* - 不能等于 excludeCertId(本次新上传的证书,避免误删)
|
||||
* 返回去重后的 ServerCertificateName 列表。
|
||||
*/
|
||||
export function pickReplacedCertNames(params: { metadataList: ServerCertificateMetadata[]; targetCertIds: Set<string> | string[]; excludeCertId?: string }): string[] {
|
||||
const { metadataList, targetCertIds, excludeCertId } = params;
|
||||
const targetIdSet = targetCertIds instanceof Set ? targetCertIds : new Set(targetCertIds);
|
||||
|
||||
const names = new Set<string>();
|
||||
for (const metadata of metadataList) {
|
||||
const certId = metadata.ServerCertificateId;
|
||||
const certName = metadata.ServerCertificateName;
|
||||
if (!certId || !certName) {
|
||||
continue;
|
||||
}
|
||||
if (!targetIdSet.has(certId)) {
|
||||
continue;
|
||||
}
|
||||
if (excludeCertId && certId === excludeCertId) {
|
||||
continue;
|
||||
}
|
||||
names.add(certName);
|
||||
}
|
||||
return [...names];
|
||||
}
|
||||
|
||||
// CloudFront ViewerCertificate 字段(仅保留本插件用到的字段)
|
||||
export type ViewerCertificate = {
|
||||
CloudFrontDefaultCertificate?: boolean;
|
||||
ACMCertificateArn?: string;
|
||||
IAMCertificateId?: string;
|
||||
Certificate?: string;
|
||||
CertificateSource?: string;
|
||||
SSLSupportMethod?: string;
|
||||
MinimumProtocolVersion?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 基于旧 ViewerCertificate 构造使用 IAM 证书的新配置。
|
||||
* - CloudFront 要求 ACMCertificateArn、IAMCertificateId、CloudFrontDefaultCertificate 三者只能存在其一,
|
||||
* 因此这里只保留 IAM 证书,并显式将 CloudFrontDefaultCertificate 置为 false、不携带 ACMCertificateArn。
|
||||
* - Certificate/CertificateSource 为 AWS 已废弃字段,更新时不再携带,避免旧的 ACM 值残留导致校验冲突。
|
||||
* - SSLSupportMethod 强制为 sni-only:AWS 中国区 CloudFront 只支持 SNI,不支持 vip(专用IP),
|
||||
* 若沿用旧的 vip 值会报 "The parameter ViewerCertificate with the specified SSL support method isn't available in this region"。
|
||||
* - MinimumProtocolVersion 沿用旧值,缺失时给出安全默认值。
|
||||
*/
|
||||
export function buildIamViewerCertificate(params: { oldViewerCertificate?: ViewerCertificate; certId: string }): ViewerCertificate {
|
||||
const { oldViewerCertificate, certId } = params;
|
||||
const old = oldViewerCertificate || {};
|
||||
return {
|
||||
CloudFrontDefaultCertificate: false,
|
||||
IAMCertificateId: certId,
|
||||
SSLSupportMethod: "sni-only",
|
||||
MinimumProtocolVersion: old.MinimumProtocolVersion || "TLSv1.2_2021",
|
||||
};
|
||||
}
|
||||
|
||||
export class AwsIAMClient {
|
||||
options: AwsIAMClientOptions;
|
||||
access: AwsCNAccess;
|
||||
region: string;
|
||||
logger?: ILogger;
|
||||
constructor(options: AwsIAMClientOptions) {
|
||||
this.options = options;
|
||||
this.access = options.access;
|
||||
this.region = options.region;
|
||||
this.logger = options.logger;
|
||||
}
|
||||
async importCertificate(certInfo: CertInfo, certName: string) {
|
||||
// 创建 IAM 客户端
|
||||
const { IAMClient, UploadServerCertificateCommand } = await this.access.importRuntime("@aws-sdk/client-iam");
|
||||
|
||||
// 统一创建 IAM 客户端,供上传/查询/删除复用
|
||||
private async createIamClient() {
|
||||
const iamModule = await this.access.importRuntime("@aws-sdk/client-iam");
|
||||
const { IAMClient } = iamModule;
|
||||
const iamClient = new IAMClient({
|
||||
region: this.region, // 替换为您的 AWS 区域
|
||||
credentials: {
|
||||
@@ -23,20 +109,99 @@ export class AwsIAMClient {
|
||||
secretAccessKey: this.access.secretAccessKey,
|
||||
},
|
||||
});
|
||||
return { iamClient, iamModule };
|
||||
}
|
||||
|
||||
const cert = certInfo.crt.split("-----END CERTIFICATE-----")[0] + "-----END CERTIFICATE-----";
|
||||
const chain = certInfo.crt.split("-----END CERTIFICATE-----\n")[1];
|
||||
async importCertificate(certInfo: CertInfo, certName: string) {
|
||||
const { iamClient, iamModule } = await this.createIamClient();
|
||||
const { UploadServerCertificateCommand } = iamModule;
|
||||
|
||||
const { cert, chain } = splitCertAndChain(certInfo.crt);
|
||||
// 构建上传参数
|
||||
const command = new UploadServerCertificateCommand({
|
||||
Path: "/cloudfront/",
|
||||
ServerCertificateName: certName,
|
||||
CertificateBody: cert,
|
||||
PrivateKey: certInfo.key,
|
||||
CertificateChain: chain,
|
||||
CertificateChain: chain || undefined,
|
||||
});
|
||||
const data = await iamClient.send(command);
|
||||
console.log("Upload successful:", data);
|
||||
// 返回证书 ID
|
||||
return data.ServerCertificateMetadata.ServerCertificateId;
|
||||
|
||||
try {
|
||||
const data = await iamClient.send(command);
|
||||
// 返回证书 ID
|
||||
return data.ServerCertificateMetadata.ServerCertificateId;
|
||||
} catch (err) {
|
||||
const message = err.message || String(err);
|
||||
const requestId = err.$metadata?.requestId || err.requestId;
|
||||
console.error(`IAM 调用失败: ${message}, requestId: ${requestId}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// 拉取 /cloudfront/ path 下的全部 server certificate 元信息(处理分页)
|
||||
async listCloudFrontServerCertificates(): Promise<ServerCertificateMetadata[]> {
|
||||
const { iamClient, iamModule } = await this.createIamClient();
|
||||
const { ListServerCertificatesCommand } = iamModule;
|
||||
|
||||
const metadataList: ServerCertificateMetadata[] = [];
|
||||
let marker: string | undefined = undefined;
|
||||
do {
|
||||
const command = new ListServerCertificatesCommand({
|
||||
PathPrefix: "/cloudfront/",
|
||||
Marker: marker,
|
||||
});
|
||||
const data: any = await iamClient.send(command);
|
||||
const pageList: ServerCertificateMetadata[] = data.ServerCertificateMetadataList || [];
|
||||
metadataList.push(...pageList);
|
||||
marker = data.IsTruncated ? data.Marker : undefined;
|
||||
} while (marker);
|
||||
|
||||
return metadataList;
|
||||
}
|
||||
|
||||
// 按名称删除 IAM server certificate
|
||||
async deleteServerCertificate(serverCertificateName: string) {
|
||||
const { iamClient, iamModule } = await this.createIamClient();
|
||||
const { DeleteServerCertificateCommand } = iamModule;
|
||||
const command = new DeleteServerCertificateCommand({
|
||||
ServerCertificateName: serverCertificateName,
|
||||
});
|
||||
await iamClient.send(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理本次被替换掉的旧证书(无论是否过期)。
|
||||
* distribution 已改用新证书,旧证书不再被其引用,直接删除以避免 IAM 堆积。
|
||||
* 必须在更新完 CloudFront 引用之后调用,否则旧证书仍被引用会报 DeleteConflict。
|
||||
* 删除失败(如仍被其他分配引用)时只告警,不阻断部署流程。
|
||||
*/
|
||||
async deleteReplacedCerts(params: { oldCertIds: Set<string> | string[]; newCertId?: string }) {
|
||||
const { oldCertIds, newCertId } = params;
|
||||
const targetIdSet = oldCertIds instanceof Set ? oldCertIds : new Set(oldCertIds);
|
||||
if (targetIdSet.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataList = await this.listCloudFrontServerCertificates();
|
||||
const replacedCertNames = pickReplacedCertNames({
|
||||
metadataList,
|
||||
targetCertIds: targetIdSet,
|
||||
excludeCertId: newCertId,
|
||||
});
|
||||
|
||||
if (replacedCertNames.length === 0) {
|
||||
this.logger?.info("没有需要清理的旧证书");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const certName of replacedCertNames) {
|
||||
try {
|
||||
await this.deleteServerCertificate(certName);
|
||||
this.logger?.info(`已删除被替换的旧证书: ${certName}`);
|
||||
} catch (err: any) {
|
||||
const message = err?.message || String(err);
|
||||
this.logger?.warn(`删除旧证书失败(可能仍被其他分配引用),已跳过: ${certName}, 原因: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+58
-15
@@ -1,7 +1,7 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { AwsCNAccess } from "../access.js";
|
||||
import { AwsIAMClient } from "../libs/aws-iam-client.js";
|
||||
import { AwsIAMClient, buildIamViewerCertificate } from "../libs/aws-iam-client.js";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { AwsCNRegions } from "../constants.js";
|
||||
|
||||
@@ -77,10 +77,19 @@ export class AwsCNDeployToCloudFront extends AbstractTaskPlugin {
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess<AwsCNAccess>(this.accessId);
|
||||
|
||||
const iamClient = new AwsIAMClient({
|
||||
access,
|
||||
region: this.region,
|
||||
logger: this.logger,
|
||||
});
|
||||
|
||||
// 本次是否真正上传了新证书(cert 为字符串时表示直接使用已有证书ID,不涉及替换过期旧证书)
|
||||
const uploadedNewCert = typeof this.cert !== "string";
|
||||
|
||||
let certId = this.cert as string;
|
||||
if (typeof this.cert !== "string") {
|
||||
if (uploadedNewCert) {
|
||||
//先上传
|
||||
certId = await this.uploadToIAM(access, this.cert);
|
||||
certId = await this.uploadToIAM(iamClient, this.cert as CertInfo);
|
||||
}
|
||||
//部署到CloudFront
|
||||
|
||||
@@ -93,6 +102,9 @@ export class AwsCNDeployToCloudFront extends AbstractTaskPlugin {
|
||||
},
|
||||
});
|
||||
|
||||
// 记录每个分配部署前引用的旧 IAM 证书ID,部署完成后清理其中已过期的
|
||||
const oldCertIds = new Set<string>();
|
||||
|
||||
// update-distribution
|
||||
for (const distributionId of this.distributionIds) {
|
||||
// get-distribution-config
|
||||
@@ -100,30 +112,61 @@ export class AwsCNDeployToCloudFront extends AbstractTaskPlugin {
|
||||
Id: distributionId,
|
||||
});
|
||||
|
||||
const configData = await cloudFrontClient.send(getDistributionConfigCommand);
|
||||
const configData: any = await this.sendCloudFrontCommand(() => cloudFrontClient.send(getDistributionConfigCommand), `获取CloudFront配置(${distributionId})`);
|
||||
|
||||
const oldViewerCertificate = configData.DistributionConfig?.ViewerCertificate;
|
||||
const oldCertId = oldViewerCertificate?.IAMCertificateId;
|
||||
if (oldCertId) {
|
||||
oldCertIds.add(oldCertId);
|
||||
}
|
||||
|
||||
// 使用干净的 IAM ViewerCertificate,避免与旧的 ACM/默认证书字段冲突导致 InvalidViewerCertificate
|
||||
const viewerCertificate = buildIamViewerCertificate({ oldViewerCertificate, certId });
|
||||
|
||||
const updateDistributionCommand = new UpdateDistributionCommand({
|
||||
DistributionConfig: {
|
||||
...configData.DistributionConfig,
|
||||
ViewerCertificate: {
|
||||
...configData.DistributionConfig.ViewerCertificate,
|
||||
IAMCertificateId: certId,
|
||||
},
|
||||
ViewerCertificate: viewerCertificate,
|
||||
},
|
||||
Id: distributionId,
|
||||
IfMatch: configData.ETag,
|
||||
});
|
||||
await cloudFrontClient.send(updateDistributionCommand);
|
||||
await this.sendCloudFrontCommand(() => cloudFrontClient.send(updateDistributionCommand), `更新CloudFront证书(${distributionId})`);
|
||||
this.logger.info(`部署${distributionId}完成:`);
|
||||
}
|
||||
this.logger.info("部署完成");
|
||||
|
||||
// 仅当本次上传了新证书时,清理被替换掉的旧证书(无论是否过期);清理失败不影响部署结果
|
||||
if (uploadedNewCert) {
|
||||
try {
|
||||
await iamClient.deleteReplacedCerts({ oldCertIds, newCertId: certId });
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`清理旧证书失败,已忽略: ${err?.message || err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadToIAM(access: AwsCNAccess, cert: CertInfo) {
|
||||
const acmClient = new AwsIAMClient({
|
||||
access,
|
||||
region: this.region,
|
||||
});
|
||||
const awsCertID = await acmClient.importCertificate(cert, this.appendTimeSuffix(this.certName));
|
||||
/**
|
||||
* 统一包装 CloudFront 调用错误。
|
||||
* 命中 AWS 权限不足(AccessDenied / not authorized)时,抛出可读的中文提示,
|
||||
* 指明该 IAM 用户需要补充的 CloudFront 与 IAM 权限,方便运维在 AWS 控制台排查。
|
||||
*/
|
||||
private async sendCloudFrontCommand<T>(action: () => Promise<T>, actionDesc: string): Promise<T> {
|
||||
try {
|
||||
return await action();
|
||||
} catch (err: any) {
|
||||
const message = err?.message || String(err);
|
||||
const isAuthError = err?.name === "AccessDenied" || /not authorized to perform|no identity-based policy/i.test(message);
|
||||
if (isAuthError) {
|
||||
const requiredPermissions = ["cloudfront:ListDistributions", "cloudfront:GetDistributionConfig", "cloudfront:UpdateDistribution", "iam:UploadServerCertificate"].join("、");
|
||||
throw new Error(`${actionDesc}失败:AWS 账号权限不足,请为该 IAM 用户附加 CloudFront 部署所需权限(${requiredPermissions})。原始错误:${message}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadToIAM(iamClient: AwsIAMClient, cert: CertInfo) {
|
||||
const awsCertID = await iamClient.importCertificate(cert, this.appendTimeSuffix(this.certName));
|
||||
this.logger.info("证书上传成功,id=", awsCertID);
|
||||
return awsCertID;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
|
||||
}
|
||||
this._result.pipelinePrivateVars.cert = cert;
|
||||
|
||||
if (isNew || !cert.pfx || !cert.der || !cert.jks || !cert.p7b) {
|
||||
if (isNew || !cert.pfx) {
|
||||
try {
|
||||
const converter = new CertConverter({ logger: this.logger });
|
||||
const res = await converter.convert({
|
||||
|
||||
@@ -101,7 +101,7 @@ export class AliossAccess extends BaseAccess {
|
||||
|
||||
async getClient(access: AliyunAccess) {
|
||||
// @ts-ignore
|
||||
const OSS = await access.importRuntime("ali-oss");
|
||||
const OSS = await this.importRuntime("ali-oss");
|
||||
return new OSS.default({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
import { AliyunClientV2 } from "../lib/aliyun-client-v2.js";
|
||||
import { AliyunSslClient } from "../lib/ssl-client.js";
|
||||
@IsAccess({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ILogger } from "@certd/basic";
|
||||
import { ILogger } from "@certd/basic";
|
||||
import { AliyunAccess } from "../access/aliyun-access.js";
|
||||
import { importRuntime as importRuntimeDirect } from "@certd/pipeline";
|
||||
|
||||
export type AliyunClientV2Req = {
|
||||
action: string;
|
||||
@@ -26,11 +27,15 @@ export class AliyunClientV2 {
|
||||
this.endpoint = opts.endpoint;
|
||||
}
|
||||
|
||||
async importRuntime(name: string) {
|
||||
return await importRuntimeDirect(name, this.logger);
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
if (this.client) {
|
||||
return this.client;
|
||||
}
|
||||
const $OpenApi = await this.access.importRuntime("@alicloud/openapi-client");
|
||||
const $OpenApi = await this.importRuntime("@alicloud/openapi-client");
|
||||
// const Credential = await import("@alicloud/credentials");
|
||||
// //@ts-ignore
|
||||
// const credential = new Credential.default.default({
|
||||
@@ -52,9 +57,9 @@ export class AliyunClientV2 {
|
||||
async doRequest(req: AliyunClientV2Req) {
|
||||
const client = await this.getClient();
|
||||
|
||||
const $OpenApi = await this.access.importRuntime("@alicloud/openapi-client");
|
||||
const $Util = await this.access.importRuntime("@alicloud/tea-util");
|
||||
const OpenApiUtil = await this.access.importRuntime("@alicloud/openapi-util");
|
||||
const $OpenApi = await this.importRuntime("@alicloud/openapi-client");
|
||||
const $Util = await this.importRuntime("@alicloud/tea-util");
|
||||
const OpenApiUtil = await this.importRuntime("@alicloud/openapi-util");
|
||||
const params = new $OpenApi.Params({
|
||||
// 接口名称
|
||||
action: req.action,
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { getGlobalAgents, ILogger } from "@certd/basic";
|
||||
import { ImportRuntime } from "@certd/pipeline";
|
||||
import { getGlobalAgents, ILogger } from "@certd/basic";
|
||||
import { importRuntime as importRuntimeDirect } from "@certd/pipeline";
|
||||
|
||||
export class AliyunClient {
|
||||
client: any;
|
||||
logger: ILogger;
|
||||
agent: any;
|
||||
useROAClient: boolean;
|
||||
importRuntime: ImportRuntime;
|
||||
|
||||
constructor(opts: { logger: ILogger; useROAClient?: boolean; importRuntime?: ImportRuntime }) {
|
||||
constructor(opts: { logger: ILogger; useROAClient?: boolean }) {
|
||||
this.logger = opts.logger;
|
||||
this.useROAClient = opts.useROAClient || false;
|
||||
this.importRuntime = opts.importRuntime || (async (specifier: string) => await import(specifier));
|
||||
|
||||
const agents = getGlobalAgents();
|
||||
this.agent = agents.httpsAgent;
|
||||
}
|
||||
|
||||
async importRuntime(specifier: string) {
|
||||
return await importRuntimeDirect(specifier, this.logger);
|
||||
}
|
||||
|
||||
async getSdk() {
|
||||
if (this.useROAClient) {
|
||||
return await this.getROAClient();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ILogger, utils } from "@certd/basic";
|
||||
import { ILogger, utils } from "@certd/basic";
|
||||
import { AliyunAccess } from "../access/index.js";
|
||||
import { AliyunClient } from "./index.js";
|
||||
import { CertInfo, CertReader, SimpleCertDetail } from "@certd/plugin-lib";
|
||||
@@ -42,6 +42,7 @@ export type CasCertId = {
|
||||
export class AliyunSslClient {
|
||||
opts: AliyunSslClientOpts;
|
||||
logger: ILogger;
|
||||
|
||||
constructor(opts: AliyunSslClientOpts) {
|
||||
this.opts = opts;
|
||||
this.logger = opts.logger;
|
||||
@@ -55,7 +56,7 @@ export class AliyunSslClient {
|
||||
|
||||
async getClient() {
|
||||
const access = this.opts.access;
|
||||
const client = new AliyunClient({ logger: this.opts.logger, importRuntime: access.importRuntime.bind(access) });
|
||||
const client = new AliyunClient({ logger: this.opts.logger });
|
||||
|
||||
let endpoint = this.opts.endpoint || "cas.aliyuncs.com";
|
||||
if (this.opts.endpoint == null && this.opts.region) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import dayjs from "dayjs";
|
||||
import dayjs from "dayjs";
|
||||
import { TencentAccess, TencentCosAccess, TencentCosClient } from "../../tencent/index.js";
|
||||
import { BaseOssClient, OssFileItem } from "../api.js";
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export class TencentAccess extends BaseAccess {
|
||||
}
|
||||
|
||||
async getStsClient() {
|
||||
const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sts/v20180813/index.js");
|
||||
const sdk = await this.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sts/v20180813/index.js");
|
||||
const StsClient = sdk.v20180813.Client;
|
||||
|
||||
const clientConfig = {
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { TencentAccess } from "../access.js";
|
||||
import { TencentAccess } from "../access.js";
|
||||
import { ILogger, safePromise } from "@certd/basic";
|
||||
import { ImportRuntime } from "@certd/pipeline";
|
||||
import fs from "fs";
|
||||
import { importRuntime as importRuntimeDirect } from "@certd/pipeline";
|
||||
|
||||
export class TencentCosClient {
|
||||
access: TencentAccess;
|
||||
logger: ILogger;
|
||||
region: string;
|
||||
bucket: string;
|
||||
importRuntime: ImportRuntime;
|
||||
|
||||
constructor(opts: { access: TencentAccess; logger: ILogger; region: string; bucket: string; importRuntime?: ImportRuntime }) {
|
||||
constructor(opts: { access: TencentAccess; logger: ILogger; region: string; bucket: string }) {
|
||||
this.access = opts.access;
|
||||
this.logger = opts.logger;
|
||||
this.bucket = opts.bucket;
|
||||
this.region = opts.region;
|
||||
this.importRuntime = opts.importRuntime || (async (specifier: string) => await import(specifier));
|
||||
}
|
||||
|
||||
async importRuntime(specifier: string) {
|
||||
return await importRuntimeDirect(specifier, this.logger);
|
||||
}
|
||||
|
||||
async getCosClient() {
|
||||
|
||||
-348
@@ -1,348 +0,0 @@
|
||||
name: NginxProxyManagerDeploy
|
||||
icon: logos:nginx
|
||||
title: Nginx Proxy Manager-部署到主机
|
||||
group: panel
|
||||
desc: 上传自定义证书到 Nginx Proxy Manager,并绑定到所选主机。
|
||||
setting: null
|
||||
sysSetting: null
|
||||
type: custom
|
||||
disabled: false
|
||||
version: 1.0.0
|
||||
pluginType: deploy
|
||||
author: samler
|
||||
input:
|
||||
cert:
|
||||
title: 域名证书
|
||||
helper: 请选择前置任务产出的证书。
|
||||
component:
|
||||
name: output-selector
|
||||
from:
|
||||
- ":cert:"
|
||||
required: true
|
||||
order: 0
|
||||
certDomains:
|
||||
title: 证书域名
|
||||
component:
|
||||
name: cert-domains-getter
|
||||
mergeScript: |
|
||||
return {
|
||||
component: {
|
||||
inputKey: ctx.compute(({ form }) => {
|
||||
return form.cert;
|
||||
}),
|
||||
},
|
||||
}
|
||||
required: false
|
||||
order: 0
|
||||
accessId:
|
||||
title: NPM授权
|
||||
component:
|
||||
name: access-selector
|
||||
type: samler/nginxProxyManager
|
||||
helper: 选择用于部署的 Nginx Proxy Manager 授权。
|
||||
required: true
|
||||
order: 0
|
||||
proxyHostIds:
|
||||
title: 代理主机
|
||||
component:
|
||||
name: remote-select
|
||||
vModel: value
|
||||
mode: tags
|
||||
type: plugin
|
||||
action: onGetProxyHostOptions
|
||||
search: true
|
||||
pager: false
|
||||
multi: true
|
||||
watches:
|
||||
- certDomains
|
||||
- accessId
|
||||
required: true
|
||||
helper: 选择要绑定此证书的一个或多个代理主机。
|
||||
mergeScript: |
|
||||
return {
|
||||
component: {
|
||||
form: ctx.compute(({ form }) => {
|
||||
return form;
|
||||
}),
|
||||
},
|
||||
}
|
||||
order: 0
|
||||
certificateLabel:
|
||||
title: 证书标识
|
||||
component:
|
||||
name: a-input
|
||||
allowClear: true
|
||||
placeholder: certd_npm_example_com
|
||||
helper: 可选。留空时默认使用 certd_npm_<主域名规范化>.
|
||||
required: false
|
||||
order: 0
|
||||
cleanupMatchingCertificates:
|
||||
title: 自动清理未使用证书
|
||||
component:
|
||||
name: a-switch
|
||||
vModel: checked
|
||||
helper: 部署成功后,自动删除除当前证书外所有未被任何主机引用的证书。
|
||||
required: false
|
||||
order: 0
|
||||
output: {}
|
||||
default:
|
||||
strategy:
|
||||
runStrategy: 1
|
||||
showRunStrategy: false
|
||||
content: |
|
||||
const { AbstractTaskPlugin } = await import("@certd/pipeline");
|
||||
const { CertReader } = await import("@certd/plugin-cert");
|
||||
|
||||
function normalizeDomain(domain) {
|
||||
return String(domain ?? "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function wildcardMatches(pattern, candidate) {
|
||||
if (!pattern.startsWith("*.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const suffix = pattern.slice(1).toLowerCase();
|
||||
return candidate.endsWith(suffix);
|
||||
}
|
||||
|
||||
function isDomainMatch(left, right) {
|
||||
const normalizedLeft = normalizeDomain(left);
|
||||
const normalizedRight = normalizeDomain(right);
|
||||
|
||||
return (
|
||||
normalizedLeft === normalizedRight ||
|
||||
wildcardMatches(normalizedLeft, normalizedRight) ||
|
||||
wildcardMatches(normalizedRight, normalizedLeft)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeDomainIdentity(domain) {
|
||||
return normalizeDomain(domain).replace(/^\*\./, "");
|
||||
}
|
||||
|
||||
function certificateHasBindings(certificate) {
|
||||
return (
|
||||
(certificate.proxy_hosts?.length ?? 0) > 0 ||
|
||||
(certificate.redirection_hosts?.length ?? 0) > 0 ||
|
||||
(certificate.dead_hosts?.length ?? 0) > 0 ||
|
||||
(certificate.streams?.length ?? 0) > 0
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeDomainSegment(value) {
|
||||
const sanitized = String(value ?? "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.replace(/_+/g, "_");
|
||||
|
||||
return sanitized || "unknown";
|
||||
}
|
||||
|
||||
function buildDefaultCertificateLabel(cert) {
|
||||
const mainDomain = CertReader.getMainDomain(cert.crt);
|
||||
return `certd_npm_${sanitizeDomainSegment(mainDomain)}`;
|
||||
}
|
||||
|
||||
function normalizeStringList(input) {
|
||||
if (Array.isArray(input)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (input == null || input === "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [input];
|
||||
}
|
||||
|
||||
function resolveCertificateDomains(cert, configuredDomains) {
|
||||
const configured = normalizeStringList(configuredDomains)
|
||||
.map((value) => String(value).trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (configured.length > 0) {
|
||||
return Array.from(new Set(configured));
|
||||
}
|
||||
|
||||
return new CertReader(cert).getAllDomains();
|
||||
}
|
||||
|
||||
function buildProxyHostLabel(host) {
|
||||
const domains = host.domain_names?.length ? host.domain_names.join(", ") : "(no domains)";
|
||||
return `${domains} <#${host.id}>`;
|
||||
}
|
||||
|
||||
function hasAnyCertDomainMatch(host, certDomains) {
|
||||
if (!certDomains.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hostDomains = host.domain_names ?? [];
|
||||
return hostDomains.some((hostDomain) => certDomains.some((certDomain) => isDomainMatch(hostDomain, certDomain)));
|
||||
}
|
||||
|
||||
function buildProxyHostOptions(hosts, certDomains) {
|
||||
const sortedHosts = [...hosts].sort((left, right) => {
|
||||
return buildProxyHostLabel(left).localeCompare(buildProxyHostLabel(right));
|
||||
});
|
||||
|
||||
const matched = [];
|
||||
const unmatched = [];
|
||||
|
||||
for (const host of sortedHosts) {
|
||||
const option = {
|
||||
label: buildProxyHostLabel(host),
|
||||
value: String(host.id),
|
||||
domain: host.domain_names?.[0] ?? "",
|
||||
};
|
||||
|
||||
if (hasAnyCertDomainMatch(host, certDomains)) {
|
||||
matched.push(option);
|
||||
} else {
|
||||
unmatched.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
if (matched.length && unmatched.length) {
|
||||
return [
|
||||
{
|
||||
label: "匹配证书域名的主机",
|
||||
options: matched,
|
||||
},
|
||||
{
|
||||
label: "其他代理主机",
|
||||
options: unmatched,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return matched.length ? matched : unmatched;
|
||||
}
|
||||
|
||||
function normalizeProxyHostIds(proxyHostIds) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
normalizeStringList(proxyHostIds)
|
||||
.map((value) => Number.parseInt(String(value), 10))
|
||||
.filter((value) => Number.isInteger(value) && value > 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return class NpmDeployToProxyHosts extends AbstractTaskPlugin {
|
||||
cert;
|
||||
certDomains;
|
||||
accessId;
|
||||
proxyHostIds;
|
||||
certificateLabel;
|
||||
cleanupMatchingCertificates = false;
|
||||
|
||||
async execute() {
|
||||
const access = await this.getAccess(this.accessId);
|
||||
const client = access.createClient();
|
||||
const proxyHostIds = normalizeProxyHostIds(this.proxyHostIds);
|
||||
|
||||
if (proxyHostIds.length === 0) {
|
||||
throw new Error("请至少选择一个 Nginx Proxy Manager 代理主机");
|
||||
}
|
||||
|
||||
const certificateLabel = this.certificateLabel?.trim() || buildDefaultCertificateLabel(this.cert);
|
||||
const certificateDomains = resolveCertificateDomains(this.cert, this.certDomains);
|
||||
|
||||
let certificate = await client.findCustomCertificateByNiceName(certificateLabel);
|
||||
if (!certificate) {
|
||||
this.logger.info(`在 Nginx Proxy Manager 中创建自定义证书 "${certificateLabel}"`);
|
||||
certificate = await client.createCustomCertificate(certificateLabel, certificateDomains);
|
||||
} else {
|
||||
this.logger.info(`复用已有自定义证书 "${certificateLabel}" (#${certificate.id})`);
|
||||
}
|
||||
|
||||
await client.uploadCertificate(certificate.id, {
|
||||
certificate: this.cert.crt,
|
||||
certificateKey: this.cert.key,
|
||||
intermediateCertificate: this.cert.ic,
|
||||
});
|
||||
this.logger.info(`证书内容已上传到 Nginx Proxy Manager 证书 #${certificate.id}`);
|
||||
|
||||
for (const proxyHostId of proxyHostIds) {
|
||||
this.logger.info(`将证书 #${certificate.id} 绑定到代理主机 #${proxyHostId}`);
|
||||
try {
|
||||
await client.assignCertificateToProxyHost(proxyHostId, certificate.id);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`为代理主机 #${proxyHostId} 绑定证书失败:${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.cleanupMatchingCertificates === true) {
|
||||
await this.cleanupOldCertificates(client, certificate.id);
|
||||
}
|
||||
|
||||
this.logger.info(`部署完成,共更新 ${proxyHostIds.length} 个代理主机`);
|
||||
}
|
||||
|
||||
async onGetProxyHostOptions(req = {}) {
|
||||
if (!this.accessId) {
|
||||
throw new Error("请先选择 Nginx Proxy Manager 授权");
|
||||
}
|
||||
|
||||
const access = await this.getAccess(this.accessId);
|
||||
const proxyHosts = await access.getProxyHostList(req);
|
||||
return buildProxyHostOptions(proxyHosts, normalizeStringList(this.certDomains));
|
||||
}
|
||||
|
||||
async cleanupOldCertificates(client, currentCertificateId) {
|
||||
const certificates = await client.getCertificatesWithExpand(undefined, [
|
||||
"proxy_hosts",
|
||||
"redirection_hosts",
|
||||
"dead_hosts",
|
||||
"streams",
|
||||
]);
|
||||
|
||||
const candidates = certificates.filter((certificate) => {
|
||||
return certificate.id !== currentCertificateId;
|
||||
});
|
||||
|
||||
if (candidates.length === 0) {
|
||||
this.logger.info("未发现可自动清理的旧证书");
|
||||
return;
|
||||
}
|
||||
|
||||
const deletedIds = [];
|
||||
const skippedInUse = [];
|
||||
const failedDeletes = [];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (certificateHasBindings(candidate)) {
|
||||
skippedInUse.push(`#${candidate.id} ${candidate.nice_name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.info(`自动清理旧证书 #${candidate.id} ${candidate.nice_name}`);
|
||||
try {
|
||||
await client.deleteCertificate(candidate.id);
|
||||
deletedIds.push(candidate.id);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
failedDeletes.push(`#${candidate.id} ${candidate.nice_name}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedIds.length > 0) {
|
||||
this.logger.info(`自动清理完成,共删除 ${deletedIds.length} 张旧证书:${deletedIds.map((id) => `#${id}`).join(", ")}`);
|
||||
} else {
|
||||
this.logger.info("未删除任何旧证书");
|
||||
}
|
||||
|
||||
if (skippedInUse.length > 0) {
|
||||
this.logger.info(`以下旧证书仍被其他资源引用,已跳过清理:${skippedInUse.join(", ")}`);
|
||||
}
|
||||
|
||||
if (failedDeletes.length > 0) {
|
||||
this.logger.warn(`以下旧证书清理失败,已跳过:${failedDeletes.join(", ")}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
import FormData from "form-data";
|
||||
import { authenticator } from "otplib";
|
||||
|
||||
export interface ProxyHost {
|
||||
id: number;
|
||||
@@ -274,6 +273,7 @@ export class NginxProxyManagerAccess extends BaseAccess {
|
||||
|
||||
let code: string;
|
||||
try {
|
||||
const { authenticator } = await this.importRuntime("otplib");
|
||||
code = authenticator.generate(this.totpSecret);
|
||||
} catch (error) {
|
||||
throw this.describeError(error, "Generating TOTP code");
|
||||
|
||||
-344
@@ -1,344 +0,0 @@
|
||||
name: nginxProxyManager
|
||||
icon: logos:nginx
|
||||
title: Nginx Proxy Manager 授权
|
||||
group: null
|
||||
desc: 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。
|
||||
setting: null
|
||||
sysSetting: null
|
||||
type: custom
|
||||
disabled: false
|
||||
version: 1.0.0
|
||||
pluginType: access
|
||||
author: samler
|
||||
input:
|
||||
endpoint:
|
||||
title: NPM 地址
|
||||
component:
|
||||
name: a-input
|
||||
allowClear: true
|
||||
placeholder: https://npm.example.com
|
||||
helper: 请输入 Nginx Proxy Manager 根地址,不要带 /api 后缀。
|
||||
required: true
|
||||
email:
|
||||
title: 邮箱
|
||||
component:
|
||||
name: a-input
|
||||
allowClear: true
|
||||
placeholder: admin@example.com
|
||||
required: true
|
||||
password:
|
||||
title: 密码
|
||||
component:
|
||||
name: a-input-password
|
||||
allowClear: true
|
||||
placeholder: 请输入密码
|
||||
required: true
|
||||
encrypt: true
|
||||
totpSecret:
|
||||
title: TOTP 密钥
|
||||
component:
|
||||
name: a-input-password
|
||||
allowClear: true
|
||||
placeholder: Optional base32 TOTP secret
|
||||
helper: 当 Nginx Proxy Manager 账号开启 2FA 时必填。
|
||||
required: false
|
||||
encrypt: true
|
||||
ignoreTls:
|
||||
title: 忽略无效 TLS
|
||||
component:
|
||||
name: a-switch
|
||||
vModel: checked
|
||||
helper: 仅在 Nginx Proxy Manager 使用自签 HTTPS 证书时开启。
|
||||
required: false
|
||||
testRequest:
|
||||
title: 测试
|
||||
component:
|
||||
name: api-test
|
||||
action: TestRequest
|
||||
helper: 测试登录并拉取代理主机列表。
|
||||
content: |
|
||||
const { BaseAccess } = await import("@certd/pipeline");
|
||||
const httpsModule = await import("node:https");
|
||||
const { URL } = await import("node:url");
|
||||
const axiosModule = await import("axios");
|
||||
const formDataModule = await import("form-data");
|
||||
const { authenticator } = await import("otplib");
|
||||
|
||||
const https = httpsModule.default ?? httpsModule;
|
||||
const axios = axiosModule.default ?? axiosModule;
|
||||
const FormData = formDataModule.default ?? formDataModule;
|
||||
|
||||
function normalizeEndpoint(endpoint) {
|
||||
const trimmed = String(endpoint ?? "").trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("Nginx Proxy Manager 地址不能为空");
|
||||
}
|
||||
|
||||
const withoutTrailingSlash = trimmed.replace(/\/+$/, "");
|
||||
return withoutTrailingSlash.endsWith("/api")
|
||||
? withoutTrailingSlash.slice(0, -4)
|
||||
: withoutTrailingSlash;
|
||||
}
|
||||
|
||||
function buildHttpsAgent(endpoint, ignoreTls) {
|
||||
const url = new URL(endpoint);
|
||||
if (url.protocol !== "https:") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new https.Agent({
|
||||
rejectUnauthorized: !ignoreTls,
|
||||
});
|
||||
}
|
||||
|
||||
function describeError(error, action) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status;
|
||||
const data = error.response?.data;
|
||||
const message =
|
||||
data?.error?.message ||
|
||||
data?.error ||
|
||||
data?.message ||
|
||||
(typeof data === "string" ? data : null) ||
|
||||
error.message;
|
||||
return new Error(`${action} failed${status ? ` (${status})` : ""}: ${message}`);
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return new Error(`${action} failed: ${error.message}`);
|
||||
}
|
||||
|
||||
return new Error(`${action} failed`);
|
||||
}
|
||||
|
||||
class NginxProxyManagerClient {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
this.endpoint = normalizeEndpoint(options.endpoint);
|
||||
this.apiBaseUrl = `${this.endpoint}/api`;
|
||||
this.token = undefined;
|
||||
this.tokenPromise = undefined;
|
||||
this.httpClient = axios.create({
|
||||
baseURL: this.apiBaseUrl,
|
||||
timeout: 30000,
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Infinity,
|
||||
httpsAgent: buildHttpsAgent(this.endpoint, options.ignoreTls === true),
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async verifyAccess() {
|
||||
const proxyHosts = await this.getProxyHosts();
|
||||
return {
|
||||
proxyHostCount: proxyHosts.length,
|
||||
};
|
||||
}
|
||||
|
||||
async getProxyHosts(searchQuery) {
|
||||
return await this.requestWithAuth({
|
||||
method: "GET",
|
||||
url: "/nginx/proxy-hosts",
|
||||
params: {
|
||||
expand: "certificate",
|
||||
...(searchQuery ? { query: searchQuery } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getCertificates(searchQuery) {
|
||||
return await this.requestWithAuth({
|
||||
method: "GET",
|
||||
url: "/nginx/certificates",
|
||||
params: searchQuery ? { query: searchQuery } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async getCertificatesWithExpand(searchQuery, expand = []) {
|
||||
return await this.requestWithAuth({
|
||||
method: "GET",
|
||||
url: "/nginx/certificates",
|
||||
params: {
|
||||
...(searchQuery ? { query: searchQuery } : {}),
|
||||
...(expand.length > 0 ? { expand: expand.join(",") } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findCustomCertificateByNiceName(niceName) {
|
||||
const certificates = await this.getCertificates(niceName);
|
||||
return certificates.find((certificate) => {
|
||||
return certificate.provider === "other" && certificate.nice_name === niceName;
|
||||
});
|
||||
}
|
||||
|
||||
async createCustomCertificate(niceName, domainNames = []) {
|
||||
return await this.requestWithAuth({
|
||||
method: "POST",
|
||||
url: "/nginx/certificates",
|
||||
data: {
|
||||
provider: "other",
|
||||
nice_name: niceName,
|
||||
domain_names: domainNames,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteCertificate(certificateId) {
|
||||
await this.requestWithAuth({
|
||||
method: "DELETE",
|
||||
url: `/nginx/certificates/${certificateId}`,
|
||||
});
|
||||
}
|
||||
|
||||
async uploadCertificate(certificateId, payload) {
|
||||
const form = new FormData();
|
||||
form.append("certificate", Buffer.from(payload.certificate, "utf8"), {
|
||||
filename: "fullchain.pem",
|
||||
contentType: "application/x-pem-file",
|
||||
});
|
||||
form.append("certificate_key", Buffer.from(payload.certificateKey, "utf8"), {
|
||||
filename: "privkey.pem",
|
||||
contentType: "application/x-pem-file",
|
||||
});
|
||||
if (payload.intermediateCertificate) {
|
||||
form.append("intermediate_certificate", Buffer.from(payload.intermediateCertificate, "utf8"), {
|
||||
filename: "chain.pem",
|
||||
contentType: "application/x-pem-file",
|
||||
});
|
||||
}
|
||||
|
||||
await this.requestWithAuth({
|
||||
method: "POST",
|
||||
url: `/nginx/certificates/${certificateId}/upload`,
|
||||
data: form,
|
||||
headers: form.getHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
async assignCertificateToProxyHost(hostId, certificateId) {
|
||||
await this.requestWithAuth({
|
||||
method: "PUT",
|
||||
url: `/nginx/proxy-hosts/${hostId}`,
|
||||
data: {
|
||||
certificate_id: certificateId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async login() {
|
||||
if (this.token) {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
if (!this.tokenPromise) {
|
||||
this.tokenPromise = this.performLogin().finally(() => {
|
||||
this.tokenPromise = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
this.token = await this.tokenPromise;
|
||||
return this.token;
|
||||
}
|
||||
|
||||
async performLogin() {
|
||||
const initialLogin = await this.request({
|
||||
method: "POST",
|
||||
url: "/tokens",
|
||||
data: {
|
||||
identity: this.options.email,
|
||||
secret: this.options.password,
|
||||
},
|
||||
});
|
||||
|
||||
if (initialLogin.token) {
|
||||
return initialLogin.token;
|
||||
}
|
||||
|
||||
if (!initialLogin.requires_2fa || !initialLogin.challenge_token) {
|
||||
throw new Error("登录失败:Nginx Proxy Manager 未返回访问令牌");
|
||||
}
|
||||
|
||||
if (!this.options.totpSecret) {
|
||||
throw new Error("登录失败:该 Nginx Proxy Manager 账号启用了 2FA,但未配置 totpSecret");
|
||||
}
|
||||
|
||||
let code;
|
||||
try {
|
||||
code = authenticator.generate(this.options.totpSecret);
|
||||
} catch (error) {
|
||||
throw describeError(error, "Generating TOTP code");
|
||||
}
|
||||
|
||||
const completedLogin = await this.request({
|
||||
method: "POST",
|
||||
url: "/tokens/2fa",
|
||||
data: {
|
||||
challenge_token: initialLogin.challenge_token,
|
||||
code,
|
||||
},
|
||||
});
|
||||
|
||||
if (!completedLogin.token) {
|
||||
throw new Error("2FA 登录失败:Nginx Proxy Manager 未返回访问令牌");
|
||||
}
|
||||
|
||||
return completedLogin.token;
|
||||
}
|
||||
|
||||
async requestWithAuth(config) {
|
||||
const token = await this.login();
|
||||
const headers = {
|
||||
...(config.headers ?? {}),
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
|
||||
return await this.request({
|
||||
...config,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
async request(config) {
|
||||
const action = `${config.method ?? "GET"} ${config.url ?? "/"}`;
|
||||
try {
|
||||
const response = await this.httpClient.request(config);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw describeError(error, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return class NginxProxyManagerAccess extends BaseAccess {
|
||||
endpoint = "";
|
||||
email = "";
|
||||
password = "";
|
||||
totpSecret = "";
|
||||
ignoreTls = false;
|
||||
testRequest = true;
|
||||
|
||||
createClient() {
|
||||
return new NginxProxyManagerClient({
|
||||
endpoint: this.endpoint,
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
totpSecret: this.totpSecret || undefined,
|
||||
ignoreTls: this.ignoreTls === true,
|
||||
});
|
||||
}
|
||||
|
||||
async onTestRequest() {
|
||||
const client = this.createClient();
|
||||
const result = await client.verifyAccess();
|
||||
this.ctx.logger.info(`Nginx Proxy Manager 授权验证成功,找到 ${result.proxyHostCount} 个代理主机`);
|
||||
return `成功(${result.proxyHostCount} 个代理主机)`;
|
||||
}
|
||||
|
||||
async getProxyHostList(req = {}) {
|
||||
const client = this.createClient();
|
||||
return await client.getProxyHosts(req.searchKey);
|
||||
}
|
||||
};
|
||||
@@ -121,7 +121,7 @@ export class DingTalkNotification extends BaseNotification {
|
||||
at.isAtAll = true;
|
||||
}
|
||||
|
||||
const color = body.errorMessage ? "red" : "green";
|
||||
const color = body.errorMessage ? "#CC0000": "#00CC66";
|
||||
const res = await this.http.request({
|
||||
url: webhook,
|
||||
method: "POST",
|
||||
|
||||
@@ -49,7 +49,7 @@ export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
|
||||
issuerUrl = "";
|
||||
|
||||
async getClient() {
|
||||
const client = await import("openid-client");
|
||||
const client = await this.importRuntime("openid-client");
|
||||
const server = new URL(this.issuerUrl); // Authorization Server's Issuer Identifier
|
||||
|
||||
const config = await client.discovery(server, this.clientId, this.clientSecretKey);
|
||||
|
||||
@@ -142,7 +142,7 @@ export class DeployCertToTencentCosPlugin extends AbstractTaskPlugin {
|
||||
async onGetDomainList(data: any) {
|
||||
const access = await this.getAccess(this.accessId);
|
||||
|
||||
const cosv5 = await (this as any).importRuntime("cos-nodejs-sdk-v5");
|
||||
const cosv5 = await this.importRuntime("cos-nodejs-sdk-v5");
|
||||
const cos = new cosv5.default({
|
||||
SecretId: access.secretId,
|
||||
SecretKey: access.secretKey,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user