mirror of
https://github.com/certd/certd.git
synced 2026-10-14 09:55:57 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc60cd4cd1 | ||
|
|
dade599acb | ||
|
|
1070417a36 | ||
|
|
81f676f1ca | ||
|
|
0cf0608d3f | ||
|
|
dac94b64f0 | ||
|
|
8c88092235 | ||
|
|
2a2e963914 | ||
|
|
1b882a2ca4 | ||
|
|
bd9433478a | ||
|
|
32ade8e4a3 | ||
|
|
f2bcdeeb33 | ||
|
|
4099af7f5d |
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **plugin-service:** 修复同步自己开发的云端插件不显示已安装状态问题 ([dac94b6](https://github.com/certd/certd/commit/dac94b64f0ceb7176e2cf07ce220be0aa5ed98d2))
|
||||
* **plugin:** 修复插件配置保存与加载的相关问题 ([bd94334](https://github.com/certd/certd/commit/bd9433478a43d5d64ce6370386d0fe0aaff07d2e))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **plugin:** add Fastly custom TLS certificates integration ([#797](https://github.com/certd/certd/issues/797)) [@szhu25](https://github.com/szhu25) ([2a2e963](https://github.com/certd/certd/commit/2a2e963914c4dfc192cbe0a4bbb9c115e626da01))
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -105,14 +105,17 @@ https://certd.handfree.work/
|
||||
|
||||
#### Docker镜像说明:
|
||||
|
||||
##### 1. 镜像地址格式:
|
||||
##### 1. 镜像地址:
|
||||
|
||||
```
|
||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
|
||||
------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型 (中括号表示可选)
|
||||
| 镜像仓库 | 最新预览版 | 稳定版 | slim版 | armv7版 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
|
||||
| Docker Hub | `certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
|
||||
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
|
||||
|
||||
```
|
||||
##### 2. 版本标签:
|
||||
> 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
|
||||
|
||||
##### 2. 版本标签说明:
|
||||
|
||||
**最新版本标签:**
|
||||
|
||||
@@ -131,16 +134,8 @@ registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
|
||||
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` | `certd:1.43.0-slim` |
|
||||
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` | `certd:1.43.0-armv7` |
|
||||
|
||||
##### 3. 镜像地址:
|
||||
|
||||
| 镜像仓库 | 最新预览版 | slim | armv7 |
|
||||
| --- | --- | --- | --- |
|
||||
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` |
|
||||
| Docker Hub | `certd/certd:latest` | `certd:slim` |
|
||||
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` |
|
||||
|
||||
> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
|
||||
> 2. 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
|
||||
> 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
|
||||
|
||||
最新版本号:
|
||||

|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复插件市场插件安装无效的bug ([15d2bd8](https://github.com/certd/certd/commit/15d2bd8484a9ddd142bf4ef03d839c74405f9e6d))
|
||||
* preserve Route53 TXT record values ([#791](https://github.com/certd/certd/issues/791)) ([f5430a8](https://github.com/certd/certd/commit/f5430a8925cf6cf4e16c75f2358fbd78437e6705))
|
||||
|
||||
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -6,13 +6,17 @@
|
||||
|
||||
Certd 提供多种 Docker 镜像版本,您可以根据需要选择:
|
||||
|
||||
#### 1. 镜像地址格式:
|
||||
#### 1. 镜像地址:
|
||||
|
||||
```
|
||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
|
||||
------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型
|
||||
```
|
||||
#### 2. 版本标签:
|
||||
| 镜像仓库 | 最新预览版 | 稳定版 | slim版 | armv7版 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
|
||||
| Docker Hub | `certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
|
||||
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:stable` | `certd:slim` | `certd:armv7` |
|
||||
|
||||
> 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
|
||||
|
||||
#### 2. 版本标签说明:
|
||||
|
||||
**最新版本标签:**
|
||||
|
||||
@@ -25,22 +29,14 @@ registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
|
||||
|
||||
> 根据基础镜像不同,分为如下三个分支版本,没有特殊需求选择默认的即可(他们功能是一样的)
|
||||
|
||||
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 | 指定版本 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小,支持x86、ARM架构 | `certd:stable` | `certd:1.43.0` |
|
||||
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 | 指定版本 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小 | `certd:stable` | `certd:1.43.0` |
|
||||
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` | `certd:1.43.0-slim` |
|
||||
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` | `certd:1.43.0-armv7` |
|
||||
|
||||
#### 3. 镜像地址:
|
||||
|
||||
| 镜像仓库 | 最新预览版 | slim版 | armv7版 |
|
||||
| --- | --- | --- | --- |
|
||||
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` |
|
||||
| Docker Hub | `certd/certd:latest` | `certd:slim` | `certd:armv7` |
|
||||
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` |
|
||||
|
||||
> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
|
||||
> 2. 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
|
||||
> 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
|
||||
|
||||
最新版本号:
|
||||

|
||||
|
||||
@@ -36,52 +36,53 @@
|
||||
| 32.| **Dokploy授权** | |
|
||||
| 33.| **Dynadot授权** | |
|
||||
| 34.| **farcdn授权** | |
|
||||
| 35.| **FlexCDN授权** | |
|
||||
| 36.| **Gcore** | Gcore |
|
||||
| 37.| **Github授权** | |
|
||||
| 38.| **godaddy授权** | |
|
||||
| 39.| **HiPM DNSMgr** | HiPM DNSMgr API Token 授权 |
|
||||
| 40.| **金山云授权** | |
|
||||
| 41.| **FTP授权** | |
|
||||
| 42.| **七牛OSS授权** | |
|
||||
| 43.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 |
|
||||
| 44.| **s3/minio授权** | S3/minio oss授权 |
|
||||
| 45.| **namesilo授权** | |
|
||||
| 46.| **Next Terminal 授权** | 用于访问 Next Terminal API 的授权配置 |
|
||||
| 47.| **Nginx Proxy Manager 授权** | 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 |
|
||||
| 48.| **1panel授权** | 账号和密码 |
|
||||
| 49.| **支付宝** | |
|
||||
| 50.| **白山云授权** | |
|
||||
| 51.| **宝塔云WAF授权** | 用于连接和管理宝塔云WAF服务的授权配置 |
|
||||
| 52.| **cdnfly授权** | |
|
||||
| 53.| **k8s授权** | |
|
||||
| 54.| **括彩云cdn授权** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 55.| **LeCDN授权** | |
|
||||
| 56.| **lucky** | |
|
||||
| 57.| **猫云授权** | |
|
||||
| 58.| **plesk授权** | |
|
||||
| 59.| **长亭雷池授权** | |
|
||||
| 60.| **群晖登录授权** | |
|
||||
| 61.| **uniCloud** | unicloud授权 |
|
||||
| 62.| **微信支付** | |
|
||||
| 63.| **易盾rcdn授权** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 64.| **易发云短信** | sms.yfyidc.cn/ |
|
||||
| 65.| **易盾DCDN授权** | https://user.yiduncdn.com |
|
||||
| 66.| **易支付** | |
|
||||
| 67.| **proxmox** | |
|
||||
| 68.| **Spaceship.com 授权** | Spaceship.com API 授权插件 |
|
||||
| 69.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器授权 |
|
||||
| 70.| **UCloud授权** | 优刻得授权 |
|
||||
| 71.| **又拍云** | |
|
||||
| 72.| **网宿授权** | |
|
||||
| 73.| **西部数码授权** | |
|
||||
| 74.| **我爱云授权** | 我爱云CDN |
|
||||
| 75.| **新网授权(代理方式)** | |
|
||||
| 76.| **新网授权** | |
|
||||
| 77.| **新网互联授权** | 仅支持代理账号,ip需要加入白名单 |
|
||||
| 78.| **Zenlayer授权** | Zenlayer授权 |
|
||||
| 79.| **GoEdge授权** | |
|
||||
| 80.| **雨云授权** | https://app.rainyun.com/ |
|
||||
| 35.| **Fastly授权** | Fastly CDN / TLS Custom Certificates API 授权 |
|
||||
| 36.| **FlexCDN授权** | |
|
||||
| 37.| **Gcore** | Gcore |
|
||||
| 38.| **Github授权** | |
|
||||
| 39.| **godaddy授权** | |
|
||||
| 40.| **HiPM DNSMgr** | HiPM DNSMgr API Token 授权 |
|
||||
| 41.| **金山云授权** | |
|
||||
| 42.| **FTP授权** | |
|
||||
| 43.| **七牛OSS授权** | |
|
||||
| 44.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 |
|
||||
| 45.| **s3/minio授权** | S3/minio oss授权 |
|
||||
| 46.| **namesilo授权** | |
|
||||
| 47.| **Next Terminal 授权** | 用于访问 Next Terminal API 的授权配置 |
|
||||
| 48.| **Nginx Proxy Manager 授权** | 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 |
|
||||
| 49.| **1panel授权** | 账号和密码 |
|
||||
| 50.| **支付宝** | |
|
||||
| 51.| **白山云授权** | |
|
||||
| 52.| **宝塔云WAF授权** | 用于连接和管理宝塔云WAF服务的授权配置 |
|
||||
| 53.| **cdnfly授权** | |
|
||||
| 54.| **k8s授权** | |
|
||||
| 55.| **括彩云cdn授权** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 56.| **LeCDN授权** | |
|
||||
| 57.| **lucky** | |
|
||||
| 58.| **猫云授权** | |
|
||||
| 59.| **plesk授权** | |
|
||||
| 60.| **长亭雷池授权** | |
|
||||
| 61.| **群晖登录授权** | |
|
||||
| 62.| **uniCloud** | unicloud授权 |
|
||||
| 63.| **微信支付** | |
|
||||
| 64.| **易盾rcdn授权** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 65.| **易发云短信** | sms.yfyidc.cn/ |
|
||||
| 66.| **易盾DCDN授权** | https://user.yiduncdn.com |
|
||||
| 67.| **易支付** | |
|
||||
| 68.| **proxmox** | |
|
||||
| 69.| **Spaceship.com 授权** | Spaceship.com API 授权插件 |
|
||||
| 70.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器授权 |
|
||||
| 71.| **UCloud授权** | 优刻得授权 |
|
||||
| 72.| **又拍云** | |
|
||||
| 73.| **网宿授权** | |
|
||||
| 74.| **西部数码授权** | |
|
||||
| 75.| **我爱云授权** | 我爱云CDN |
|
||||
| 76.| **新网授权(代理方式)** | |
|
||||
| 77.| **新网授权** | |
|
||||
| 78.| **新网互联授权** | 仅支持代理账号,ip需要加入白名单 |
|
||||
| 79.| **Zenlayer授权** | Zenlayer授权 |
|
||||
| 80.| **GoEdge授权** | |
|
||||
| 81.| **雨云授权** | https://app.rainyun.com/ |
|
||||
|
||||
<style module>
|
||||
table th:first-of-type {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 任务插件
|
||||
共 `135` 款任务插件
|
||||
共 `139` 款任务插件
|
||||
## 1. 证书申请
|
||||
|
||||
| 序号 | 名称 | 说明 |
|
||||
@@ -31,26 +31,30 @@
|
||||
| 4.| **中国移动-部署证书到CDN** | 中国移动自动部署证书到CDN |
|
||||
| 5.| **多吉云-部署到多吉云CDN** | |
|
||||
| 6.| **farcdn-更新证书** | www.farcdn.net |
|
||||
| 7.| **FlexCDN-更新证书** | |
|
||||
| 8.| **Gcore-刷新Gcore证书** | 刷新现有的证书 |
|
||||
| 9.| **Gcore-部署证书到Gcore** | 仅上传 并不会部署到cdn |
|
||||
| 10.| **GoEdge-更新证书** | GoEdge |
|
||||
| 11.| **金山云-更新CDN证书** | 金山云自动更新CDN证书 |
|
||||
| 12.| **白山云-更新证书** | |
|
||||
| 13.| **cdnfly-部署证书到cdnfly** | cdnfly |
|
||||
| 14.| **天翼云-部署证书到CDN** | 部署证书到天翼云CDN和全站加速 |
|
||||
| 15.| **括彩云-部署到括彩云CDN** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 16.| **LeCDN-更新证书V2** | 支持新版本LeCDN |
|
||||
| 17.| **LeCDN-更新证书** | |
|
||||
| 18.| **Maoyun-更新猫云CDN证书** | |
|
||||
| 19.| **易盾-部署到易盾DCDN** | 主要是防御,http://user.yiduncdn.com/ |
|
||||
| 20.| **易盾-部署到易盾RCDN** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 21.| **雨云-更新证书** | app.rainyun.com |
|
||||
| 22.| **又拍云-部署证书到CDN/USS** | 支持又拍云CDN,又拍云云存储USS |
|
||||
| 23.| **网宿-更新证书** | 网宿证书自动更新 |
|
||||
| 24.| **西数-部署到虚拟主机** | 西部数码部署证书到虚拟主机 |
|
||||
| 25.| **我爱云-部署证书到我爱云CDN** | 部署证书到我爱云CDN |
|
||||
| 26.| **Zenlayer-刷新证书** | 刷新Zenlayer CDN证书 |
|
||||
| 7.| **Fastly-部署TLS激活** | 部署 Fastly 证书 (创建 TLS Activation 绑定证书到域名) |
|
||||
| 8.| **Fastly-清除缓存** | 清除 Fastly 指定 Service 的所有缓存 (Purge All) |
|
||||
| 9.| **Fastly-更新证书** | 自动更新 Fastly CDN 上的已有自定义证书 |
|
||||
| 10.| **Fastly-上传证书到Fastly** | 部署 / 上传 SSL/TLS 自定义证书到 Fastly CDN |
|
||||
| 11.| **FlexCDN-更新证书** | |
|
||||
| 12.| **Gcore-刷新Gcore证书** | 刷新现有的证书 |
|
||||
| 13.| **Gcore-部署证书到Gcore** | 仅上传 并不会部署到cdn |
|
||||
| 14.| **GoEdge-更新证书** | GoEdge |
|
||||
| 15.| **金山云-更新CDN证书** | 金山云自动更新CDN证书 |
|
||||
| 16.| **白山云-更新证书** | |
|
||||
| 17.| **cdnfly-部署证书到cdnfly** | cdnfly |
|
||||
| 18.| **天翼云-部署证书到CDN** | 部署证书到天翼云CDN和全站加速 |
|
||||
| 19.| **括彩云-部署到括彩云CDN** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 20.| **LeCDN-更新证书V2** | 支持新版本LeCDN |
|
||||
| 21.| **LeCDN-更新证书** | |
|
||||
| 22.| **Maoyun-更新猫云CDN证书** | |
|
||||
| 23.| **易盾-部署到易盾DCDN** | 主要是防御,http://user.yiduncdn.com/ |
|
||||
| 24.| **易盾-部署到易盾RCDN** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 25.| **雨云-更新证书** | app.rainyun.com |
|
||||
| 26.| **又拍云-部署证书到CDN/USS** | 支持又拍云CDN,又拍云云存储USS |
|
||||
| 27.| **网宿-更新证书** | 网宿证书自动更新 |
|
||||
| 28.| **西数-部署到虚拟主机** | 西部数码部署证书到虚拟主机 |
|
||||
| 29.| **我爱云-部署证书到我爱云CDN** | 部署证书到我爱云CDN |
|
||||
| 30.| **Zenlayer-刷新证书** | 刷新Zenlayer CDN证书 |
|
||||
## 4. 面板
|
||||
|
||||
| 序号 | 名称 | 说明 |
|
||||
|
||||
+1
-1
@@ -9,5 +9,5 @@
|
||||
}
|
||||
},
|
||||
"npmClient": "pnpm",
|
||||
"version": "1.44.2"
|
||||
"version": "1.44.3"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/publishlab/node-acme-client/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.44.2](https://github.com/publishlab/node-acme-client/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**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.44.2",
|
||||
"version": "1.44.3",
|
||||
"type": "module",
|
||||
"module": "./dist/index.js",
|
||||
"main": "./dist/index.js",
|
||||
@@ -18,7 +18,7 @@
|
||||
"types"
|
||||
],
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.44.2",
|
||||
"@certd/basic": "^1.44.3",
|
||||
"@peculiar/x509": "^1.11.0",
|
||||
"asn1js": "^3.0.5",
|
||||
"axios": "^1.9.0",
|
||||
@@ -76,5 +76,5 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/publishlab/node-acme-client/issues"
|
||||
},
|
||||
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
@@ -1 +1 @@
|
||||
21:57
|
||||
23:46
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/basic",
|
||||
"private": false,
|
||||
"version": "1.44.2",
|
||||
"version": "1.44.3",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -54,5 +54,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/pipeline",
|
||||
"private": false,
|
||||
"version": "1.44.2",
|
||||
"version": "1.44.3",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -21,8 +21,8 @@
|
||||
"lint": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.44.2",
|
||||
"@certd/plus-core": "^1.44.2",
|
||||
"@certd/basic": "^1.44.3",
|
||||
"@certd/plus-core": "^1.44.3",
|
||||
"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": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-huawei",
|
||||
"private": false,
|
||||
"version": "1.44.2",
|
||||
"version": "1.44.3",
|
||||
"main": "./dist/bundle.js",
|
||||
"module": "./dist/bundle.js",
|
||||
"types": "./dist/d/index.d.ts",
|
||||
@@ -31,5 +31,5 @@
|
||||
"prettier": "3.3.3",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-iframe",
|
||||
"private": false,
|
||||
"version": "1.44.2",
|
||||
"version": "1.44.3",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -37,5 +37,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/jdcloud",
|
||||
"version": "1.44.2",
|
||||
"version": "1.44.3",
|
||||
"description": "jdcloud openApi sdk",
|
||||
"main": "./dist/bundle.js",
|
||||
"module": "./dist/bundle.js",
|
||||
@@ -63,5 +63,5 @@
|
||||
"fetch"
|
||||
]
|
||||
},
|
||||
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-k8s",
|
||||
"private": false,
|
||||
"version": "1.44.2",
|
||||
"version": "1.44.3",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -21,7 +21,7 @@
|
||||
"lint": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.44.2",
|
||||
"@certd/basic": "^1.44.3",
|
||||
"@kubernetes/client-node": "0.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -38,5 +38,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-server
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/lib-server",
|
||||
"version": "1.44.2",
|
||||
"version": "1.44.3",
|
||||
"description": "midway with flyway, sql upgrade way ",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
@@ -29,11 +29,11 @@
|
||||
],
|
||||
"license": "AGPL",
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^1.44.2",
|
||||
"@certd/basic": "^1.44.2",
|
||||
"@certd/pipeline": "^1.44.2",
|
||||
"@certd/plugin-lib": "^1.44.2",
|
||||
"@certd/plus-core": "^1.44.2",
|
||||
"@certd/acme-client": "^1.44.3",
|
||||
"@certd/basic": "^1.44.3",
|
||||
"@certd/pipeline": "^1.44.3",
|
||||
"@certd/plugin-lib": "^1.44.3",
|
||||
"@certd/plus-core": "^1.44.3",
|
||||
"@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": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/midway-flyway-js",
|
||||
"version": "1.44.2",
|
||||
"version": "1.44.3",
|
||||
"description": "midway with flyway, sql upgrade way ",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
@@ -52,5 +52,5 @@
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/plugin-cert",
|
||||
"private": false,
|
||||
"version": "1.44.2",
|
||||
"version": "1.44.3",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -20,7 +20,7 @@
|
||||
"lint": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/plugin-lib": "^1.44.2"
|
||||
"@certd/plugin-lib": "^1.44.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.12",
|
||||
@@ -38,5 +38,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-lib
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-lib
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/plugin-lib",
|
||||
"private": false,
|
||||
"version": "1.44.2",
|
||||
"version": "1.44.3",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -17,9 +17,9 @@
|
||||
"lint": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^1.44.2",
|
||||
"@certd/basic": "^1.44.2",
|
||||
"@certd/pipeline": "^1.44.2",
|
||||
"@certd/acme-client": "^1.44.3",
|
||||
"@certd/basic": "^1.44.3",
|
||||
"@certd/pipeline": "^1.44.3",
|
||||
"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": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **plugin:** 修复插件配置保存与加载的相关问题 ([bd94334](https://github.com/certd/certd/commit/bd9433478a43d5d64ce6370386d0fe0aaff07d2e))
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/ui-client",
|
||||
"version": "1.44.2",
|
||||
"version": "1.44.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --open",
|
||||
@@ -104,8 +104,8 @@
|
||||
"zod-defaults": "^0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/lib-iframe": "^1.44.2",
|
||||
"@certd/pipeline": "^1.44.2",
|
||||
"@certd/lib-iframe": "^1.44.3",
|
||||
"@certd/pipeline": "^1.44.3",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@types/chai": "^4.3.12",
|
||||
|
||||
@@ -113,7 +113,8 @@ const onFinishFailed = (errorInfo: any) => {
|
||||
const { openConfigDialog } = usePluginConfig();
|
||||
|
||||
async function doPluginConfig() {
|
||||
const certApplyInfo = await getPluginSetting("CertApply");
|
||||
const certApplyInfo: any = await getPluginSetting("CertApply");
|
||||
certApplyInfo.pluginType = "plugin";
|
||||
await openConfigDialog({
|
||||
row: certApplyInfo,
|
||||
onSuccess: async () => {
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<script setup lang="tsx">
|
||||
import { dict, FsRender } from "@fast-crud/fast-crud";
|
||||
import { cloneDeep, merge, unset } from "lodash-es";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { computed, onMounted, provide, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import Rollbackable from "./rollbackable.vue";
|
||||
import { fillPluginDefine, usePluginStore } from "/@/store/plugin";
|
||||
@@ -68,7 +68,7 @@ const labelCol = ref({
|
||||
},
|
||||
});
|
||||
const wrapperCol = ref({ span: 16 });
|
||||
const configForm: any = reactive({});
|
||||
const configForm: any = reactive({ type: props.plugin.fullName || props.plugin.name });
|
||||
|
||||
const showDict = dict({
|
||||
data: [
|
||||
@@ -185,9 +185,11 @@ const optionsMappingKey = {
|
||||
},
|
||||
editRender(item: any) {
|
||||
return () => {
|
||||
const options = item.component?.options || [];
|
||||
if (options.length === 0) {
|
||||
return <span class="text-gray-400">该组件没有预设选项</span>;
|
||||
const options = reactive(cloneDeep(item.component?.options || []));
|
||||
|
||||
const showAdd = options.length === 0;
|
||||
function addOption(opt: any) {
|
||||
options.push(opt);
|
||||
}
|
||||
|
||||
const onLabelChange = (optValue: string, newLabel: string) => {
|
||||
@@ -233,6 +235,13 @@ const optionsMappingKey = {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{showAdd && (
|
||||
<div class="mt-2">
|
||||
<a-button type="primary" size={() => addOption({ value: "", label: "" })}>
|
||||
添加选项
|
||||
</a-button>
|
||||
</div>
|
||||
)}
|
||||
<div class="helper mt-1">只需填写需要自定义的选项,留空则使用原始显示内容</div>
|
||||
</div>
|
||||
);
|
||||
@@ -289,6 +298,17 @@ onMounted(async () => {
|
||||
defineExpose({
|
||||
getForm,
|
||||
});
|
||||
|
||||
provide("getCurrentPluginDefine", () => {
|
||||
return { value: props.plugin };
|
||||
});
|
||||
provide("get:scope", () => {
|
||||
return getScope();
|
||||
});
|
||||
provide("get:plugin:type", () => {
|
||||
debugger;
|
||||
return props.plugin.pluginType;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
||||
@@ -49,7 +49,7 @@ export function usePluginConfig() {
|
||||
}
|
||||
}
|
||||
const res = await api.savePluginSetting({
|
||||
name: row.fullName,
|
||||
name: row.fullName || row.name,
|
||||
sysSetting: {
|
||||
metadata: {
|
||||
input: newForm,
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **plugin-service:** 修复同步自己开发的云端插件不显示已安装状态问题 ([dac94b6](https://github.com/certd/certd/commit/dac94b64f0ceb7176e2cf07ce220be0aa5ed98d2))
|
||||
* **plugin:** 修复插件配置保存与加载的相关问题 ([bd94334](https://github.com/certd/certd/commit/bd9433478a43d5d64ce6370386d0fe0aaff07d2e))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **plugin:** add Fastly custom TLS certificates integration ([#797](https://github.com/certd/certd/issues/797)) [@szhu25](https://github.com/szhu25) ([2a2e963](https://github.com/certd/certd/commit/2a2e963914c4dfc192cbe0a4bbb9c115e626da01))
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: fastly
|
||||
title: Fastly授权
|
||||
icon: simple-icons:fastly
|
||||
desc: Fastly CDN / TLS Custom Certificates API 授权
|
||||
input:
|
||||
apiKey:
|
||||
title: API Token
|
||||
component:
|
||||
placeholder: Fastly API Key / Token
|
||||
helper: 前往 Fastly Account Settings -> Personal Access Tokens 创建 Token
|
||||
required: true
|
||||
encrypt: true
|
||||
proxy:
|
||||
title: HTTP代理
|
||||
component:
|
||||
placeholder: http://xxxx.xxx.xx:10811
|
||||
helper: 可选:是否使用 HTTP 代理访问 Fastly API
|
||||
required: false
|
||||
encrypt: false
|
||||
testRequest:
|
||||
title: 测试
|
||||
component:
|
||||
name: api-test
|
||||
action: TestRequest
|
||||
helper: 测试授权是否正确
|
||||
pluginType: access
|
||||
type: builtIn
|
||||
scriptFilePath: /plugins/plugin-fastly/access.js
|
||||
@@ -150,6 +150,15 @@ input:
|
||||
|
||||
helper: 请选择distributions id
|
||||
order: 0
|
||||
waitForDeployed:
|
||||
title: 等待部署完成
|
||||
value: false
|
||||
helper: >-
|
||||
开启后,会轮询等待每个CloudFront分配状态变为Deployed(通常需要几分钟)再结束任务。关闭时,提交证书更新后立即结束——CloudFront会在后台自行完成部署。仅当后续任务依赖证书已全球生效时才需要开启。
|
||||
component:
|
||||
name: a-switch
|
||||
vModel: checked
|
||||
order: 0
|
||||
output: {}
|
||||
pluginType: deploy
|
||||
type: builtIn
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
showRunStrategy: false
|
||||
default:
|
||||
strategy:
|
||||
runStrategy: 1
|
||||
name: FastlyDeployCert
|
||||
title: Fastly-部署TLS激活
|
||||
desc: 部署 Fastly 证书 (创建 TLS Activation 绑定证书到域名)
|
||||
icon: simple-icons:fastly
|
||||
group: cdn
|
||||
input:
|
||||
cert:
|
||||
title: 域名证书
|
||||
helper: 选择前置任务输出的域名证书(将自动上传到 Fastly),或选择前置【Fastly-上传证书到Fastly】任务输出的 Fastly 证书ID。
|
||||
component:
|
||||
name: output-selector
|
||||
from:
|
||||
- ':cert:'
|
||||
- FastlyUploadCert
|
||||
required: true
|
||||
order: 0
|
||||
name:
|
||||
title: 证书名称
|
||||
helper: 可选。当传入的是域名证书需要先上传时,作为 Fastly 上的自定义证书名称/标签
|
||||
component:
|
||||
placeholder: '例如: my-fastly-cert'
|
||||
required: false
|
||||
order: 0
|
||||
accessId:
|
||||
title: Access授权
|
||||
helper: Fastly 授权凭证
|
||||
component:
|
||||
name: access-selector
|
||||
type: fastly
|
||||
required: true
|
||||
order: 0
|
||||
domainIds:
|
||||
title: TLS 域名
|
||||
component:
|
||||
name: remote-select
|
||||
vModel: value
|
||||
mode: tags
|
||||
type: plugin
|
||||
action: onGetTlsDomainList
|
||||
search: false
|
||||
pager: false
|
||||
single: false
|
||||
watches:
|
||||
- certDomains
|
||||
- accessId
|
||||
required: true
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
component:{
|
||||
form: ctx.compute(({form})=>{
|
||||
return form
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
helper: >-
|
||||
选择要绑定证书的域名,可多选(将为每个域名创建或更新一个 TLS 激活)。列表为你 Fastly 服务上配置的域名,请选择被证书覆盖的具体主机名(如
|
||||
www.example.com),而非通配 *.example.com(除非确有服务使用通配域名)。列表没有的也可直接输入。
|
||||
order: 0
|
||||
configurationId:
|
||||
title: TLS 配置
|
||||
component:
|
||||
name: remote-select
|
||||
vModel: value
|
||||
mode: tags
|
||||
type: plugin
|
||||
action: onGetTlsConfigurationList
|
||||
search: false
|
||||
pager: false
|
||||
single: true
|
||||
watches:
|
||||
- certDomains
|
||||
- accessId
|
||||
required: true
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
component:{
|
||||
form: ctx.compute(({form})=>{
|
||||
return form
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
helper: 选择关联的 TLS 配置
|
||||
order: 0
|
||||
output: {}
|
||||
pluginType: deploy
|
||||
type: builtIn
|
||||
scriptFilePath: /plugins/plugin-fastly/plugins/plugin-deploy-to-service.js
|
||||
@@ -0,0 +1,49 @@
|
||||
showRunStrategy: false
|
||||
default:
|
||||
strategy:
|
||||
runStrategy: 1
|
||||
name: FastlyPurgeCache
|
||||
title: Fastly-清除缓存
|
||||
desc: 清除 Fastly 指定 Service 的所有缓存 (Purge All)
|
||||
icon: simple-icons:fastly
|
||||
group: cdn
|
||||
input:
|
||||
accessId:
|
||||
title: Access授权
|
||||
helper: Fastly 授权凭证
|
||||
component:
|
||||
name: access-selector
|
||||
type: fastly
|
||||
required: true
|
||||
order: 0
|
||||
serviceId:
|
||||
title: Service (服务)
|
||||
component:
|
||||
name: remote-select
|
||||
vModel: value
|
||||
mode: tags
|
||||
type: plugin
|
||||
action: onGetServiceList
|
||||
search: false
|
||||
pager: false
|
||||
single: false
|
||||
watches:
|
||||
- certDomains
|
||||
- accessId
|
||||
required: true
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
component:{
|
||||
form: ctx.compute(({form})=>{
|
||||
return form
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
helper: 选择要清理所有缓存的 Fastly 服务
|
||||
order: 0
|
||||
output: {}
|
||||
pluginType: deploy
|
||||
type: builtIn
|
||||
scriptFilePath: /plugins/plugin-fastly/plugins/plugin-purge-cache.js
|
||||
@@ -0,0 +1,60 @@
|
||||
showRunStrategy: false
|
||||
default:
|
||||
strategy:
|
||||
runStrategy: 1
|
||||
name: FastlyRefreshCert
|
||||
title: Fastly-更新证书
|
||||
desc: 自动更新 Fastly CDN 上的已有自定义证书
|
||||
icon: simple-icons:fastly
|
||||
group: cdn
|
||||
input:
|
||||
cert:
|
||||
title: 域名证书
|
||||
helper: 请选择前置任务输出的域名证书
|
||||
component:
|
||||
name: output-selector
|
||||
from:
|
||||
- ':cert:'
|
||||
required: true
|
||||
order: 0
|
||||
accessId:
|
||||
title: Access授权
|
||||
helper: Fastly 授权凭证
|
||||
component:
|
||||
name: access-selector
|
||||
type: fastly
|
||||
required: true
|
||||
order: 0
|
||||
certList:
|
||||
title: 证书列表
|
||||
component:
|
||||
name: remote-select
|
||||
vModel: value
|
||||
mode: tags
|
||||
type: plugin
|
||||
action: onGetCertList
|
||||
search: false
|
||||
pager: false
|
||||
single: false
|
||||
watches:
|
||||
- certDomains
|
||||
- accessId
|
||||
required: true
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
component:{
|
||||
form: ctx.compute(({form})=>{
|
||||
return form
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
helper: >-
|
||||
选择要更新的 Fastly 证书 (必须是已存在的自定义证书)。注意:Fastly
|
||||
要求更新后的证书与原证书使用相同私钥,请确保续期时复用私钥(reuse key),否则更新会失败。
|
||||
order: 0
|
||||
output: {}
|
||||
pluginType: deploy
|
||||
type: builtIn
|
||||
scriptFilePath: /plugins/plugin-fastly/plugins/plugin-refresh-cert.js
|
||||
@@ -0,0 +1,49 @@
|
||||
showRunStrategy: false
|
||||
default:
|
||||
strategy:
|
||||
runStrategy: 1
|
||||
name: FastlyUploadCert
|
||||
title: Fastly-上传证书到Fastly
|
||||
desc: 部署 / 上传 SSL/TLS 自定义证书到 Fastly CDN
|
||||
icon: simple-icons:fastly
|
||||
group: cdn
|
||||
input:
|
||||
cert:
|
||||
title: 域名证书
|
||||
helper: 请选择前置任务输出的域名证书
|
||||
component:
|
||||
name: output-selector
|
||||
from:
|
||||
- ':cert:'
|
||||
required: true
|
||||
order: 0
|
||||
accessId:
|
||||
title: Access授权
|
||||
helper: Fastly 授权凭证
|
||||
component:
|
||||
name: access-selector
|
||||
type: fastly
|
||||
required: true
|
||||
order: 0
|
||||
certificateId:
|
||||
title: 证书ID (tls_certificate_id)
|
||||
helper: >-
|
||||
可选。若填写则对 Fastly 已有证书进行更新(PATCH);留空则新建上传(POST)。注意:Fastly
|
||||
更新证书要求新证书与原证书使用相同的私钥,请确保续期时复用私钥(reuse key),否则更新会失败。
|
||||
component:
|
||||
placeholder: '例如: tls_cert_xxx (留空表示新建)'
|
||||
required: false
|
||||
order: 0
|
||||
name:
|
||||
title: 证书名称
|
||||
helper: 可选。上传到 Fastly 上的自定义证书名称/标签
|
||||
component:
|
||||
placeholder: '例如: my-fastly-cert'
|
||||
required: false
|
||||
order: 0
|
||||
output:
|
||||
fastlyCertId:
|
||||
title: Fastly 证书 ID
|
||||
pluginType: deploy
|
||||
type: builtIn
|
||||
scriptFilePath: /plugins/plugin-fastly/plugins/plugin-upload-cert.js
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/ui-server",
|
||||
"version": "1.44.2",
|
||||
"version": "1.44.3",
|
||||
"description": "fast-server base midway",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -43,20 +43,20 @@
|
||||
"lint1": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^1.44.2",
|
||||
"@certd/basic": "^1.44.2",
|
||||
"@certd/commercial-core": "^1.44.2",
|
||||
"@certd/acme-client": "^1.44.3",
|
||||
"@certd/basic": "^1.44.3",
|
||||
"@certd/commercial-core": "^1.44.3",
|
||||
"@certd/cv4pve-api-javascript": "^8.4.2",
|
||||
"@certd/jdcloud": "^1.44.2",
|
||||
"@certd/lib-huawei": "^1.44.2",
|
||||
"@certd/lib-k8s": "^1.44.2",
|
||||
"@certd/lib-server": "^1.44.2",
|
||||
"@certd/midway-flyway-js": "^1.44.2",
|
||||
"@certd/pipeline": "^1.44.2",
|
||||
"@certd/plugin-cert": "^1.44.2",
|
||||
"@certd/plugin-lib": "^1.44.2",
|
||||
"@certd/plugin-plus": "^1.44.2",
|
||||
"@certd/plus-core": "^1.44.2",
|
||||
"@certd/jdcloud": "^1.44.3",
|
||||
"@certd/lib-huawei": "^1.44.3",
|
||||
"@certd/lib-k8s": "^1.44.3",
|
||||
"@certd/lib-server": "^1.44.3",
|
||||
"@certd/midway-flyway-js": "^1.44.3",
|
||||
"@certd/pipeline": "^1.44.3",
|
||||
"@certd/plugin-cert": "^1.44.3",
|
||||
"@certd/plugin-lib": "^1.44.3",
|
||||
"@certd/plugin-plus": "^1.44.3",
|
||||
"@certd/plus-core": "^1.44.3",
|
||||
"@koa/cors": "^5.0.0",
|
||||
"@midwayjs/bootstrap": "3.20.11",
|
||||
"@midwayjs/cache": "3.14.0",
|
||||
|
||||
@@ -6,6 +6,7 @@ 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";
|
||||
import { PluginConfigService } from "../plugin/service/plugin-config-service.js";
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
@@ -13,6 +14,9 @@ export class AutoLoadPlugins {
|
||||
@Inject()
|
||||
pluginService: PluginService;
|
||||
|
||||
@Inject()
|
||||
pluginConfigService: PluginConfigService;
|
||||
|
||||
@Config("runtimeDeps")
|
||||
runtimeDepsConfig: any;
|
||||
|
||||
@@ -34,8 +38,8 @@ export class AutoLoadPlugins {
|
||||
}
|
||||
// await import("../../plugins/index.js")
|
||||
await this.pluginService.registerFromDb();
|
||||
|
||||
await registerPaymentProviders();
|
||||
await this.pluginConfigService.loadAllPluginSetting();
|
||||
registerPaymentProviders();
|
||||
logger.info(`加载插件完成,加载模式:${process.env.certd_plugin_loadmode}`);
|
||||
|
||||
//初始化第三方依赖服务
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { PluginService } from "./plugin-service.js";
|
||||
import { accessRegistry, notificationRegistry, pluginRegistry } from "@certd/pipeline";
|
||||
import { dnsProviderRegistry } from "@certd/plugin-lib";
|
||||
import { addonRegistry } from "@certd/lib-server";
|
||||
|
||||
export type PluginConfig = {
|
||||
name: string;
|
||||
@@ -66,15 +69,54 @@ export class PluginConfigService {
|
||||
};
|
||||
const { id } = await this.pluginService.add(pluginEntity);
|
||||
pluginEntity.id = id;
|
||||
this.loadPluginSetting(name, sysSetting);
|
||||
} else {
|
||||
const setting = JSON.parse(pluginEntity.sysSetting || "{}");
|
||||
if (sysSetting.metadata) {
|
||||
setting.metadata = sysSetting.metadata;
|
||||
}
|
||||
if (sysSetting.input) {
|
||||
//如果没有新提交,不覆盖旧的input
|
||||
setting.input = sysSetting.input;
|
||||
}
|
||||
await this.pluginService.getRepository().update({ fullName: name }, { sysSetting: JSON.stringify(setting) });
|
||||
this.loadPluginSetting(name, setting);
|
||||
}
|
||||
}
|
||||
|
||||
async loadPluginSetting(name: string, sysSetting: any) {
|
||||
let pluginDefine = null;
|
||||
if (!pluginDefine) {
|
||||
pluginDefine = accessRegistry.getDefine(name);
|
||||
}
|
||||
if (!pluginDefine) {
|
||||
pluginDefine = pluginRegistry.getDefine(name);
|
||||
}
|
||||
if (!pluginDefine) {
|
||||
pluginDefine = dnsProviderRegistry.getDefine(name);
|
||||
}
|
||||
if (!pluginDefine) {
|
||||
pluginDefine = addonRegistry.getDefine(name);
|
||||
}
|
||||
if (!pluginDefine) {
|
||||
pluginDefine = notificationRegistry.getDefine(name);
|
||||
}
|
||||
if (!pluginDefine) {
|
||||
return;
|
||||
}
|
||||
pluginDefine.sysSetting = sysSetting;
|
||||
}
|
||||
|
||||
async loadAllPluginSetting() {
|
||||
const pluginSettings = await this.pluginService.getRepository().find({
|
||||
select: {
|
||||
fullName: true,
|
||||
type: true,
|
||||
sysSetting: true,
|
||||
},
|
||||
});
|
||||
for (const plugin of pluginSettings) {
|
||||
this.loadPluginSetting(plugin.fullName, JSON.parse(plugin.sysSetting || "{}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1004,6 +1004,13 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
score: itemFromStore.score,
|
||||
downloadCount: itemFromStore.downloadCount,
|
||||
latestVersion: itemFromStore.version,
|
||||
developerId: itemFromStore.developerId,
|
||||
vip: itemFromStore.vip,
|
||||
dependPlugins: itemFromStore.dependPlugins,
|
||||
aiCheckStatus: itemFromStore.aiCheckStatus,
|
||||
status: itemFromStore.status,
|
||||
appId: itemFromStore.appId,
|
||||
author: itemFromStore.author,
|
||||
};
|
||||
await this.updateById(updateBean);
|
||||
} else {
|
||||
|
||||
@@ -50,3 +50,4 @@
|
||||
// export * from './plugin-nginx-proxy-manager/index.js'
|
||||
// export * from './plugin-hipmdnsmgr/index.js'
|
||||
// export * from './plugin-dynadot/index.js'
|
||||
// export * from './plugin-fastly/index.js';
|
||||
|
||||
@@ -35,7 +35,7 @@ async function createAwsClient(records: any[]) {
|
||||
ChangeResourceRecordSetsCommand,
|
||||
}),
|
||||
} as any,
|
||||
logger: { info() {}, error() {} } as any,
|
||||
logger: { info() {}, warn() {}, error() {} } as any,
|
||||
region: "us-east-1",
|
||||
});
|
||||
return { client, sentChanges };
|
||||
@@ -62,10 +62,7 @@ describe("AwsClient.route53ChangeRecord", () => {
|
||||
|
||||
const change = sentChanges[0].ChangeBatch.Changes[0];
|
||||
assert.equal(change.Action, "UPSERT");
|
||||
assert.deepEqual(change.ResourceRecordSet.ResourceRecords, [
|
||||
{ Value: '"apex-token"' },
|
||||
{ Value: '"wildcard-token"' },
|
||||
]);
|
||||
assert.deepEqual(change.ResourceRecordSet.ResourceRecords, [{ Value: '"apex-token"' }, { Value: '"wildcard-token"' }]);
|
||||
assert.equal(change.ResourceRecordSet.TTL, 300);
|
||||
});
|
||||
|
||||
@@ -92,3 +89,124 @@ describe("AwsClient.route53ChangeRecord", () => {
|
||||
assert.deepEqual(change.ResourceRecordSet.ResourceRecords, [{ Value: '"wildcard-token"' }]);
|
||||
});
|
||||
});
|
||||
|
||||
function throttle(name: string, extra: Record<string, any> = {}) {
|
||||
const err: any = new Error(name);
|
||||
err.name = name;
|
||||
return Object.assign(err, extra);
|
||||
}
|
||||
|
||||
describe("AwsClient.withRetry", () => {
|
||||
it("retries a throttled call and then resolves", async () => {
|
||||
const { client } = await createAwsClient([]);
|
||||
let calls = 0;
|
||||
const result = await client.withRetry(async () => {
|
||||
calls++;
|
||||
if (calls < 3) {
|
||||
throw throttle("ThrottlingException");
|
||||
}
|
||||
return "ok";
|
||||
});
|
||||
assert.equal(result, "ok");
|
||||
assert.equal(calls, 3);
|
||||
});
|
||||
|
||||
it("retries Route53 PriorRequestNotComplete", async () => {
|
||||
const { client } = await createAwsClient([]);
|
||||
let calls = 0;
|
||||
const result = await client.withRetry(async () => {
|
||||
calls++;
|
||||
if (calls < 2) {
|
||||
throw throttle("PriorRequestNotComplete");
|
||||
}
|
||||
return "done";
|
||||
});
|
||||
assert.equal(result, "done");
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
it("retries on HTTP 429 without a known error code", async () => {
|
||||
const { client } = await createAwsClient([]);
|
||||
let calls = 0;
|
||||
await client.withRetry(async () => {
|
||||
calls++;
|
||||
if (calls < 2) {
|
||||
const err: any = new Error("too many requests");
|
||||
err.$metadata = { httpStatusCode: 429 };
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
it("retries transient HTTP 5xx", async () => {
|
||||
const { client } = await createAwsClient([]);
|
||||
let calls = 0;
|
||||
await client.withRetry(async () => {
|
||||
calls++;
|
||||
if (calls < 2) {
|
||||
const err: any = new Error("service unavailable");
|
||||
err.$metadata = { httpStatusCode: 503 };
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
it("retries when the SDK marks the error $retryable", async () => {
|
||||
const { client } = await createAwsClient([]);
|
||||
let calls = 0;
|
||||
await client.withRetry(async () => {
|
||||
calls++;
|
||||
if (calls < 2) {
|
||||
const err: any = new Error("internal failure");
|
||||
err.name = "InternalFailure";
|
||||
err.$retryable = {};
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
it("retries connection-level errors", async () => {
|
||||
const { client } = await createAwsClient([]);
|
||||
let calls = 0;
|
||||
await client.withRetry(async () => {
|
||||
calls++;
|
||||
if (calls < 2) {
|
||||
throw throttle("Error", { code: "ECONNRESET" });
|
||||
}
|
||||
return null;
|
||||
});
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
it("does not retry non-throttling errors", async () => {
|
||||
const { client } = await createAwsClient([]);
|
||||
let calls = 0;
|
||||
await assert.rejects(
|
||||
client.withRetry(async () => {
|
||||
calls++;
|
||||
throw throttle("AccessDeniedException");
|
||||
}),
|
||||
/AccessDeniedException/
|
||||
);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
|
||||
it("stops after maxAttempts and rethrows the throttling error", async () => {
|
||||
const { client } = await createAwsClient([]);
|
||||
let calls = 0;
|
||||
await assert.rejects(
|
||||
client.withRetry(async () => {
|
||||
calls++;
|
||||
throw throttle("Throttling", { message: "Rate exceeded" });
|
||||
}, 3),
|
||||
/Throttling|Rate exceeded/
|
||||
);
|
||||
assert.equal(calls, 3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,8 @@ export class AwsClient {
|
||||
accessKeyId: this.access.accessKeyId, // 从环境变量中读取
|
||||
secretAccessKey: this.access.secretAccessKey,
|
||||
},
|
||||
// Disable the SDK's own retries; withRetry/doRequest is the single retry authority.
|
||||
maxAttempts: 1,
|
||||
});
|
||||
|
||||
// Split the full PEM chain: first block is the leaf cert, the rest is the intermediate chain
|
||||
@@ -57,6 +59,7 @@ export class AwsClient {
|
||||
accessKeyId: this.access.accessKeyId, // 从环境变量中读取
|
||||
secretAccessKey: this.access.secretAccessKey,
|
||||
},
|
||||
maxAttempts: 1,
|
||||
});
|
||||
|
||||
const command = new GetCallerIdentityCommand({});
|
||||
@@ -75,6 +78,7 @@ export class AwsClient {
|
||||
accessKeyId: this.access.accessKeyId, // 从环境变量中读取
|
||||
secretAccessKey: this.access.secretAccessKey,
|
||||
},
|
||||
maxAttempts: 1,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -202,10 +206,107 @@ export class AwsClient {
|
||||
|
||||
async doRequest<T>(call: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await call();
|
||||
} catch (err) {
|
||||
this.logger.error(`调用接口失败:${err.Error?.Message || err.message},requestId:${err.requestId}`);
|
||||
return await this.withRetry(call);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`请求失败:${err.Error?.Message || err.message},requestId:${err.requestId}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries an AWS SDK call with exponential backoff + jitter while the error is
|
||||
* retryable. See {@link AwsClient.isRetryableAwsError}.
|
||||
*
|
||||
* `T` defaults to `any` so that call sites passing an untyped SDK client
|
||||
* (`@aws-sdk/*` is a lazyDependency and has no types at build time) don't
|
||||
* collapse the result to `unknown`.
|
||||
*/
|
||||
async withRetry<T = any>(call: () => Promise<T>, maxAttempts = 5, baseDelayMs = 2000): Promise<T> {
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await call();
|
||||
} catch (err: any) {
|
||||
if (attempt >= maxAttempts || !AwsClient.isRetryableAwsError(err)) {
|
||||
throw err;
|
||||
}
|
||||
const code = err?.name || err?.Code || err?.code || err?.$metadata?.httpStatusCode || "unknown";
|
||||
const ceiling = baseDelayMs * Math.pow(2, attempt - 1); // 2s, 4s, 8s, 16s …
|
||||
const delay = Math.round(ceiling / 2 + Math.random() * (ceiling / 2)); // equal jitter: ~1-2s, ~2-4s, ~4-8s, ~8-16s
|
||||
this.logger.warn(`AWS request failed (${code}), attempt ${attempt}/${maxAttempts}, retrying in ${delay}ms…`);
|
||||
await utils.sleep(delay);
|
||||
}
|
||||
}
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects AWS errors that are safe to retry. Because we disable the SDK's own
|
||||
* retries (maxAttempts: 1 on every client), this is the single source of truth
|
||||
* and covers what the SDK's `standard` strategy would have retried:
|
||||
* - throttling / rate-limit codes across ACM / STS / CloudFront / Route53,
|
||||
* including Route53's PriorRequestNotComplete concurrent-change limit;
|
||||
* - SDK v3 retry hints (`$retryable`);
|
||||
* - transient HTTP 429 and 5xx;
|
||||
* - connection-level errors (ECONNRESET, ETIMEDOUT, EAI_AGAIN, …).
|
||||
*/
|
||||
static isRetryableAwsError(err: any): boolean {
|
||||
if (!err) {
|
||||
return false;
|
||||
}
|
||||
const retryableCodes = new Set([
|
||||
"TooManyRequestsException",
|
||||
"ThrottlingException",
|
||||
"ThrottledException",
|
||||
"RequestLimitExceeded",
|
||||
"RequestThrottled",
|
||||
"Throttling",
|
||||
"SlowDown",
|
||||
"PriorRequestNotComplete",
|
||||
"RequestTimeout",
|
||||
"RequestTimeoutException",
|
||||
"TimeoutError",
|
||||
]);
|
||||
const connectionCodes = new Set(["ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "EAI_AGAIN", "ENOTFOUND", "EHOSTUNREACH", "ENETUNREACH"]);
|
||||
const code = err.name || err.Code || err.code || "";
|
||||
const httpStatus = err.$metadata?.httpStatusCode;
|
||||
return (
|
||||
retryableCodes.has(code) ||
|
||||
connectionCodes.has(err.code) ||
|
||||
err.$retryable != null ||
|
||||
httpStatus === 429 ||
|
||||
(typeof httpStatus === "number" && httpStatus >= 500) ||
|
||||
(typeof err.message === "string" && err.message.toLowerCase().includes("rate exceeded"))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls a CloudFront distribution until its Status becomes "Deployed".
|
||||
* CloudFront propagates changes globally and can take several minutes.
|
||||
*/
|
||||
async waitForDistributionDeployed(cloudFrontClient: any, distributionId: string, timeoutMs = 600_000, pollIntervalMs = 15_000): Promise<void> {
|
||||
const { GetDistributionCommand } = await this.access.importRuntime("@aws-sdk/client-cloudfront");
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
let status: string | undefined;
|
||||
try {
|
||||
const res: any = await this.withRetry(() => cloudFrontClient.send(new GetDistributionCommand({ Id: distributionId })));
|
||||
status = res?.Distribution?.Status;
|
||||
} catch (err: any) {
|
||||
// A retryable error that outlives withRetry's budget shouldn't abort the whole deploy;
|
||||
// keep polling until the deadline and let genuine (non-retryable) errors propagate.
|
||||
if (!AwsClient.isRetryableAwsError(err)) {
|
||||
throw err;
|
||||
}
|
||||
this.logger.warn(`CloudFront status check failed (${err?.name || err?.code}), will retry on next poll…`);
|
||||
await utils.sleep(pollIntervalMs);
|
||||
continue;
|
||||
}
|
||||
this.logger.info(`CloudFront distribution ${distributionId} status: ${status}`);
|
||||
if (status === "Deployed") {
|
||||
return;
|
||||
}
|
||||
await utils.sleep(pollIntervalMs);
|
||||
}
|
||||
throw new Error(`Timed out waiting for CloudFront distribution ${distributionId} to reach Deployed status`);
|
||||
}
|
||||
}
|
||||
|
||||
+50
-27
@@ -67,15 +67,32 @@ export class AwsDeployToCloudFront extends AbstractTaskPlugin {
|
||||
)
|
||||
distributionIds!: string[];
|
||||
|
||||
@TaskInput({
|
||||
title: "等待部署完成",
|
||||
value: false,
|
||||
helper: "开启后,会轮询等待每个CloudFront分配状态变为Deployed(通常需要几分钟)再结束任务。" + "关闭时,提交证书更新后立即结束——CloudFront会在后台自行完成部署。" + "仅当后续任务依赖证书已全球生效时才需要开启。",
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
})
|
||||
waitForDeployed = false;
|
||||
|
||||
async onInstance() {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess<AwsAccess>(this.accessId);
|
||||
|
||||
const acmClient = new AwsClient({
|
||||
access,
|
||||
region: this.region,
|
||||
logger: this.logger,
|
||||
});
|
||||
|
||||
let certId = this.cert as string;
|
||||
if (typeof this.cert !== "string") {
|
||||
//先上传
|
||||
certId = await this.uploadToACM(access, this.cert);
|
||||
certId = await this.uploadToACM(acmClient, this.cert);
|
||||
}
|
||||
//部署到CloudFront
|
||||
|
||||
@@ -86,42 +103,47 @@ export class AwsDeployToCloudFront extends AbstractTaskPlugin {
|
||||
accessKeyId: access.accessKeyId,
|
||||
secretAccessKey: access.secretAccessKey,
|
||||
},
|
||||
// Disable the SDK's own retries; acmClient.withRetry is the single retry authority.
|
||||
maxAttempts: 1,
|
||||
});
|
||||
|
||||
// update-distribution
|
||||
for (const distributionId of this.distributionIds) {
|
||||
// get-distribution-config
|
||||
const getDistributionConfigCommand = new GetDistributionConfigCommand({
|
||||
Id: distributionId,
|
||||
});
|
||||
// get-distribution-config (with retry for throttling)
|
||||
const configData: any = await acmClient.withRetry(() => cloudFrontClient.send(new GetDistributionConfigCommand({ Id: distributionId })));
|
||||
|
||||
const configData = await cloudFrontClient.send(getDistributionConfigCommand);
|
||||
await acmClient.withRetry(() =>
|
||||
cloudFrontClient.send(
|
||||
new UpdateDistributionCommand({
|
||||
DistributionConfig: {
|
||||
...configData.DistributionConfig,
|
||||
ViewerCertificate: {
|
||||
...configData.DistributionConfig.ViewerCertificate,
|
||||
CloudFrontDefaultCertificate: false,
|
||||
ACMCertificateArn: certId,
|
||||
},
|
||||
},
|
||||
Id: distributionId,
|
||||
IfMatch: configData.ETag,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const updateDistributionCommand = new UpdateDistributionCommand({
|
||||
DistributionConfig: {
|
||||
...configData.DistributionConfig,
|
||||
ViewerCertificate: {
|
||||
...configData.DistributionConfig.ViewerCertificate,
|
||||
CloudFrontDefaultCertificate: false,
|
||||
ACMCertificateArn: certId,
|
||||
},
|
||||
},
|
||||
Id: distributionId,
|
||||
IfMatch: configData.ETag,
|
||||
});
|
||||
await cloudFrontClient.send(updateDistributionCommand);
|
||||
this.logger.info(`部署${distributionId}完成:`);
|
||||
if (this.waitForDeployed) {
|
||||
this.logger.info(`证书已提交到 ${distributionId},等待全局部署完成…`);
|
||||
// CloudFront propagates globally in a few minutes; only block when the
|
||||
// user opts in (e.g. a downstream task needs the cert already live).
|
||||
await acmClient.waitForDistributionDeployed(cloudFrontClient, distributionId);
|
||||
this.logger.info(`部署 ${distributionId} 完成`);
|
||||
} else {
|
||||
this.logger.info(`证书已提交到 ${distributionId},CloudFront 将在后台完成部署`);
|
||||
}
|
||||
}
|
||||
this.logger.info("部署完成");
|
||||
}
|
||||
|
||||
private async uploadToACM(access: AwsAccess, cert: CertInfo) {
|
||||
const acmClient = new AwsClient({
|
||||
access,
|
||||
region: this.region,
|
||||
logger: this.logger,
|
||||
});
|
||||
const awsCertARN = await acmClient.importCertificate(cert);
|
||||
private async uploadToACM(acmClient: AwsClient, cert: CertInfo) {
|
||||
const awsCertARN = await acmClient.withRetry(() => acmClient.importCertificate(cert));
|
||||
this.logger.info("证书上传成功,id=", awsCertARN);
|
||||
return awsCertARN;
|
||||
}
|
||||
@@ -140,6 +162,7 @@ export class AwsDeployToCloudFront extends AbstractTaskPlugin {
|
||||
accessKeyId: access.accessKeyId,
|
||||
secretAccessKey: access.secretAccessKey,
|
||||
},
|
||||
maxAttempts: 1,
|
||||
});
|
||||
// list-distributions
|
||||
const listDistributionsCommand = new ListDistributionsCommand({});
|
||||
|
||||
@@ -65,7 +65,7 @@ export class AwsUploadToACM extends AbstractTaskPlugin {
|
||||
region,
|
||||
logger: this.logger,
|
||||
});
|
||||
this.awsCertARN = await acmClient.importCertificate(cert);
|
||||
this.awsCertARN = await acmClient.withRetry(() => acmClient.importCertificate(cert));
|
||||
this.logger.info("证书上传成功,id=", this.awsCertARN);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
|
||||
/**
|
||||
* Fastly Access Plugin
|
||||
* Provides API authentication and HTTP helper for Fastly API endpoints.
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "fastly",
|
||||
title: "Fastly授权",
|
||||
icon: "simple-icons:fastly",
|
||||
desc: "Fastly CDN / TLS Custom Certificates API 授权",
|
||||
})
|
||||
export class FastlyAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "API Token",
|
||||
component: {
|
||||
placeholder: "Fastly API Key / Token",
|
||||
},
|
||||
helper: "前往 Fastly Account Settings -> Personal Access Tokens 创建 Token",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
apiKey = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "HTTP代理",
|
||||
component: {
|
||||
placeholder: "http://xxxx.xxx.xx:10811",
|
||||
},
|
||||
helper: "可选:是否使用 HTTP 代理访问 Fastly API",
|
||||
required: false,
|
||||
encrypt: false,
|
||||
})
|
||||
proxy = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "测试",
|
||||
component: {
|
||||
name: "api-test",
|
||||
action: "TestRequest",
|
||||
},
|
||||
helper: "测试授权是否正确",
|
||||
})
|
||||
testRequest = true;
|
||||
|
||||
async onTestRequest() {
|
||||
await this.doRequestApi("/tls/certificates?page[size]=1", null, "get");
|
||||
return "ok";
|
||||
}
|
||||
|
||||
async doRequestApi(url: string, data: any = null, method = "post") {
|
||||
const baseUrl = "https://api.fastly.com";
|
||||
const requestUrl = url.startsWith("http") ? url : `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Fastly-Key": this.apiKey,
|
||||
Accept: "application/vnd.api+json",
|
||||
};
|
||||
|
||||
if (data && (method.toLowerCase() === "post" || method.toLowerCase() === "patch" || method.toLowerCase() === "put")) {
|
||||
headers["Content-Type"] = "application/vnd.api+json";
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.ctx.http.request<any, any>({
|
||||
url: requestUrl,
|
||||
method,
|
||||
headers,
|
||||
data,
|
||||
httpProxy: this.proxy,
|
||||
});
|
||||
|
||||
return res;
|
||||
} catch (e: any) {
|
||||
const errorData = e.response?.data;
|
||||
if (errorData) {
|
||||
const errorsStr = JSON.stringify(errorData);
|
||||
this.ctx.logger.error(`Fastly API Error: ${errorsStr}`);
|
||||
throw new Error(`Fastly API 请求失败: ${errorsStr}`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches every page of a Fastly JSON:API list endpoint.
|
||||
* ctx.http already unwraps the response to its body, so the item array is at `body.data`.
|
||||
* Fastly paginates these endpoints at 20 items/page by default; without looping only the
|
||||
* first page would be visible in the selectors.
|
||||
*/
|
||||
async listAllJsonApi(path: string, pageSize = 100): Promise<any[]> {
|
||||
const all: any[] = [];
|
||||
const maxPages = 100; // hard cap to avoid an infinite loop if the API misbehaves
|
||||
for (let page = 1; page <= maxPages; page++) {
|
||||
const sep = path.includes("?") ? "&" : "?";
|
||||
const body = await this.doRequestApi(`${path}${sep}page[number]=${page}&page[size]=${pageSize}`, null, "get");
|
||||
const items = body?.data;
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
break;
|
||||
}
|
||||
all.push(...items);
|
||||
const totalPages = body?.meta?.total_pages;
|
||||
if (totalPages != null ? page >= totalPages : items.length < pageSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
async getServices() {
|
||||
// legacy /service endpoint returns a bare array of objects (not JSON:API)
|
||||
const res = await this.doRequestApi("/service?per_page=200", null, "get");
|
||||
return Array.isArray(res) ? res : res?.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Every domain configured on the active version of each Fastly service.
|
||||
* These are the names you can actually create a TLS activation for — unlike
|
||||
* `/tls/domains`, which just echoes the SAN entries of your certificates
|
||||
* (so a wildcard cert shows "*.example.com" that no service serves directly).
|
||||
*/
|
||||
async getServiceDomains(): Promise<string[]> {
|
||||
const services = await this.getServices();
|
||||
const domains = new Set<string>();
|
||||
for (const svc of services) {
|
||||
if (!svc?.id) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const activeVersion = this.resolveActiveVersion(svc);
|
||||
if (activeVersion == null) {
|
||||
continue;
|
||||
}
|
||||
const list = await this.doRequestApi(`/service/${svc.id}/version/${activeVersion}/domain`, null, "get");
|
||||
const items: any[] = Array.isArray(list) ? list : list?.data || [];
|
||||
for (const d of items) {
|
||||
if (d?.name) {
|
||||
domains.add(d.name);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.ctx.logger.error(`获取 Fastly 服务 ${svc.id} 域名失败: ${e?.message}`);
|
||||
}
|
||||
}
|
||||
return [...domains];
|
||||
}
|
||||
|
||||
private resolveActiveVersion(svc: any): number | undefined {
|
||||
const versions: any[] = Array.isArray(svc?.versions) ? svc.versions : [];
|
||||
const active = versions.find((v: any) => v?.active === true);
|
||||
if (active?.number != null) {
|
||||
return active.number;
|
||||
}
|
||||
if (typeof svc?.version === "number") {
|
||||
return svc.version;
|
||||
}
|
||||
const numbers = versions.map((v: any) => v?.number).filter((n: any) => typeof n === "number");
|
||||
return numbers.length ? Math.max(...numbers) : undefined;
|
||||
}
|
||||
|
||||
async getTlsConfigurations() {
|
||||
return this.listAllJsonApi("/tls/configurations");
|
||||
}
|
||||
|
||||
async getTlsDomains() {
|
||||
return this.listAllJsonApi("/tls/domains");
|
||||
}
|
||||
|
||||
async getCertificates() {
|
||||
return this.listAllJsonApi("/tls/certificates");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a brand new custom TLS certificate on Fastly.
|
||||
* Fastly requires the private key to exist first, so this is a 2-step flow:
|
||||
* upload (or reuse) the key at /tls/private_keys, then create the certificate
|
||||
* referencing it. Returns the new tls_certificate id.
|
||||
*/
|
||||
async createCertificate(certPem: string, keyPem: string, name?: string): Promise<string> {
|
||||
const privateKeyId = await this.ensurePrivateKey(keyPem, name);
|
||||
|
||||
this.ctx.logger.info("开始上传证书到 Fastly...");
|
||||
const certPayload = {
|
||||
data: {
|
||||
type: "tls_certificate",
|
||||
attributes: {
|
||||
cert_blob: certPem,
|
||||
...(name && { name }),
|
||||
},
|
||||
relationships: {
|
||||
tls_private_key: {
|
||||
data: {
|
||||
type: "tls_private_key",
|
||||
id: privateKeyId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const certRes = await this.doRequestApi("/tls/certificates", certPayload, "post");
|
||||
return certRes?.data?.id || "";
|
||||
} catch (e: any) {
|
||||
// Fastly also rejects a byte-identical certificate that is already stored.
|
||||
const message: string = e?.message || "";
|
||||
if (!/already exists|already in use/i.test(message)) {
|
||||
throw e;
|
||||
}
|
||||
const existingId = FastlyAccess.extractExistingId(message);
|
||||
if (existingId) {
|
||||
this.ctx.logger.info(`Fastly 已存在相同证书,复用 certId: ${existingId}`);
|
||||
return existingId;
|
||||
}
|
||||
throw new Error(
|
||||
`Fastly 证书已存在:${message}。` +
|
||||
"该证书可能此前已上传。请在【Fastly-上传证书到Fastly】任务中填写该证书ID以走更新(PATCH)流程。"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing custom TLS certificate on Fastly (PATCH).
|
||||
* Only cert_blob is sent; the private key stays linked to the certificate.
|
||||
* Fastly requires the replacement certificate to use the same private key as the
|
||||
* original, so renewals feeding this must reuse their key.
|
||||
* Returns the tls_certificate id (falls back to the passed id if the API omits it).
|
||||
*/
|
||||
async updateCertificate(certificateId: string, certPem: string, name?: string): Promise<string> {
|
||||
const payload = {
|
||||
data: {
|
||||
type: "tls_certificate",
|
||||
id: certificateId,
|
||||
attributes: {
|
||||
cert_blob: certPem,
|
||||
...(name && { name }),
|
||||
},
|
||||
},
|
||||
};
|
||||
const res = await this.doRequestApi(`/tls/certificates/${certificateId}`, payload, "patch");
|
||||
return res?.data?.id || certificateId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a certificate to one TLS domain under a TLS configuration.
|
||||
* A tls_activation is per (certificate, configuration, domain), and only one may
|
||||
* exist per domain+configuration. If one is already there (e.g. on renewal) we
|
||||
* PATCH it onto the new certificate instead of failing on a duplicate POST.
|
||||
*/
|
||||
async deployActivation(certificateId: string, configurationId: string, domainId: string): Promise<{ id: string; action: "created" | "updated" }> {
|
||||
const existing = await this.findActivation(configurationId, domainId);
|
||||
if (existing?.id) {
|
||||
const res = await this.doRequestApi(
|
||||
`/tls/activations/${existing.id}`,
|
||||
{
|
||||
data: {
|
||||
type: "tls_activation",
|
||||
id: existing.id,
|
||||
relationships: {
|
||||
tls_certificate: { data: { type: "tls_certificate", id: certificateId } },
|
||||
},
|
||||
},
|
||||
},
|
||||
"patch"
|
||||
);
|
||||
return { id: res?.data?.id || existing.id, action: "updated" };
|
||||
}
|
||||
|
||||
const res = await this.doRequestApi(
|
||||
"/tls/activations",
|
||||
{
|
||||
data: {
|
||||
type: "tls_activation",
|
||||
relationships: {
|
||||
tls_certificate: { data: { type: "tls_certificate", id: certificateId } },
|
||||
tls_configuration: { data: { type: "tls_configuration", id: configurationId } },
|
||||
tls_domain: { data: { type: "tls_domain", id: domainId } },
|
||||
},
|
||||
},
|
||||
},
|
||||
"post"
|
||||
);
|
||||
return { id: res?.data?.id || "", action: "created" };
|
||||
}
|
||||
|
||||
private async findActivation(configurationId: string, domainId: string): Promise<any | undefined> {
|
||||
// encodeURIComponent 按规范保留 `*`,但 Fastly 过滤条件要求通配符编码为 `%2A`。
|
||||
const encodedDomainId = encodeURIComponent(domainId).replace(/\*/g, "%2A");
|
||||
const body = await this.doRequestApi(
|
||||
`/tls/activations?filter[tls_domain.id]=${encodedDomainId}&page[size]=100`,
|
||||
null,
|
||||
"get"
|
||||
);
|
||||
const items: any[] = Array.isArray(body?.data) ? body.data : [];
|
||||
if (items.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
// Prefer the activation tied to this configuration; fall back to the sole match.
|
||||
return items.find((a: any) => a?.relationships?.tls_configuration?.data?.id === configurationId) ?? (items.length === 1 ? items[0] : undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the private key, or reuses the one Fastly already stores.
|
||||
* Fastly deduplicates TLS private keys by content: re-uploading an existing key
|
||||
* fails with 400 "Key already exists: '<id>'" (common on renewal when the key is
|
||||
* reused). That id is the existing tls_private_key resource, so recover and use it.
|
||||
*/
|
||||
private async ensurePrivateKey(keyPem: string, name?: string): Promise<string> {
|
||||
this.ctx.logger.info("开始上传私钥到 Fastly...");
|
||||
try {
|
||||
const keyRes = await this.doRequestApi(
|
||||
"/tls/private_keys",
|
||||
{ data: { type: "tls_private_key", attributes: { key: keyPem, ...(name && { name }) } } },
|
||||
"post"
|
||||
);
|
||||
const id = keyRes?.data?.id;
|
||||
if (!id) {
|
||||
throw new Error("Fastly 私钥上传失败,未获取到 private key ID");
|
||||
}
|
||||
this.ctx.logger.info(`Fastly 私钥上传成功, privateKeyId: ${id}`);
|
||||
return id;
|
||||
} catch (e: any) {
|
||||
const message: string = e?.message || "";
|
||||
if (!/already exists/i.test(message)) {
|
||||
throw e;
|
||||
}
|
||||
const idFromError = FastlyAccess.extractExistingId(message);
|
||||
if (idFromError) {
|
||||
this.ctx.logger.info(`Fastly 已存在相同私钥,复用 privateKeyId: ${idFromError}`);
|
||||
return idFromError;
|
||||
}
|
||||
if (name) {
|
||||
const keys = await this.listAllJsonApi("/tls/private_keys");
|
||||
const matched = keys.filter((k: any) => k?.attributes?.name === name);
|
||||
if (matched.length === 1) {
|
||||
this.ctx.logger.info(`Fastly 已存在同名私钥,复用 privateKeyId: ${matched[0].id}`);
|
||||
return matched[0].id;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Fastly 私钥已存在但无法确定其ID:${message}。` +
|
||||
"请在【Fastly-上传证书到Fastly】任务中填写已有证书ID以走更新(PATCH)流程," +
|
||||
"或在 Fastly 控制台删除该孤立私钥后重试。"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls the existing resource id out of a Fastly "already exists" error, e.g.
|
||||
* `... Key already exists: 'EO8Drv7EYjThQ4DUPo4Ot0'`. Returns undefined when the
|
||||
* message carries no id (the caller then falls back or surfaces guidance).
|
||||
*/
|
||||
private static extractExistingId(message: string): string | undefined {
|
||||
return message.match(/already exists:\s*['"]?([A-Za-z0-9_-]{8,})['"]?/i)?.[1];
|
||||
}
|
||||
}
|
||||
|
||||
new FastlyAccess();
|
||||
@@ -0,0 +1,505 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { FastlyAccess } from "./access.js";
|
||||
import { FastlyUploadCertPlugin } from "./plugins/plugin-upload-cert.js";
|
||||
import { FastlyPurgeCachePlugin } from "./plugins/plugin-purge-cache.js";
|
||||
import { FastlyDeployCertPlugin } from "./plugins/plugin-deploy-to-service.js";
|
||||
import { FastlyRefreshCertPlugin } from "./plugins/plugin-refresh-cert.js";
|
||||
const mockCert = {
|
||||
crt: "-----BEGIN CERTIFICATE-----\nMOCKCERT\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMOCKINTERMEDIATE\n-----END CERTIFICATE-----",
|
||||
key: "-----BEGIN PRIVATE KEY-----\nMOCKKEY\n-----END PRIVATE KEY-----",
|
||||
};
|
||||
|
||||
// Real FastlyAccess so plugins exercise its createCertificate/updateCertificate
|
||||
// helpers; only the low-level doRequestApi is stubbed.
|
||||
function fakeAccess(doRequestApi: (path: string, payload: any, method: string) => any) {
|
||||
const access = new FastlyAccess();
|
||||
(access as any).ctx = { logger: { info: () => {}, error: () => {} } };
|
||||
(access as any).doRequestApi = doRequestApi;
|
||||
return access;
|
||||
}
|
||||
|
||||
describe("FastlyAccess", () => {
|
||||
it("should build correct request headers and URL for POST", async () => {
|
||||
const access = new FastlyAccess();
|
||||
access.apiKey = "test-fastly-key";
|
||||
|
||||
let capturedReq: any = null;
|
||||
(access as any).ctx = {
|
||||
http: {
|
||||
request: async (req: any) => {
|
||||
capturedReq = req;
|
||||
return { data: { id: "tls_cert_123" } };
|
||||
},
|
||||
},
|
||||
logger: { info: () => {}, error: () => {} },
|
||||
};
|
||||
|
||||
const res = await access.doRequestApi("/tls/certificates", { test: true }, "post");
|
||||
|
||||
assert.equal(capturedReq.url, "https://api.fastly.com/tls/certificates");
|
||||
assert.equal(capturedReq.headers["Fastly-Key"], "test-fastly-key");
|
||||
assert.equal(capturedReq.headers["Accept"], "application/vnd.api+json");
|
||||
assert.equal(capturedReq.headers["Content-Type"], "application/vnd.api+json");
|
||||
assert.equal(res.data.id, "tls_cert_123");
|
||||
});
|
||||
|
||||
it("should NOT set Content-Type for GET requests", async () => {
|
||||
const access = new FastlyAccess();
|
||||
access.apiKey = "test-key";
|
||||
|
||||
let capturedReq: any = null;
|
||||
(access as any).ctx = {
|
||||
http: {
|
||||
request: async (req: any) => {
|
||||
capturedReq = req;
|
||||
return { data: [] };
|
||||
},
|
||||
},
|
||||
logger: { info: () => {}, error: () => {} },
|
||||
};
|
||||
|
||||
await access.doRequestApi("/tls/certificates?page[size]=1", null, "get");
|
||||
assert.equal(capturedReq.headers["Content-Type"], undefined);
|
||||
assert.equal(capturedReq.headers["Fastly-Key"], "test-key");
|
||||
});
|
||||
|
||||
it("should pass proxy to http request when proxy is set", async () => {
|
||||
const access = new FastlyAccess();
|
||||
access.apiKey = "test-key";
|
||||
access.proxy = "http://proxy.example.com:3128";
|
||||
|
||||
let capturedReq: any = null;
|
||||
(access as any).ctx = {
|
||||
http: {
|
||||
request: async (req: any) => {
|
||||
capturedReq = req;
|
||||
return {};
|
||||
},
|
||||
},
|
||||
logger: { info: () => {}, error: () => {} },
|
||||
};
|
||||
|
||||
await access.doRequestApi("/tls/certificates", null, "get");
|
||||
assert.equal(capturedReq.httpProxy, "http://proxy.example.com:3128");
|
||||
});
|
||||
});
|
||||
|
||||
describe("FastlyAccess list helpers", () => {
|
||||
function mockAccess(handler: (url: string) => any) {
|
||||
const access = new FastlyAccess();
|
||||
access.apiKey = "k";
|
||||
(access as any).ctx = {
|
||||
http: { request: async (req: any) => handler(req.url) },
|
||||
logger: { info: () => {}, error: () => {} },
|
||||
};
|
||||
return access;
|
||||
}
|
||||
|
||||
it("getCertificates returns body.data (not body.data.data) and aggregates all pages", async () => {
|
||||
const urls: string[] = [];
|
||||
const access = mockAccess((url: string) => {
|
||||
urls.push(url);
|
||||
const page = Number(url.match(/page\[number\]=(\d+)/)?.[1]);
|
||||
// 3 pages total, 2 items each
|
||||
const pageItems = [
|
||||
[{ id: "c1" }, { id: "c2" }],
|
||||
[{ id: "c3" }, { id: "c4" }],
|
||||
[{ id: "c5" }, { id: "c6" }],
|
||||
];
|
||||
return { data: pageItems[page - 1] ?? [], meta: { total_pages: 3 } };
|
||||
});
|
||||
|
||||
const list = await access.getCertificates();
|
||||
assert.deepEqual(
|
||||
list.map((x: any) => x.id),
|
||||
["c1", "c2", "c3", "c4", "c5", "c6"]
|
||||
);
|
||||
assert.equal(urls.length, 3);
|
||||
});
|
||||
|
||||
it("stops paginating on a short page when total_pages is absent", async () => {
|
||||
let calls = 0;
|
||||
const access = mockAccess(() => {
|
||||
calls++;
|
||||
// single short page (< pageSize) => no more requests
|
||||
return { data: [{ id: "d1" }] };
|
||||
});
|
||||
|
||||
const list = await access.getTlsDomains();
|
||||
assert.deepEqual(list, [{ id: "d1" }]);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
|
||||
it("getServices unwraps a bare array response", async () => {
|
||||
const access = mockAccess(() => [{ id: "svc1", name: "a" }]);
|
||||
const list = await access.getServices();
|
||||
assert.deepEqual(list, [{ id: "svc1", name: "a" }]);
|
||||
});
|
||||
|
||||
it("getServiceDomains collects domains from each service's active version", async () => {
|
||||
const access = mockAccess((url: string) => {
|
||||
if (url.endsWith("/service?per_page=200")) {
|
||||
return [
|
||||
{
|
||||
id: "svc1",
|
||||
versions: [
|
||||
{ number: 1, active: false },
|
||||
{ number: 2, active: true },
|
||||
],
|
||||
},
|
||||
{ id: "svc2", version: 5 },
|
||||
];
|
||||
}
|
||||
if (url.endsWith("/service/svc1/version/2/domain")) {
|
||||
return [{ name: "www.g0l.net" }, { name: "img.g0l.net" }];
|
||||
}
|
||||
if (url.endsWith("/service/svc2/version/5/domain")) {
|
||||
return [{ name: "www.g0l.net" }, { name: "api.other.net" }];
|
||||
}
|
||||
throw new Error(`unexpected ${url}`);
|
||||
});
|
||||
|
||||
const domains = await access.getServiceDomains();
|
||||
assert.deepEqual(domains.sort(), ["api.other.net", "img.g0l.net", "www.g0l.net"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("FastlyUploadCertPlugin - new certificate (2-step flow)", () => {
|
||||
it("should upload private key first then create certificate with relationship", async () => {
|
||||
const plugin = new FastlyUploadCertPlugin();
|
||||
plugin.cert = mockCert as any;
|
||||
plugin.accessId = "access-1";
|
||||
plugin.name = "my-cert";
|
||||
|
||||
const calls: { path: string; payload: any; method: string }[] = [];
|
||||
|
||||
const mockAccess = fakeAccess(async (path: string, payload: any, method: string) => {
|
||||
calls.push({ path, payload, method });
|
||||
if (path === "/tls/private_keys") {
|
||||
return { data: { id: "pk_abc123" } };
|
||||
}
|
||||
if (path === "/tls/certificates") {
|
||||
return { data: { id: "tls_cert_new_999" } };
|
||||
}
|
||||
throw new Error(`Unexpected call: ${path}`);
|
||||
});
|
||||
|
||||
(plugin as any).getAccess = async () => mockAccess;
|
||||
(plugin as any).logger = { info: () => {}, error: () => {} };
|
||||
|
||||
await plugin.execute();
|
||||
|
||||
// Step 1: private key upload
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(calls[0].path, "/tls/private_keys");
|
||||
assert.equal(calls[0].method, "post");
|
||||
assert.equal(calls[0].payload.data.type, "tls_private_key");
|
||||
assert.equal(calls[0].payload.data.attributes.key, mockCert.key);
|
||||
assert.equal(calls[0].payload.data.attributes.name, "my-cert");
|
||||
|
||||
// Step 2: certificate upload with relationship
|
||||
assert.equal(calls[1].path, "/tls/certificates");
|
||||
assert.equal(calls[1].method, "post");
|
||||
assert.equal(calls[1].payload.data.type, "tls_certificate");
|
||||
assert.equal(calls[1].payload.data.attributes.cert_blob, mockCert.crt);
|
||||
assert.equal(calls[1].payload.data.relationships.tls_private_key.data.id, "pk_abc123");
|
||||
assert.equal(calls[1].payload.data.relationships.tls_private_key.data.type, "tls_private_key");
|
||||
|
||||
assert.equal(plugin.fastlyCertId, "tls_cert_new_999");
|
||||
});
|
||||
|
||||
it("should throw if private key upload returns no ID", async () => {
|
||||
const plugin = new FastlyUploadCertPlugin();
|
||||
plugin.cert = mockCert as any;
|
||||
plugin.accessId = "access-1";
|
||||
|
||||
const mockAccess = fakeAccess(async () => ({ data: {} })); // no id returned
|
||||
|
||||
(plugin as any).getAccess = async () => mockAccess;
|
||||
(plugin as any).logger = { info: () => {}, error: () => {} };
|
||||
|
||||
await assert.rejects(() => plugin.execute(), /Fastly 私钥上传失败,未获取到 private key ID/);
|
||||
});
|
||||
|
||||
it("reuses the existing private key when Fastly reports it already exists", async () => {
|
||||
const plugin = new FastlyUploadCertPlugin();
|
||||
plugin.cert = mockCert as any;
|
||||
plugin.accessId = "access-1";
|
||||
|
||||
const calls: { path: string; payload: any; method: string }[] = [];
|
||||
|
||||
const mockAccess = fakeAccess(async (path: string, payload: any, method: string) => {
|
||||
calls.push({ path, payload, method });
|
||||
if (path === "/tls/private_keys") {
|
||||
throw new Error('Fastly API 请求失败: {"errors":[{"title":"Can\'t create key","detail":"Key already exists: \'EO8Drv7EYjThQ4DUPo4Ot0\'"}]}');
|
||||
}
|
||||
if (path === "/tls/certificates") {
|
||||
return { data: { id: "tls_cert_new_1" } };
|
||||
}
|
||||
throw new Error(`Unexpected call: ${path}`);
|
||||
});
|
||||
|
||||
(plugin as any).getAccess = async () => mockAccess;
|
||||
(plugin as any).logger = { info: () => {}, error: () => {} };
|
||||
|
||||
await plugin.execute();
|
||||
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(calls[1].path, "/tls/certificates");
|
||||
assert.equal(calls[1].payload.data.relationships.tls_private_key.data.id, "EO8Drv7EYjThQ4DUPo4Ot0");
|
||||
assert.equal(plugin.fastlyCertId, "tls_cert_new_1");
|
||||
});
|
||||
|
||||
it("reuses the existing certificate when Fastly reports it already exists", async () => {
|
||||
const plugin = new FastlyUploadCertPlugin();
|
||||
plugin.cert = mockCert as any;
|
||||
plugin.accessId = "access-1";
|
||||
|
||||
const mockAccess = fakeAccess(async (path: string) => {
|
||||
if (path === "/tls/private_keys") {
|
||||
return { data: { id: "pk_1" } };
|
||||
}
|
||||
if (path === "/tls/certificates") {
|
||||
throw new Error('Fastly API 请求失败: {"errors":[{"title":"Can\'t create certificate","detail":"Certificate already exists: \'aBcDeFgHiJkLmNoPqRsTuV\'"}]}');
|
||||
}
|
||||
throw new Error(`Unexpected call: ${path}`);
|
||||
});
|
||||
|
||||
(plugin as any).getAccess = async () => mockAccess;
|
||||
(plugin as any).logger = { info: () => {}, error: () => {} };
|
||||
|
||||
await plugin.execute();
|
||||
|
||||
assert.equal(plugin.fastlyCertId, "aBcDeFgHiJkLmNoPqRsTuV");
|
||||
});
|
||||
|
||||
it("surfaces guidance when the certificate already exists without a recoverable id", async () => {
|
||||
const plugin = new FastlyUploadCertPlugin();
|
||||
plugin.cert = mockCert as any;
|
||||
plugin.accessId = "access-1";
|
||||
|
||||
const mockAccess = fakeAccess(async (path: string) => {
|
||||
if (path === "/tls/private_keys") {
|
||||
return { data: { id: "pk_1" } };
|
||||
}
|
||||
if (path === "/tls/certificates") {
|
||||
throw new Error('Fastly API 请求失败: {"errors":[{"detail":"cert_blob is already in use"}]}');
|
||||
}
|
||||
throw new Error(`Unexpected call: ${path}`);
|
||||
});
|
||||
|
||||
(plugin as any).getAccess = async () => mockAccess;
|
||||
(plugin as any).logger = { info: () => {}, error: () => {} };
|
||||
|
||||
await assert.rejects(() => plugin.execute(), /填写该证书ID以走更新\(PATCH\)流程/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("FastlyUploadCertPlugin - update existing certificate (PATCH)", () => {
|
||||
it("should PATCH existing certificate with only cert_blob, no private key step", async () => {
|
||||
const plugin = new FastlyUploadCertPlugin();
|
||||
plugin.cert = mockCert as any;
|
||||
plugin.accessId = "access-1";
|
||||
plugin.certificateId = "tls_cert_existing_123";
|
||||
plugin.name = "updated-cert";
|
||||
|
||||
const calls: { path: string; payload: any; method: string }[] = [];
|
||||
|
||||
const mockAccess = fakeAccess(async (path: string, payload: any, method: string) => {
|
||||
calls.push({ path, payload, method });
|
||||
return { data: { id: "tls_cert_existing_123" } };
|
||||
});
|
||||
|
||||
(plugin as any).getAccess = async () => mockAccess;
|
||||
(plugin as any).logger = { info: () => {}, error: () => {} };
|
||||
|
||||
await plugin.execute();
|
||||
|
||||
// Only 1 API call for update
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].path, "/tls/certificates/tls_cert_existing_123");
|
||||
assert.equal(calls[0].method, "patch");
|
||||
assert.equal(calls[0].payload.data.type, "tls_certificate");
|
||||
assert.equal(calls[0].payload.data.id, "tls_cert_existing_123");
|
||||
assert.equal(calls[0].payload.data.attributes.cert_blob, mockCert.crt);
|
||||
assert.equal(calls[0].payload.data.attributes.name, "updated-cert");
|
||||
// No relationships on PATCH
|
||||
assert.equal(calls[0].payload.data.relationships, undefined);
|
||||
|
||||
assert.equal(plugin.fastlyCertId, "tls_cert_existing_123");
|
||||
});
|
||||
|
||||
it("should fallback fastlyCertId to certificateId when response has no id", async () => {
|
||||
const plugin = new FastlyUploadCertPlugin();
|
||||
plugin.cert = mockCert as any;
|
||||
plugin.accessId = "access-1";
|
||||
plugin.certificateId = "tls_cert_fallback_id";
|
||||
|
||||
const mockAccess = fakeAccess(async () => ({ data: {} })); // no id in response
|
||||
|
||||
(plugin as any).getAccess = async () => mockAccess;
|
||||
(plugin as any).logger = { info: () => {}, error: () => {} };
|
||||
|
||||
await plugin.execute();
|
||||
assert.equal(plugin.fastlyCertId, "tls_cert_fallback_id");
|
||||
});
|
||||
});
|
||||
|
||||
describe("FastlyPurgeCachePlugin", () => {
|
||||
it("should call purge_all on the provided serviceId", async () => {
|
||||
const plugin = new FastlyPurgeCachePlugin();
|
||||
plugin.accessId = "access-1";
|
||||
plugin.serviceId = "svc_123";
|
||||
|
||||
let capturedUrl = "";
|
||||
let capturedMethod = "";
|
||||
|
||||
const mockAccess = {
|
||||
doRequestApi: async (path: string, payload: any, method: string) => {
|
||||
capturedUrl = path;
|
||||
capturedMethod = method;
|
||||
return { data: { status: "ok" } };
|
||||
},
|
||||
};
|
||||
|
||||
(plugin as any).getAccess = async () => mockAccess;
|
||||
(plugin as any).logger = { info: () => {}, error: () => {} };
|
||||
|
||||
await plugin.execute();
|
||||
|
||||
assert.equal(capturedUrl, "/service/svc_123/purge_all");
|
||||
assert.equal(capturedMethod, "post");
|
||||
});
|
||||
});
|
||||
|
||||
describe("FastlyDeployCertPlugin", () => {
|
||||
it("creates one tls_activation per domain when none exists yet", async () => {
|
||||
const plugin = new FastlyDeployCertPlugin();
|
||||
plugin.accessId = "access-1";
|
||||
plugin.cert = "cert_1";
|
||||
plugin.domainIds = ["*.g0l.net", "g0l.net"];
|
||||
plugin.configurationId = "cfg_1";
|
||||
|
||||
const calls: { path: string; payload: any; method: string }[] = [];
|
||||
|
||||
const mockAccess = fakeAccess(async (path: string, payload: any, method: string) => {
|
||||
calls.push({ path, payload, method });
|
||||
if (method === "get") {
|
||||
return { data: [] }; // no existing activation
|
||||
}
|
||||
return { data: { id: "act_new" } };
|
||||
});
|
||||
|
||||
(plugin as any).getAccess = async () => mockAccess;
|
||||
(plugin as any).logger = { info: () => {}, error: () => {} };
|
||||
|
||||
await plugin.execute();
|
||||
|
||||
const posts = calls.filter(c => c.method === "post");
|
||||
assert.equal(posts.length, 2);
|
||||
assert.equal(posts[0].path, "/tls/activations");
|
||||
assert.equal(posts[0].payload.data.relationships.tls_certificate.data.id, "cert_1");
|
||||
assert.equal(posts[0].payload.data.relationships.tls_configuration.data.id, "cfg_1");
|
||||
assert.equal(posts[0].payload.data.relationships.tls_domain.data.id, "*.g0l.net");
|
||||
assert.equal(posts[1].payload.data.relationships.tls_domain.data.id, "g0l.net");
|
||||
// domain filter is url-encoded (wildcard -> %2A)
|
||||
assert.ok(calls[0].path.startsWith("/tls/activations?filter[tls_domain.id]=%2A.g0l.net"));
|
||||
});
|
||||
|
||||
it("patches the existing activation onto the new cert (renewal)", async () => {
|
||||
const plugin = new FastlyDeployCertPlugin();
|
||||
plugin.accessId = "access-1";
|
||||
plugin.cert = "cert_new";
|
||||
plugin.domainIds = ["g0l.net"];
|
||||
plugin.configurationId = "cfg_1";
|
||||
|
||||
const calls: { path: string; payload: any; method: string }[] = [];
|
||||
|
||||
const mockAccess = fakeAccess(async (path: string, payload: any, method: string) => {
|
||||
calls.push({ path, payload, method });
|
||||
if (method === "get") {
|
||||
return { data: [{ id: "act_1", relationships: { tls_configuration: { data: { id: "cfg_1" } } } }] };
|
||||
}
|
||||
return { data: { id: "act_1" } };
|
||||
});
|
||||
|
||||
(plugin as any).getAccess = async () => mockAccess;
|
||||
(plugin as any).logger = { info: () => {}, error: () => {} };
|
||||
|
||||
await plugin.execute();
|
||||
|
||||
const write = calls.find(c => c.method !== "get")!;
|
||||
assert.equal(write.method, "patch");
|
||||
assert.equal(write.path, "/tls/activations/act_1");
|
||||
assert.equal(write.payload.data.id, "act_1");
|
||||
assert.equal(write.payload.data.relationships.tls_certificate.data.id, "cert_new");
|
||||
assert.equal(write.payload.data.relationships.tls_configuration, undefined);
|
||||
});
|
||||
|
||||
it("uploads a raw CertInfo to Fastly first, then binds the new cert id", async () => {
|
||||
const plugin = new FastlyDeployCertPlugin();
|
||||
plugin.accessId = "access-1";
|
||||
plugin.cert = mockCert as any;
|
||||
plugin.name = "my-cert";
|
||||
plugin.domainIds = ["dom_1"];
|
||||
plugin.configurationId = "cfg_1";
|
||||
|
||||
const calls: { path: string; payload: any; method: string }[] = [];
|
||||
|
||||
const mockAccess = fakeAccess(async (path: string, payload: any, method: string) => {
|
||||
calls.push({ path, payload, method });
|
||||
if (path === "/tls/private_keys") {
|
||||
return { data: { id: "pk_1" } };
|
||||
}
|
||||
if (path === "/tls/certificates") {
|
||||
return { data: { id: "tls_cert_uploaded_1" } };
|
||||
}
|
||||
if (method === "get") {
|
||||
return { data: [] };
|
||||
}
|
||||
return { data: { id: "act_new" } };
|
||||
});
|
||||
|
||||
(plugin as any).getAccess = async () => mockAccess;
|
||||
(plugin as any).logger = { info: () => {}, error: () => {} };
|
||||
|
||||
await plugin.execute();
|
||||
|
||||
assert.equal(calls[0].path, "/tls/private_keys");
|
||||
assert.equal(calls[1].path, "/tls/certificates");
|
||||
assert.equal(calls[1].payload.data.attributes.cert_blob, mockCert.crt);
|
||||
const post = calls.find(c => c.path === "/tls/activations" && c.method === "post")!;
|
||||
assert.equal(post.payload.data.relationships.tls_certificate.data.id, "tls_cert_uploaded_1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("FastlyRefreshCertPlugin", () => {
|
||||
it("should update multiple certificates via PATCH", async () => {
|
||||
const plugin = new FastlyRefreshCertPlugin();
|
||||
plugin.cert = mockCert as any;
|
||||
plugin.accessId = "access-1";
|
||||
plugin.certList = ["cert_a", "cert_b"];
|
||||
|
||||
const calls: { path: string; payload: any }[] = [];
|
||||
|
||||
const mockAccess = {
|
||||
doRequestApi: async (path: string, payload: any, method: string) => {
|
||||
calls.push({ path, payload });
|
||||
return { data: { id: path.split("/").pop() } };
|
||||
},
|
||||
};
|
||||
|
||||
(plugin as any).getAccess = async () => mockAccess;
|
||||
(plugin as any).logger = { info: () => {}, error: () => {} };
|
||||
|
||||
await plugin.execute();
|
||||
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(calls[0].path, "/tls/certificates/cert_a");
|
||||
assert.equal(calls[0].payload.data.id, "cert_a");
|
||||
assert.equal(calls[0].payload.data.attributes.cert_blob, mockCert.crt);
|
||||
|
||||
assert.equal(calls[1].path, "/tls/certificates/cert_b");
|
||||
assert.equal(calls[1].payload.data.id, "cert_b");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./access.js";
|
||||
export * from "./plugins/index.js";
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./plugin-upload-cert.js";
|
||||
export * from "./plugin-purge-cache.js";
|
||||
export * from "./plugin-deploy-to-service.js";
|
||||
export * from "./plugin-refresh-cert.js";
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { FastlyAccess } from "../access.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "FastlyDeployCert",
|
||||
title: "Fastly-部署TLS激活",
|
||||
desc: "部署 Fastly 证书 (创建 TLS Activation 绑定证书到域名)",
|
||||
icon: "simple-icons:fastly",
|
||||
group: pluginGroups.cdn.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class FastlyDeployCertPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "选择前置任务输出的域名证书(将自动上传到 Fastly)," + "或选择前置【Fastly-上传证书到Fastly】任务输出的 Fastly 证书ID。",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames, "FastlyUploadCert"],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo | string;
|
||||
|
||||
@TaskInput({
|
||||
title: "证书名称",
|
||||
helper: "可选。当传入的是域名证书需要先上传时,作为 Fastly 上的自定义证书名称/标签",
|
||||
component: {
|
||||
placeholder: "例如: my-fastly-cert",
|
||||
},
|
||||
required: false,
|
||||
})
|
||||
name = "";
|
||||
|
||||
@TaskInput({
|
||||
title: "Access授权",
|
||||
helper: "Fastly 授权凭证",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "fastly",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "TLS 域名",
|
||||
helper:
|
||||
"选择要绑定证书的域名,可多选(将为每个域名创建或更新一个 TLS 激活)。" +
|
||||
"列表为你 Fastly 服务上配置的域名,请选择被证书覆盖的具体主机名(如 www.example.com)," +
|
||||
"而非通配 *.example.com(除非确有服务使用通配域名)。列表没有的也可直接输入。",
|
||||
action: FastlyDeployCertPlugin.prototype.onGetTlsDomainList.name,
|
||||
pager: false,
|
||||
search: false,
|
||||
required: true,
|
||||
})
|
||||
)
|
||||
domainIds!: string[];
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "TLS 配置",
|
||||
helper: "选择关联的 TLS 配置",
|
||||
action: FastlyDeployCertPlugin.prototype.onGetTlsConfigurationList.name,
|
||||
pager: false,
|
||||
search: false,
|
||||
single: true,
|
||||
required: true,
|
||||
})
|
||||
)
|
||||
configurationId!: string;
|
||||
|
||||
async onInstance() {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const access = (await this.getAccess(this.accessId)) as FastlyAccess;
|
||||
|
||||
// remote-select hands back an array; tolerate a single value too (older config / single:true).
|
||||
const rawDomains: any = this.domainIds ?? (this as any).domainId;
|
||||
const domainIds: string[] = Array.isArray(rawDomains) ? rawDomains.filter(Boolean) : rawDomains ? [rawDomains] : [];
|
||||
const rawConfig: any = this.configurationId;
|
||||
const configurationId: string = Array.isArray(rawConfig) ? rawConfig[0] : rawConfig;
|
||||
|
||||
if (domainIds.length === 0 || !configurationId) {
|
||||
throw new Error("请提供完整的 TLS 域名和 TLS 配置");
|
||||
}
|
||||
|
||||
// `cert` is a string when it comes from a FastlyUploadCert step (already a Fastly
|
||||
// tls_certificate id); otherwise it is a raw CertInfo that must be uploaded first.
|
||||
let certificateId: string;
|
||||
if (typeof this.cert === "string") {
|
||||
certificateId = this.cert;
|
||||
} else {
|
||||
const certName = this.name && this.name.trim() ? this.name.trim() : undefined;
|
||||
this.logger.info("未检测到 Fastly 证书ID,先上传证书到 Fastly...");
|
||||
certificateId = await access.createCertificate(this.cert.crt, this.cert.key, certName);
|
||||
this.logger.info(`证书上传成功, Fastly 证书ID: ${certificateId}`);
|
||||
}
|
||||
|
||||
if (!certificateId) {
|
||||
throw new Error("未能获取 Fastly 证书ID");
|
||||
}
|
||||
|
||||
for (const domainId of domainIds) {
|
||||
this.logger.info(`开始部署 Fastly TLS 激活 (域名: ${domainId})...`);
|
||||
const { id, action } = await access.deployActivation(certificateId, configurationId, domainId);
|
||||
this.logger.info(`Fastly TLS 激活${action === "updated" ? "更新" : "创建"}成功 (域名: ${domainId}), 激活ID: ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
async onGetTlsDomainList() {
|
||||
const access = (await this.getAccess(this.accessId)) as FastlyAccess;
|
||||
|
||||
const [serviceDomains, tlsDomains] = await Promise.all([access.getServiceDomains().catch(() => [] as string[]), access.getTlsDomains().catch(() => [] as any[])]);
|
||||
|
||||
const options: { label: string; value: string }[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
// Domains actually served by your Fastly services — these can be activated.
|
||||
for (const name of serviceDomains) {
|
||||
if (name && !seen.has(name)) {
|
||||
seen.add(name);
|
||||
options.push({ label: name, value: name });
|
||||
}
|
||||
}
|
||||
|
||||
// TLS domains from certificates/subscriptions (incl. wildcards) as extra choices.
|
||||
for (const item of tlsDomains || []) {
|
||||
const name = item?.id;
|
||||
if (name && !seen.has(name)) {
|
||||
seen.add(name);
|
||||
options.push({ label: `${name} (来自证书)`, value: name });
|
||||
}
|
||||
}
|
||||
|
||||
return { list: options };
|
||||
}
|
||||
|
||||
async onGetTlsConfigurationList() {
|
||||
const access = (await this.getAccess(this.accessId)) as FastlyAccess;
|
||||
const list = await access.getTlsConfigurations();
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
return { list: [] };
|
||||
}
|
||||
|
||||
const options = list.map((item: any) => {
|
||||
return {
|
||||
label: `${item.attributes?.name || "Unnamed Configuration"} (${item.id})`,
|
||||
value: item.id,
|
||||
};
|
||||
});
|
||||
|
||||
return { list: options };
|
||||
}
|
||||
}
|
||||
|
||||
new FastlyDeployCertPlugin();
|
||||
@@ -0,0 +1,77 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { FastlyAccess } from "../access.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "FastlyPurgeCache",
|
||||
title: "Fastly-清除缓存",
|
||||
desc: "清除 Fastly 指定 Service 的所有缓存 (Purge All)",
|
||||
icon: "simple-icons:fastly",
|
||||
group: pluginGroups.cdn.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class FastlyPurgeCachePlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "Access授权",
|
||||
helper: "Fastly 授权凭证",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "fastly",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "Service (服务)",
|
||||
helper: "选择要清理所有缓存的 Fastly 服务",
|
||||
action: FastlyPurgeCachePlugin.prototype.onGetServiceList.name,
|
||||
pager: false,
|
||||
search: false,
|
||||
required: true,
|
||||
})
|
||||
)
|
||||
serviceId!: string;
|
||||
|
||||
async onInstance() {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const access = (await this.getAccess(this.accessId)) as FastlyAccess;
|
||||
|
||||
if (!this.serviceId) {
|
||||
throw new Error("请选择要清理缓存的 Service");
|
||||
}
|
||||
|
||||
this.logger.info(`开始清理 Fastly 服务 [${this.serviceId}] 的所有缓存...`);
|
||||
|
||||
// POST /service/{service_id}/purge_all — no body; auth is via the Fastly-Key header
|
||||
await access.doRequestApi(`/service/${this.serviceId}/purge_all`, null, "post");
|
||||
|
||||
this.logger.info(`清理 Fastly 服务 [${this.serviceId}] 缓存成功`);
|
||||
}
|
||||
|
||||
async onGetServiceList() {
|
||||
const access = (await this.getAccess(this.accessId)) as FastlyAccess;
|
||||
const services = await access.getServices();
|
||||
|
||||
if (!services || services.length === 0) {
|
||||
return { list: [] };
|
||||
}
|
||||
|
||||
const options = services.map((item: any) => {
|
||||
return {
|
||||
label: `${item.name} (${item.id})`,
|
||||
value: item.id,
|
||||
};
|
||||
});
|
||||
|
||||
return { list: options };
|
||||
}
|
||||
}
|
||||
|
||||
new FastlyPurgeCachePlugin();
|
||||
@@ -0,0 +1,104 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { FastlyAccess } from "../access.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "FastlyRefreshCert",
|
||||
title: "Fastly-更新证书",
|
||||
desc: "自动更新 Fastly CDN 上的已有自定义证书",
|
||||
icon: "simple-icons:fastly",
|
||||
group: pluginGroups.cdn.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class FastlyRefreshCertPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput({
|
||||
title: "Access授权",
|
||||
helper: "Fastly 授权凭证",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "fastly",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "证书列表",
|
||||
helper: "选择要更新的 Fastly 证书 (必须是已存在的自定义证书)。" + "注意:Fastly 要求更新后的证书与原证书使用相同私钥,请确保续期时复用私钥(reuse key),否则更新会失败。",
|
||||
action: FastlyRefreshCertPlugin.prototype.onGetCertList.name,
|
||||
pager: false,
|
||||
search: false,
|
||||
required: true,
|
||||
})
|
||||
)
|
||||
certList!: string[];
|
||||
|
||||
async onInstance() {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const access = (await this.getAccess(this.accessId)) as FastlyAccess;
|
||||
const certPem = this.cert.crt;
|
||||
|
||||
if (!this.certList || this.certList.length === 0) {
|
||||
throw new Error("请至少选择一个要更新的证书");
|
||||
}
|
||||
|
||||
for (const certId of this.certList) {
|
||||
this.logger.info(`----------- 开始更新 Fastly 证书:${certId} -----------`);
|
||||
|
||||
const payload: any = {
|
||||
data: {
|
||||
type: "tls_certificate",
|
||||
id: certId,
|
||||
attributes: {
|
||||
cert_blob: certPem,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await access.doRequestApi(`/tls/certificates/${certId}`, payload, "patch");
|
||||
this.logger.info(`----------- 更新 Fastly 证书 ${certId} 成功 -----------`);
|
||||
}
|
||||
|
||||
this.logger.info("Fastly 证书批量更新完成");
|
||||
}
|
||||
|
||||
async onGetCertList() {
|
||||
const access = (await this.getAccess(this.accessId)) as FastlyAccess;
|
||||
const list = await access.getCertificates();
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("没有找到 Fastly 证书");
|
||||
}
|
||||
|
||||
const options = list.map((item: any) => {
|
||||
const name = item.attributes?.name || "Unnamed";
|
||||
const issuedTo = item.attributes?.issued_to || "Unknown Domain";
|
||||
return {
|
||||
label: `${name} - ${issuedTo} (${item.id})`,
|
||||
value: item.id,
|
||||
};
|
||||
});
|
||||
|
||||
return { list: options };
|
||||
}
|
||||
}
|
||||
|
||||
new FastlyRefreshCertPlugin();
|
||||
@@ -0,0 +1,88 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { FastlyAccess } from "../access.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "FastlyUploadCert",
|
||||
title: "Fastly-上传证书到Fastly",
|
||||
desc: "部署 / 上传 SSL/TLS 自定义证书到 Fastly CDN",
|
||||
icon: "simple-icons:fastly",
|
||||
group: pluginGroups.cdn.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class FastlyUploadCertPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput({
|
||||
title: "Access授权",
|
||||
helper: "Fastly 授权凭证",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "fastly",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "证书ID (tls_certificate_id)",
|
||||
helper: "可选。若填写则对 Fastly 已有证书进行更新(PATCH);留空则新建上传(POST)。" + "注意:Fastly 更新证书要求新证书与原证书使用相同的私钥,请确保续期时复用私钥(reuse key),否则更新会失败。",
|
||||
component: {
|
||||
placeholder: "例如: tls_cert_xxx (留空表示新建)",
|
||||
},
|
||||
required: false,
|
||||
})
|
||||
certificateId = "";
|
||||
|
||||
@TaskInput({
|
||||
title: "证书名称",
|
||||
helper: "可选。上传到 Fastly 上的自定义证书名称/标签",
|
||||
component: {
|
||||
placeholder: "例如: my-fastly-cert",
|
||||
},
|
||||
required: false,
|
||||
})
|
||||
name = "";
|
||||
|
||||
@TaskOutput({
|
||||
title: "Fastly 证书 ID",
|
||||
})
|
||||
fastlyCertId = "";
|
||||
|
||||
async onInstance() {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const { cert, accessId, certificateId, name } = this;
|
||||
const access = (await this.getAccess(accessId)) as FastlyAccess;
|
||||
|
||||
// cert.crt is the full certificate chain (PEM); cert.key is the private key (PEM)
|
||||
const certName = name && name.trim() ? name.trim() : undefined;
|
||||
const targetId = certificateId && certificateId.trim();
|
||||
|
||||
if (targetId) {
|
||||
// Update an existing Fastly certificate in place (same private key required).
|
||||
this.logger.info(`开始更新 Fastly 证书 [${targetId}]...`);
|
||||
this.fastlyCertId = await access.updateCertificate(targetId, cert.crt, certName);
|
||||
this.logger.info(`Fastly 证书更新成功, ID: ${this.fastlyCertId}`);
|
||||
} else {
|
||||
// Create a new Fastly certificate (uploads the private key first).
|
||||
this.fastlyCertId = await access.createCertificate(cert.crt, cert.key, certName);
|
||||
this.logger.info(`Fastly 证书新建成功, ID: ${this.fastlyCertId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new FastlyUploadCertPlugin();
|
||||
@@ -1 +1 @@
|
||||
00:58
|
||||
22:05
|
||||
|
||||
@@ -1 +1 @@
|
||||
01:17
|
||||
22:16
|
||||
|
||||
Reference in New Issue
Block a user