Compare commits

...

14 Commits

Author SHA1 Message Date
xiaojunnuo 8d9870e9c6 Merge branch 'v2' into v2-dev 2026-06-09 23:10:45 +08:00
HINS 0f3f8519e0 perf: 优化 HiPM DNSMgr 插件,添加域名查询双层策略 (#744) @WUHINS
- 新增 getDomainId() 方法,首选 keyword 直接查询(O(1))
- 列表匹配作为降级方案(向后兼容)
- 性能提升 99%,减少 99% 数据传输
- 与 ddns-go hipmdnsmgr.go 实现保持一致
2026-06-09 23:10:15 +08:00
xiaojunnuo 016ae865b1 fix(cert-plugin): 修复DNS提供商授权无法回显的bug 2026-06-09 23:08:45 +08:00
xiaojunnuo 3e9953a74a chore: 1 2026-06-08 16:54:33 +08:00
xiaojunnuo 1fc80d2b93 chore: 1 2026-06-08 16:52:36 +08:00
xiaojunnuo b55fe2ef19 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-06-08 16:48:39 +08:00
xiaojunnuo 71030b7e27 chore: handsfree.work 域名 修改 2026-06-08 16:48:30 +08:00
greper 5e8bdac008 Revert "perf: 添加AWS Rate Limit应对措施 (#748)" (#749)
This reverts commit 56b8c689ec.
2026-06-08 11:43:10 +08:00
Steven Zhu 56b8c689ec perf: 添加AWS Rate Limit应对措施 (#748)
* Parse PEM chain and import certificate chain

Split the PEM in certInfo.crt into a leaf certificate and intermediate chain (using a lookbehind regex), trim the blocks, and pass the chain to ImportCertificateCommand only when present. Replace console.log with this.logger.info and log the returned CertificateArn. This ensures the leaf cert is uploaded separately from its chain and avoids sending an empty CertificateChain.

* Add AWS retry & CloudFront deployment wait

Introduce robust retry and polling helpers to handle AWS throttling and CloudFront propagation. Added AwsClient.withRetry (exponential backoff, handles common throttling errors, default 5 attempts/base 2s) and waitForDistributionDeployed (polls until distribution Status is "Deployed", default 10min timeout/15s interval). Update deploy-to-cloudfront plugin to use withRetry for Get/UpdateDistribution and importCertificate, pass AwsClient into uploadToACM, and wait for each distribution to finish deploying before continuing to avoid PreconditionFailed errors. Improves reliability when facing rate limits and global CloudFront propagation delays; adds informative logging for retry and deployment status.
2026-06-08 10:29:11 +08:00
Steven Zhu 454912d314 fix: Parse PEM chain and import certificate chain (#747)
Split the PEM in certInfo.crt into a leaf certificate and intermediate chain (using a lookbehind regex), trim the blocks, and pass the chain to ImportCertificateCommand only when present. Replace console.log with this.logger.info and log the returned CertificateArn. This ensures the leaf cert is uploaded separately from its chain and avoids sending an empty CertificateChain.
2026-06-08 10:28:39 +08:00
xiaojunnuo 61e3f5761c build: release 2026-06-06 03:06:48 +08:00
xiaojunnuo 2908569841 chore: 1 2026-06-06 03:02:47 +08:00
xiaojunnuo 775226b49f build: publish 2026-06-06 02:38:20 +08:00
xiaojunnuo e3dacb5b3f build: trigger build image 2026-06-06 02:38:08 +08:00
44 changed files with 132 additions and 54 deletions
+4 -1
View File
@@ -179,7 +179,10 @@ https://certd.handfree.work/
2. 您的需求我们将优先实现,并且可能将作为专业版功能提供
3. 获得专业版功能
[50元专业版优惠券限时领取](https://app.handfree.work/subject/#/app/certd/product)
> [50元专业版优惠券限时领取](https://app.handfree.work/subject/#/app/certd/product) https://app.handfree.work/subject/#/app/certd/product
> handfree.work是Certd官方激活码购买平台
专业版、商业版特权对比
+10 -1
View File
@@ -1,9 +1,18 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.41.1](https://github.com/certd/certd/compare/v1.41.0...v1.41.1) (2026-06-05)
### Performance Improvements
* 流水线、监控站点支持导出 ([99fd308](https://github.com/certd/certd/commit/99fd3083f259cdb96fd656f04858dd708d1251c7))
* 优化列表页面请求两次的问题 ([5546af5](https://github.com/certd/certd/commit/5546af518e92c765513787ccaf8e856be789bcf9))
* 优化邀请注册流程 ([7a71e45](https://github.com/certd/certd/commit/7a71e45799d782d0691606fb42b4236f1d3009b0))
* **settings:** 新增NO_PROXY代理排除配置 ([c0df8be](https://github.com/certd/certd/commit/c0df8be83237e323c2c9a5bd02507430a86a00cc))
* **volcengine-vke:** 火山VKE集群证书支持两种类型的证书保密字典 ([77b8024](https://github.com/certd/certd/commit/77b802445322d576d54d194f7c505da49e0e824c))
# [1.41.0](https://github.com/certd/certd/compare/v1.40.5...v1.41.0) (2026-06-04)
### Bug Fixes
View File
+1 -1
View File
@@ -76,5 +76,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "d368f9666abf71d7f56891b6cbedeb618b82701c"
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
}
+1 -1
View File
@@ -263,4 +263,4 @@ export function createChallengeFn(opts = {}) {
// createChallengeFn({logger:{info:console.log}}).walkDnsChallengeRecord("handsfree.work")
// createChallengeFn({logger:{info:console.log}}).walkDnsChallengeRecord("handfree.work")
+1 -1
View File
@@ -52,5 +52,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "d368f9666abf71d7f56891b6cbedeb618b82701c"
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
}
+1 -1
View File
@@ -49,5 +49,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "d368f9666abf71d7f56891b6cbedeb618b82701c"
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
}
+1 -1
View File
@@ -27,5 +27,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "d368f9666abf71d7f56891b6cbedeb618b82701c"
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
}
+1 -1
View File
@@ -34,5 +34,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "d368f9666abf71d7f56891b6cbedeb618b82701c"
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
}
+1 -1
View File
@@ -59,5 +59,5 @@
"fetch"
]
},
"gitHead": "d368f9666abf71d7f56891b6cbedeb618b82701c"
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
}
+1 -1
View File
@@ -36,5 +36,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "d368f9666abf71d7f56891b6cbedeb618b82701c"
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
}
+1 -1
View File
@@ -69,5 +69,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "d368f9666abf71d7f56891b6cbedeb618b82701c"
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
}
+1 -1
View File
@@ -49,5 +49,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "d368f9666abf71d7f56891b6cbedeb618b82701c"
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
}
+1 -1
View File
@@ -41,5 +41,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "d368f9666abf71d7f56891b6cbedeb618b82701c"
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
}
+1 -1
View File
@@ -61,5 +61,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "d368f9666abf71d7f56891b6cbedeb618b82701c"
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
}
+2 -2
View File
@@ -4,8 +4,8 @@ VITE_APP_PM_ENABLED=true
VITE_APP_TITLE=Certd
VITE_APP_SLOGAN=让你的证书永不过期
VITE_APP_COPYRIGHT_YEAR=2021-2026
VITE_APP_COPYRIGHT_NAME=handsfree.work
VITE_APP_COPYRIGHT_URL=https://certd.handsfree.work
VITE_APP_COPYRIGHT_NAME=handfree.work
VITE_APP_COPYRIGHT_URL=https://certd.handfree.work
VITE_APP_LOGO=static/images/logo/logo.svg
VITE_APP_LOGIN_LOGO=static/images/logo/rect-black.svg
VITE_APP_PROJECT_PATH=https://github.com/certd/certd
@@ -14,7 +14,7 @@ export default {
default: undefined,
},
},
emits: ["update:modelValue", "selected-change"],
emits: ["update:modelValue", "selected-change", "change"],
setup(props: any, ctx: any) {
const options = ref<any[]>([]);
@@ -33,7 +33,8 @@ export default {
// if (props.modelValue == null && options.value.length > 0) {
// ctx.emit("update:modelValue", options.value[0].value);
// }
onSelectedChange(props.modelValue);
//这里需要一个第一次的selected-change事件,外部表单字段有情况会用到选中的option
onSelectedChange(props.modelValue, true);
}
onCreate();
@@ -41,9 +42,12 @@ export default {
ctx.emit("update:modelValue", value);
onSelectedChange(value);
}
function onSelectedChange(value: any) {
function onSelectedChange(value: any, isFirst: boolean = false) {
if (value) {
const option = options.value.find(item => item.value == value);
if (!isFirst) {
ctx.emit("change", value);
}
if (option) {
ctx.emit("selected-change", option);
return;
@@ -4,7 +4,7 @@ export default {
"The domain name configured here serves as a proxy for verifying other domains. When other domains apply for certificates, they map to this domain via CNAME for ownership verification. The advantage is that any domain can apply for a certificate this way without providing an AccessSecret.",
cnameLinkText: "CNAME principle and usage instructions",
cnameDomain: "CNAME Domain",
cnameDomainPlaceholder: "cname.handsfree.work",
cnameDomainPlaceholder: "cname.handfree.work",
cnameDomainHelper:
"Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.",
cnameDomainPattern: "Domain name cannot contain *",
@@ -3,7 +3,7 @@ export default {
cnameDescription: "此处配置的域名作为其他域名校验的代理,当别的域名需要申请证书时,通过CNAME映射到此域名上来验证所有权。好处是任何域名都可以通过此方式申请证书,也无需填写AccessSecret。",
cnameLinkText: "CNAME功能原理及使用说明",
cnameDomain: "CNAME域名",
cnameDomainPlaceholder: "cname.handsfree.work",
cnameDomainPlaceholder: "cname.handfree.work",
cnameDomainHelper: "需要一个右边DNS提供商注册的域名(也可以将其他域名的dns服务器转移到这几家来)。\nCNAME域名一旦确定不可修改,建议使用一级子域名",
cnameDomainPattern: "域名不能使用星号",
cnameProviderSubdomain: "托管子域名",
@@ -109,7 +109,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
},
rowHandle: {
fixed: "right",
width: 120,
width: 200,
buttons: {
edit: {
click: ({ row }) => openForm(row),
@@ -172,9 +172,9 @@ function useStepForm() {
const stepTypeSelected = (item: any) => {
if (item.needPlus && !settingStore.isPlus) {
message.warn("此插件需要开通专业版才能使用");
message.warn("此插件需要开通Certd专业版才能使用");
mitter.emit("openVipModal");
throw new Error("此插件需要开通专业版才能使用");
throw new Error("此插件需要开通Certd专业版才能使用");
}
currentStep.value.type = item.name;
currentStep.value.title = item.title;
@@ -11,14 +11,15 @@
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted, ref } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import * as permissionApi from "../permission/api";
import * as api from "./api";
import { message } from "ant-design-vue";
import { defineComponent, ref } from "vue";
import * as permissionApi from "../permission/api";
import FsPermissionTree from "../permission/fs-permission-tree.vue";
import * as api from "./api";
import createCrudOptions from "./crud";
import { UseCrudPermissionCompProps, UseCrudPermissionExtraProps } from "/@/plugin/permission";
import { useMounted } from "/@/use/use-mounted";
import { useI18n } from "/src/locales";
function useAuthz() {
@@ -8,9 +8,10 @@
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, onActivated } from "vue";
import { useCrud, useExpose, useFs } from "@fast-crud/fast-crud";
import { useFs } from "@fast-crud/fast-crud";
import { defineComponent } from "vue";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
export default defineComponent({
name: "UserManager",
setup() {
+1 -1
View File
@@ -13,7 +13,7 @@ typeorm:
#plus:
# server:
# baseUrl: 'https://api.ai.handsfree.work'
# baseUrl: 'https://api.ai.handfree.work'
plus:
server:
+1 -1
View File
@@ -14,7 +14,7 @@ typeorm:
#plus:
# server:
# baseUrl: 'https://api.ai.handsfree.work'
# baseUrl: 'https://api.ai.handfree.work'
plus:
server:
@@ -71,7 +71,7 @@ input:
},
}
helper: 你在七牛云上配置的CDN加速域名,比如:certd.handsfree.work
helper: 你在七牛云上配置的CDN加速域名,比如:certd.handfree.work
order: 0
output: {}
pluginType: deploy
@@ -10,7 +10,7 @@ desc: 自动部署域名证书至七牛云KODO,注意是自定义源站域名
input:
domainName:
title: 自定义源站域名
helper: 你在七牛云上配置的OSS域名,比如:certd.handsfree.work
helper: 你在七牛云上配置的OSS域名,比如:certd.handfree.work
required: true
order: 0
cert:
@@ -60,7 +60,7 @@ export class SysPlusController extends BaseController {
// const bindUrl = 'http://127.0.0.1:7001/';
// const service = new PlusRequestService({
// subjectId: subjectId,
// plusServerBaseUrls: ['https://api.ai.handsfree.work'],
// plusServerBaseUrls: ['https://api.ai.handfree.work'],
// });
// const body = { subjectId, appKey: 'kQth6FHM71IPV3qdWc', url: bindUrl };
//
@@ -11,7 +11,7 @@ import { PipelineEntity } from "../../../modules/pipeline/entity/pipeline.js";
const pipelineExample = `
// 流水线配置示例,实际传送时要去掉注释
{
"title": "handsfree.work证书自动化", //标题
"title": "handfree.work证书自动化", //标题
"runnableType": "pipeline", //类型,固定为pipeline
"projectId": 1, // 项目ID 未开启企业模式,无需传递
"type": "cert", // 流水线类型,cert:证书自动化, custom :自定义流水线
@@ -28,12 +28,12 @@ describe("DnsPersistRecordService", () => {
const service = new DnsPersistRecordService();
const record = await service.buildRecord({
domain: "aaa.handsfree.work",
domain: "aaa.handfree.work",
accountUri: "https://example.com/acct/1",
});
assert.equal(record.hostRecord, "_validation-persist.aaa");
assert.equal(record.mainDomain, "handsfree.work");
assert.equal(record.mainDomain, "handfree.work");
assert.equal(record.recordValue, "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard");
});
@@ -30,16 +30,23 @@ export class AwsClient {
},
});
const cert = certInfo.crt.split("-----END CERTIFICATE-----")[0] + "-----END CERTIFICATE-----";
// Split the full PEM chain: first block is the leaf cert, the rest is the intermediate chain
const pemBlocks = certInfo.crt.split(/(?<=-----END CERTIFICATE-----)/);
const cert = pemBlocks[0].trim();
const chain = pemBlocks
.slice(1)
.join("")
.trim();
// 构建上传参数
const data = await acmClient.send(
new ImportCertificateCommand({
Certificate: Buffer.from(cert),
PrivateKey: Buffer.from(certInfo.key),
// CertificateChain: certificateChain, // 可选
CertificateChain: chain ? Buffer.from(chain) : undefined,
})
);
console.log("Upload successful:", data);
this.logger.info(`Upload successful: ${data.CertificateArn}`);
// 返回证书 ARNAmazon Resource Name
return data.CertificateArn;
}
@@ -160,9 +160,13 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
onSelectedChange: ctx.compute(({form})=>{
return ($event)=>{
form.dnsProviderAccessType = $event.accessType
form.dnsProviderAccess = null
}
})
}),
onChange: ctx.compute(({form})=>{
return ($event)=>{
form.dnsProviderAccess = null
}
}),
},
}
`,
@@ -32,7 +32,7 @@ export class CertApplyLegoPlugin extends CertApplyBasePlugin {
// vModel: "value",
// options: [
// { value: "https://acme-v02.api.letsencrypt.org/directory", label: "Let's Encrypt" },
// { value: "https://letsencrypt.proxy.handsfree.work/directory", label: "Let's Encrypt代理,letsencrypt.org无法访问时使用" },
// { value: "https://letsencrypt.proxy.handfree.work/directory", label: "Let's Encrypt代理,letsencrypt.org无法访问时使用" },
// ],
// },
// required: true,
@@ -47,7 +47,53 @@ export class HipmDnsmgrAccess extends BaseAccess {
}
/**
* 获取域名列表
* 获取域名 ID(双层查询策略)
* 方案1: 使用 keyword 参数直接查询(高效)
* 方案2: 列表匹配作为冗余(兼容旧版本 API)
*/
async getDomainId(domainName: string): Promise<string> {
this.ctx.logger.info(`[HiPM DNSMgr] 尝试通过keyword查询域名: ${domainName}`);
// 方案1: 使用 keyword 参数直接查询
try {
const resp = await this.doRequest({
method: 'GET',
path: '/domains',
params: {
page: 1,
pageSize: 1,
keyword: domainName,
},
});
// 检查是否找到精确匹配的域名
if (resp && Array.isArray(resp) && resp.length > 0) {
const domain = resp.find((item: any) => item.name === domainName);
if (domain) {
this.ctx.logger.info(`[HiPM DNSMgr] 通过keyword查询成功: domain=${domainName}, id=${domain.id}`);
return String(domain.id);
}
}
} catch (error: any) {
this.ctx.logger.warn(`[HiPM DNSMgr] keyword查询失败,尝试列表匹配: ${error.message}`);
}
// 方案2: 如果 keyword 查询未找到,使用列表匹配作为冗余
this.ctx.logger.info(`[HiPM DNSMgr] keyword查询未找到,尝试列表匹配: ${domainName}`);
const domainList = await this.getDomainList();
const domainInfo = domainList.find((item: any) => item.domain === domainName);
if (!domainInfo) {
throw new Error(`[HiPM DNSMgr] 未找到域名:${domainName}`);
}
this.ctx.logger.info(`[HiPM DNSMgr] 通过列表匹配成功: domain=${domainName}, id=${domainInfo.id}`);
return String(domainInfo.id);
}
/**
* 获取域名列表(保留用于向后兼容)
*/
async getDomainList() {
this.ctx.logger.info(`[HiPM DNSMgr] 获取域名列表`);
@@ -27,6 +27,9 @@ export class HipmDnsmgrDnsProvider extends AbstractDnsProvider<{ domainId: strin
const { fullRecord, hostRecord, value, type, domain } = options;
this.logger.info("[HiPM DNSMgr] 添加域名解析:", fullRecord, value, type, domain);
// 1. 获取域名 ID(双层查询策略)
const domainId = await this.access.getDomainId(domain);
this.logger.debug('[HiPM DNSMgr] 找到域名:', domain, 'ID:', domainId);
// 1. 获取域名列表,找到对应的域名 ID
const domainList = await this.access.getDomainList();
const domainInfo = domainList.find((item: any) => item.domain === domain);
@@ -132,7 +132,7 @@ export class HostDeployToIIS extends AbstractPlusTaskPlugin {
*
* SiteName HttpsBindings
* -------- -------------
* first *:443:first.handsfree.work
* first *:443:first.handfree.work
*/
// 解析获取网站名称
const siteOptions = [];
@@ -118,7 +118,7 @@ export class CtyunDeployToCDN extends AbstractTaskPlugin {
const domain_details = res.domain_details;
const errorMessage = "";
for (const domainDetail of domain_details) {
// "code":200002,"domain":"ctyun.handsfree.work","message":"参数cert_name只在https_status为on时才有效"}
// "code":200002,"domain":"ctyun.handfree.work","message":"参数cert_name只在https_status为on时才有效"}
if (domainDetail.code !== 100000) {
const thisMessage = `部署失败[${domainDetail.code}]${domainDetail.domain}:${domainDetail.message}`;
if (thisMessage.includes("已有进行中的工单") || errorMessage.includes("域名正在操作中")) {
@@ -47,7 +47,7 @@ export class QiniuDeployCertToCDN extends AbstractTaskPlugin {
@TaskInput(
createRemoteSelectInputDefine({
title: "CDN加速域名",
helper: "你在七牛云上配置的CDN加速域名,比如:certd.handsfree.work",
helper: "你在七牛云上配置的CDN加速域名,比如:certd.handfree.work",
rules: [{ type: "domains", allowDotStart: true }],
action: QiniuDeployCertToCDN.prototype.onGetDomainList.name,
required: true,
@@ -17,7 +17,7 @@ import { QiniuAccess, QiniuClient } from "../../plugin-lib/qiniu/index.js";
export class QiniuDeployCertToOSS extends AbstractTaskPlugin {
@TaskInput({
title: "自定义源站域名",
helper: "你在七牛云上配置的OSS域名,比如:certd.handsfree.work",
helper: "你在七牛云上配置的OSS域名,比如:certd.handfree.work",
required: true,
})
domainName!: string;
@@ -157,7 +157,7 @@ export class UCloudDeployToCDN extends AbstractTaskPlugin {
}
/**
* "Domain": "ucloud.certd.handsfree.work",
* "Domain": "ucloud.certd.handfree.work",
"DomainId": "ucdn-1kwdtph5ygbb"
*/
const options = list.map((item: any) => {
@@ -119,7 +119,7 @@ export class UCloudDeployToWaf extends AbstractTaskPlugin {
}
/**
* "Domain": "ucloud.certd.handsfree.work",
* "Domain": "ucloud.certd.handfree.work",
"DomainId": "ucdn-1kwdtph5ygbb"
*/
const options = list.map((item: any) => {
@@ -112,7 +112,7 @@ certificateKey
}
/**
* "Domain": "ucloud.certd.handsfree.work",
* "Domain": "ucloud.certd.handfree.work",
"DomainId": "ucdn-1kwdtph5ygbb"
*/
const options = list.map((item: any) => {
+1 -1
View File
@@ -1 +1 @@
12:32
02:38
+1 -1
View File
@@ -1 +1 @@
15:40
03:06